chore: import upstream snapshot with attribution
Publish Docker Image / publish (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:39:36 +08:00
commit 5bdf4cc89a
423 changed files with 88197 additions and 0 deletions
@@ -0,0 +1,75 @@
# Adding Resume Templates
> Guide for adding new resume template layouts.
## Quick Start
1. Create `components/resume/resume-{name}.tsx`
2. Export from `components/resume/index.ts`
3. Add to `FormattingControls` template selector
4. Create thumbnail image
## Template Component
```tsx
interface TemplateProps {
resumeData: ResumeData;
settings: TemplateSettings;
}
export function ResumeNewTemplate({ resumeData, settings }: TemplateProps) {
return (
<div className="resume-print">
{/* Header */}
<header className="resume-section">
<h1>{resumeData.personalInfo?.name}</h1>
</header>
{/* Sections */}
{getSortedSections(resumeData).map(section => (
<section key={section.id} className="resume-section">
<h3 className="resume-section-title">{section.displayName}</h3>
<div className="resume-items">
{/* Items */}
</div>
</section>
))}
</div>
);
}
```
## CSS Classes Required
```css
.resume-print /* Root container (Playwright waits for this) */
.resume-section /* Section wrapper */
.resume-section-title /* Section heading */
.resume-items /* Items container */
.resume-item /* Individual entry (won't page-break) */
```
## Export Template
```typescript
// components/resume/index.ts
export { ResumeNewTemplate } from './resume-new-template';
```
## Add to Selector
```typescript
// components/builder/formatting-controls.tsx
const TEMPLATES = [
{ id: 'swiss-single', name: 'Single Column' },
{ id: 'swiss-two-column', name: 'Two Column' },
{ id: 'new-template', name: 'New Template' }, // Add here
];
```
## Testing
1. Load builder with new template
2. Check all sections render
3. Verify PDF generation works
4. Test with 2+ page content
@@ -0,0 +1,77 @@
# Application Tracker Feature
> **A Kanban board for managing the job-application pipeline, auto-populated from the tailor flow.**
## Overview
The Application Tracker (`/tracker`) gives each tailored resume a place in a
seven-column Kanban pipeline. Tailoring a resume to a job auto-creates an
`applied` card; users can also add cards manually from a pasted job
description. Cards are drag-and-drop reorderable within and across columns.
## Columns (stable keys, decoupled from i18n labels)
`saved` · `applied` · `no_response` · `response` · `interview` · `accepted` · `rejected`
Auto-created cards from the tailor flow land in **`applied`**. Manual cards
default to `applied` but can be created as `saved`.
## How It Works
1. **Auto-create:** `POST /resumes/improve/confirm` (and the legacy
`POST /resumes/improve`) create an `applied` card after persisting the
tailored resume — best-effort (a tracker failure never breaks tailoring).
Company/role come from the cached keyword-extraction pass, so there is **no
extra LLM call** on this path.
2. **Manual add:** `POST /applications` creates the job from the pasted JD then
the card; when company/role aren't supplied it runs one best-effort
extraction call (falls back to blank/editable).
3. **Drag/drop:** cards reorder within a column or move across columns; the
board updates optimistically and reverts on a failed `PATCH`.
4. **Detail modal:** shows the JD + the applied resume; **Edit** opens
`/builder?id=<resume_id>`. Tolerates a deleted resume (`resume: null`).
5. **Bulk actions:** multi-select cards to move or delete in one request.
## Data Model
`Application` (SQLite, `apps/backend/app/models.py`): `application_id` (PK),
`job_id`, `resume_id` (the applied/tailored resume), `master_resume_id`
(optional base — powers the "shared resume" badge), `status` (7-key enum),
`company`, `role`, `applied_at`, `notes`, `position` (per-column order,
server-renumbered on PATCH), `created_at`, `updated_at`. `create_application`
dedupes on `(job_id, resume_id)` to survive double-submit.
## API (`prefix=/applications`, mounted under `/api/v1`)
| Method | Path | Purpose |
|--------|------|---------|
| GET | `/applications` | All cards grouped by column (all 7 keys present) |
| POST | `/applications` | Manual add (creates job + card; best-effort extraction) |
| GET | `/applications/{id}` | Card + embedded JD + resume (resume null if deleted) |
| PATCH | `/applications/{id}` | Update status/position/notes/company/role/applied_at |
| PATCH | `/applications/bulk` | Move many cards to one column |
| DELETE | `/applications/{id}` | Delete one card |
| POST | `/applications/bulk-delete` | Delete many cards |
## Key Files
| File | Purpose |
|------|---------|
| `apps/backend/app/models.py` | `Application` ORM model |
| `apps/backend/app/schemas/applications.py` | Pydantic request/response schemas + status enum |
| `apps/backend/app/routers/applications.py` | The tracker endpoints |
| `apps/backend/app/database.py` | Facade CRUD/bulk/reorder methods |
| `apps/backend/app/routers/resumes.py` | `_auto_create_tracker_application` hook (both confirm paths) |
| `apps/backend/app/services/improver.py` + `app/prompts/templates.py` | Company/role added to keyword extraction |
| `apps/frontend/app/(default)/tracker/page.tsx` | Route |
| `apps/frontend/components/tracker/*` | Board, column, card, detail modal, bulk bar, manual-add dialog |
| `apps/frontend/components/tracker/reorder.ts` | Pure drag-end resolution (`planMove`) |
| `apps/frontend/lib/api/tracker.ts` | Typed API client |
## Tests
- Backend: `tests/integration/test_applications_api.py` (CRUD, grouping, detail
tolerance, bulk), `tests/integration/test_tracker_autocreate.py` (confirm
auto-creates an `applied` card), `tests/unit/test_database.py::TestApplications`.
- Frontend: `tests/tracker-reorder.test.ts` (`planMove` within/cross-column +
empty-column drop), `tests/api-tracker.test.ts` (client payloads/URLs).
+68
View File
@@ -0,0 +1,68 @@
# Custom Sections System
> **Dynamic resume sections with full customization.**
## Section Types
| Type | Description | Example Uses |
|------|-------------|--------------|
| `personalInfo` | Special type for header (always first) | Name, contact details |
| `text` | Single text block | Summary, objective, statement |
| `itemList` | Array of items with title, subtitle, years, description | Experience, projects, publications |
| `stringList` | Simple array of strings | Skills, languages, hobbies |
## Section Features
- **Rename sections**: Change display names (e.g., "Education" → "Academic Background")
- **Reorder sections**: Up/down buttons to change section order
- **Hide sections**: Toggle visibility (hidden sections still editable, just not in PDF)
- **Delete sections**: Remove custom sections entirely
- **Add custom sections**: Create new sections with any name and type
## Section Controls (UI)
Each section (except Personal Info) has these controls in the header:
| Control | Icon | Function |
|---------|------|----------|
| Visibility | 👁 Eye / EyeOff | Toggle show/hide in PDF preview |
| Move Up | ⬆ ChevronUp | Move section earlier in order |
| Move Down | ⬇ ChevronDown | Move section later in order |
| Rename | ✏️ Pencil | Edit section display name |
| Delete | 🗑 Trash | Hide (default) or delete (custom) |
## Hidden Section Behavior
- Hidden sections appear in the form with:
- Dashed border and 60% opacity
- "Hidden from PDF" badge (amber)
- Hidden sections are still editable
- Only PDF/preview hides them (uses `getSortedSections` which filters by visibility)
- Form shows all sections (uses `getAllSections`)
## Key Files
| File | Purpose |
|------|---------|
| `apps/backend/app/schemas/models.py` | `SectionType`, `SectionMeta`, `CustomSection` models |
| `apps/frontend/lib/utils/section-helpers.ts` | Section management utilities |
| `apps/frontend/components/builder/section-header.tsx` | Section controls UI |
| `apps/frontend/components/builder/add-section-dialog.tsx` | Add custom section dialog |
| `apps/frontend/components/builder/resume-form.tsx` | Dynamic form rendering |
| `apps/frontend/components/resume/dynamic-resume-section.tsx` | Renders custom sections in templates |
## Data Structure
```typescript
interface ResumeData {
// ... existing fields (personalInfo, summary, etc.)
sectionMeta?: SectionMeta[]; // Section order, names, visibility
customSections?: Record<string, CustomSection>; // Custom section data
}
```
## Migration
Existing resumes are automatically migrated via lazy normalization - default section metadata is added when a resume is fetched if `sectionMeta` is missing.
> **Important**: The `normalize_resume_data()` function uses `copy.deepcopy(DEFAULT_SECTION_META)` to avoid shared mutable reference bugs. Always use deep copies when assigning default mutable values.
+42
View File
@@ -0,0 +1,42 @@
# Resume Enrichment Feature
> **AI-powered resume improvement with targeted questions.**
## Overview
The enrichment feature helps users improve their master resume with more detailed content by:
1. Analyzing the resume for weak/generic descriptions
2. Asking targeted questions about their experience
3. Generating additional bullet points based on answers
## How It Works
1. User clicks "Enhance Resume" on the master resume page
2. AI analyzes the resume and identifies weak/generic descriptions
3. User answers targeted questions (max 6 questions total) about their experience
4. AI generates additional bullet points based on user answers
5. New bullets are **added** to existing content (not replaced)
## Key Design Decisions
- **Maximum 6 questions**: To avoid overwhelming users, the AI generates at most 6 questions across all items
- **Additive enhancement**: Original bullet points are preserved; new enhanced bullets are appended after them
- **Question prioritization**: AI prioritizes the most impactful questions that will yield the best improvements
## Key Files
| File | Purpose |
|------|---------|
| `apps/backend/app/prompts/enrichment.py` | AI prompts for analysis and enhancement |
| `apps/backend/app/routers/enrichment.py` | API endpoints for enrichment workflow |
| `apps/frontend/hooks/use-enrichment-wizard.ts` | React state management for wizard flow |
| `apps/frontend/components/enrichment/*.tsx` | UI components for enrichment modal |
## API Endpoints
| Endpoint | Description |
|----------|-------------|
| `POST /enrichment/analyze/{resume_id}` | Analyze resume and generate questions |
| `POST /enrichment/enhance` | Generate enhanced descriptions from answers |
| `POST /enrichment/apply/{resume_id}` | Apply enhancements to resume |
+66
View File
@@ -0,0 +1,66 @@
# i18n Preparation Guide
> Plan for internationalizing Resume Matcher.
## Current State
- UI uses `next-intl` with locale files in `messages/`
- Content language preference stored via `LanguageProvider`
- Supported: en, es, zh, ja
## Translation File Location
```
apps/frontend/messages/
├── en.json
├── es.json
├── zh.json
└── ja.json
```
## Adding New Locale
1. Create `messages/{locale}.json`
2. Add locale to `i18n/config.ts`:
```typescript
export const locales = ['en', 'es', 'zh', 'ja', 'de'] as const;
```
3. Add to `SUPPORTED_LANGUAGES` in backend `config.py`
## Translation Keys
```json
{
"dashboard": {
"title": "Dashboard",
"masterResume": "Master Resume"
},
"builder": {
"save": "Save",
"download": "Download PDF"
}
}
```
## Usage in Components
```tsx
import { useTranslations } from 'next-intl';
export function MyComponent() {
const t = useTranslations('dashboard');
return <h1>{t('title')}</h1>;
}
```
## Content Language vs UI Language
- **UI Language:** Controlled by `next-intl`, affects interface text
- **Content Language:** Controlled by `LanguageProvider`, affects LLM-generated content (cover letters, tailored resumes)
## Backend i18n (Future)
Currently prompts are English-only. To support multiple languages:
1. Create `app/i18n/locales/{lang}.json`
2. Add language parameter to prompt templates
3. Pass `Accept-Language` header from frontend
+64
View File
@@ -0,0 +1,64 @@
# Internationalization (i18n)
> **Multi-language UI and content generation.**
## Supported Languages
| Code | Language | Native Name | Flag | Message File |
|------|----------|-------------|------|--------------|
| `en` | English | English | 🇺🇸 | `messages/en.json` |
| `es` | Spanish | Español | 🇪🇸 | `messages/es.json` |
| `zh` | Chinese (Simplified) | 中文 | 🇨🇳 | `messages/zh.json` |
| `ja` | Japanese | 日本語 | 🇯🇵 | `messages/ja.json` |
| `pt` | Portuguese (Brazilian) | Português | 🇧🇷 | `messages/pt-BR.json` |
> Source of truth: `apps/frontend/i18n/config.ts` (`locales`, `localeNames`, `localeFlags`). Note `pt` loads from `messages/pt-BR.json` (imported as `pt` in `apps/frontend/lib/i18n/messages.ts`).
## Two Language Settings
1. **UI Language** - Interface text (buttons, labels, navigation)
2. **Content Language** - LLM-generated content (resumes, cover letters)
Both are configured independently in the Settings page.
## How It Works
- **UI translations**: Simple JSON import approach, no external dependencies
- **Content generation**: Backend receives language, passes to LLM prompts via `{output_language}`
- **Existing content** in database remains in original language
## Key Files
| File | Purpose |
|------|---------|
| `apps/frontend/messages/*.json` | UI translation files (`en`, `es`, `zh`, `ja`, `pt-BR`) |
| `apps/frontend/lib/i18n/translations.ts` | `useTranslations` hook |
| `apps/frontend/lib/context/language-context.tsx` | LanguageProvider (UI + content) |
| `apps/backend/app/prompts/templates.py` | LLM prompts with `{output_language}` |
## Using Translations
```typescript
import { useTranslations } from '@/lib/i18n';
const { t } = useTranslations();
<button>{t('common.save')}</button>
```
## Storage
| Key | Purpose |
|-----|---------|
| `resume_matcher_ui_language` | UI language (localStorage only) |
| `resume_matcher_content_language` | Content language (localStorage + backend) |
## Adding a New Language
1. Create `apps/frontend/messages/{code}.json` with all translations
2. Add locale to `apps/frontend/i18n/config.ts`
3. Add language name to `apps/backend/app/prompts/templates.py`
4. Update `SUPPORTED_LANGUAGES` in backend config router
## Related Docs
- [i18n-preparation.md](i18n-preparation.md) - Detailed extraction plan
+39
View File
@@ -0,0 +1,39 @@
# JD Match Feature
> **Shows how well a tailored resume matches the original job description.**
## Overview
The Resume Builder includes a "JD Match" tab that shows how well a tailored resume matches the original job description.
## How It Works
1. User tailors a resume against a job description
2. Opens the tailored resume in the Builder
3. "JD MATCH" tab appears (only for tailored resumes)
4. Shows side-by-side comparison:
- **Left panel**: Original job description (read-only)
- **Right panel**: Resume with matching keywords highlighted in yellow
## Features
- **Keyword extraction**: Extracts significant keywords from JD (filters common stop words)
- **Case-insensitive matching**: Matches keywords regardless of case
- **Match statistics**: Shows total keywords, matches found, and match percentage
- **Color-coded percentage**: Green (≥50%), yellow (≥30%), red (<30%)
## Key Files
| File | Purpose |
|------|---------|
| `apps/frontend/lib/utils/keyword-matcher.ts` | Keyword extraction and matching utilities |
| `apps/frontend/components/builder/jd-comparison-view.tsx` | Main split-view component |
| `apps/frontend/components/builder/jd-display.tsx` | Read-only JD display |
| `apps/frontend/components/builder/highlighted-resume-view.tsx` | Resume with keyword highlighting |
| `apps/backend/app/routers/resumes.py` | `GET /{resume_id}/job-description` endpoint |
## API Endpoint
| Endpoint | Description |
|----------|-------------|
| `GET /resumes/{resume_id}/job-description` | Fetch JD used to tailor a resume |
+71
View File
@@ -0,0 +1,71 @@
# Resume Template Settings
> **Template types and extensive formatting controls.**
## Template Types
| Template | Description |
|----------|-------------|
| `swiss-single` | Traditional single-column layout with maximum content density |
| `swiss-two-column` | 65%/35% split with experience in main column, skills in sidebar |
| `modern` | Single-column with colorful accent headers and customizable theme colors |
| `modern-two-column` | Two-column layout combining modern accents with space-efficient design |
| `latex` | Classic serif single-column with Title-Case ruled headers and company-first entries (LaTeX-style). Single-typeface — driven by the Header Font control |
| `clean` | Minimal sans single-column with large understated gray UPPERCASE headers and single-line entries. Single-typeface — driven by the Body Font control |
| `vivid` | Colorful two-column (Awesome-CV lineage): two-tone accent name, monospace contact with circular icons, accent small-caps headers, accent arrow bullets. Supports the Accent Color control |
## Formatting Controls
| Control | Range | Default | Effect |
|---------|-------|---------|--------|
| Margins | 5-25mm | 8mm | Page margins |
| Section Spacing | 1-5 | 3 | Gap between major sections |
| Item Spacing | 1-5 | 2 | Gap between items within sections |
| Line Height | 1-5 | 3 | Text line height |
| Base Font Size | 1-5 | 3 | Overall text scale (11-16px) |
| Header Scale | 1-5 | 3 | Name/section header size multiplier |
| Header Font | serif/sans-serif/mono | serif | Font family for headers |
| Body Font | serif/sans-serif/mono | sans-serif | Font family for body text |
| Compact Mode | boolean | false | Apply 0.6x spacing multiplier (spacing only; margins unchanged) |
| Contact Icons | boolean | false | Show icons next to contact info |
| Accent Color | blue/green/orange/red | blue | Accent color for color templates (modern, modern-two-column, vivid) |
## Key Files
| File | Purpose |
|------|---------|
| `apps/frontend/lib/types/template-settings.ts` | Type definitions, defaults, CSS variable mapping |
| `apps/frontend/components/resume/styles/_tokens.css` | Global design tokens (colors) |
| `apps/frontend/components/resume/styles/_base.module.css` | Shared typography and layout styles |
| `apps/frontend/components/builder/formatting-controls.tsx` | UI controls for template settings |
| `apps/frontend/components/resume/resume-single-column.tsx` | Single column template |
| `apps/frontend/components/resume/resume-two-column.tsx` | Two column template |
| `apps/frontend/components/resume/resume-modern.tsx` | Modern single column template |
| `apps/frontend/components/resume/resume-modern-two-column.tsx` | Modern two column template |
| `apps/backend/app/routers/resumes.py` | PDF generation endpoint with accentColor support |
## CSS Variables
Templates use CSS custom properties for styling:
- `--section-gap`, `--item-gap`, `--line-height` - Spacing
- `--font-size-base`, `--header-scale`, `--section-header-scale` - Typography
- `--header-font` - Header font family
- `--body-font` - Body text font family
- `--margin-top/bottom/left/right` - Page margins
- `--accent-primary`, `--accent-light` - Accent colors for Modern templates
> **Note**: Templates should use the styles exported from `apps/frontend/components/resume/styles/_base.module.css` (e.g., `baseStyles['resume-section']`, `baseStyles['resume-item-subtitle']`) to ensure all spacing and typography respond to template settings.
### Typography Classes
The base stylesheet includes specialized classes for improved subtitle visibility:
| Class | Font Size | Weight | Usage |
|-------|-----------|--------|-------|
| `resume-item-subtitle` | 0.95× base | 600 | Company names, education degrees, project roles |
| `resume-item-subtitle-sm` | 0.88× base | 600 | Same fields in compact two-column layouts |
These classes provide **better visibility** than the generic `resume-meta` class (0.82× base, weight 400), making subtitles 13-16% larger and semi-bold.
Formatting controls include an "Effective Output" summary that reflects compact-mode adjustments for spacing/line-height.