cover letter type feature

#1
by aiivar - opened
Files changed (1) hide show
  1. app.py +226 -19
app.py CHANGED
@@ -48,9 +48,16 @@ COVER_LETTER_INSTRUCTIONS = """
48
  <div style="padding: 10px; border-radius: 6px; border: 1px solid #ddd; margin-bottom: 10px;">
49
  <p><strong>Инструкция для письма:</strong></p>
50
  <ol>
51
- <li>Загрузите резюме и вакансию на первой вкладке.</li>
52
- <li>Потом нажмите “Generate Cover Letter”.</li>
 
53
  </ol>
 
 
 
 
 
 
54
  </div>
55
  """
56
 
@@ -64,6 +71,107 @@ QA_INSTRUCTIONS = """
64
  </div>
65
  """
66
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  # ============================
68
  # Models (local transformers)
69
  # ============================
@@ -563,6 +671,7 @@ def analyze_resume(
563
  def generate_cover_letter(
564
  resume_text: str,
565
  job_description_text: str,
 
566
  ) -> str:
567
  resume_text = normalize_text(resume_text)
568
  job_description_text = normalize_text(job_description_text)
@@ -571,29 +680,35 @@ def generate_cover_letter(
571
  return "❗ Сначала загрузите резюме в первой вкладке."
572
  if not job_description_text:
573
  return "❗ Вставьте вакансию (текстом или файлом) в первой вкладке."
574
-
 
 
 
 
 
 
575
  r_ctx, j_ctx = select_context_for_llm(resume_text, job_description_text, max_chars=6500)
576
-
577
- prompt = f"""
578
- Напиши сопроводительное письмо на русском на 8–10 предложений.
579
- Требования:
580
- - Без заголовков и списков, только текст письма.
581
- - Не выдумывай опыт/технологии: используй только факты из резюме.
582
- - Привяжи письмо к требованиям вакансии.
583
-
584
- Вакансия:
585
- {j_ctx}
586
-
587
- Выдержки резюме:
588
- {r_ctx}
589
- """
590
  return groq_chat(prompt, "You are an expert in writing tailored cover letters.", LETTER_MODEL, DEFAULT_MAX_TOKENS_LETTER)
591
 
592
 
593
  # ============================
594
  # UI
595
  # ============================
596
- with gr.Blocks() as demo:
 
 
 
 
 
 
 
 
 
 
 
597
  gr.HTML(TITLE)
598
 
599
  with gr.Tab("Resume Analyzer"):
@@ -638,8 +753,100 @@ with gr.Blocks() as demo:
638
 
639
  with gr.Tab("Cover Letter Generator"):
640
  gr.HTML(COVER_LETTER_INSTRUCTIONS)
641
- generate_cl_btn = gr.Button("Generate Cover Letter")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
642
  cover_letter_output = gr.Markdown()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
643
 
644
  # NEW TAB: local transformer Q&A
645
  with gr.Tab("Q&A (Local Transformer)"):
 
48
  <div style="padding: 10px; border-radius: 6px; border: 1px solid #ddd; margin-bottom: 10px;">
49
  <p><strong>Инструкция для письма:</strong></p>
50
  <ol>
51
+ <li>Загрузите резюме и вакансию на первой вкладке</li>
52
+ <li>Выберите стиль сопроводительного письма</li>
53
+ <li>Нажмите "Generate Cover Letter"</li>
54
  </ol>
55
+ <p style="margin-top: 10px; font-size: 14px; color: #666;">
56
+ <strong>Совет:</strong> Попробуйте разные стили для разных типов компаний:
57
+ <br>• Формальный — для корпораций и банков
58
+ <br>• Современный — для IT-компаний и стартапов
59
+ <br>• Креативный — для дизайнеров, маркетологов
60
+ </p>
61
  </div>
62
  """
63
 
 
71
  </div>
72
  """
73
 
74
+ # ============================
75
+ # Cover Letter Types
76
+ # ============================
77
+ COVER_LETTER_TYPES = {
78
+ "formal": {
79
+ "name": "Формальный стиль",
80
+ "description": "Классическое деловое письмо, строгое и профессиональное",
81
+ "prompt_template": """Напиши формальное сопроводительное письмо на русском языке в деловом стиле. Требования:
82
+ - Используй официально-деловой стиль с уважительным обращением
83
+ - Структура: приветствие, представление, соответствие требованиям вакансии, заключение
84
+ - Объём: 8-10 предложений
85
+ - Избегай разговорных выражений, используй стандартные формулировки
86
+ - Ссылайся на конкретные требования вакансии
87
+
88
+ Вакансия:
89
+ {j_ctx}
90
+
91
+ Выдержки резюме:
92
+ {r_ctx}"""
93
+ },
94
+ "modern": {
95
+ "name": "Современный стиль",
96
+ "description": "Современный прямой стиль, популярный в IT и стартапах",
97
+ "prompt_template": """Напиши современное сопроводительное письмо на русском языке. Требования:
98
+ - Современный, прямой стиль без излишней формальности
99
+ - Акцент на конкретных результатах и метриках
100
+ - Структура: краткое введение, ключевые достижения, почему подхожу, призыв к действию
101
+ - 6-8 предложений, без шаблонных фраз
102
+ - Используй активные глаголы и конкретные примеры
103
+
104
+ Вакансия:
105
+ {j_ctx}
106
+
107
+ Выдержки резюме:
108
+ {r_ctx}"""
109
+ },
110
+ "technical": {
111
+ "name": "Технический фокус",
112
+ "description": "С акцентом на технические навыки и конкретные технологии",
113
+ "prompt_template": """Напиши технически ориентированное сопроводительное письмо на русском языке для IT-off специалиста. Требования:
114
+ - Фокус на технических навыках и технологиях из вакансии
115
+ - Конкретные примеры использования технологий из резюме
116
+ - Упоминание методологий, инструментов, фреймворков
117
+ - Структура: техническое соответствие, опыт работы с конкретным стеком, релевантные проекты
118
+ - Объём: 7-9 предложений
119
+
120
+ Вакансия:
121
+ {j_ctx}
122
+
123
+ Выдержки резюме:
124
+ {r_ctx}"""
125
+ },
126
+ "creative": {
127
+ "name": "Креативный подход",
128
+ "description": "Творческий стиль для дизайнеров, маркетологов, копирайтеров",
129
+ "prompt_template": """Напиши креативное сопроводительное письмо на русском языке. Требования:
130
+ - Креативный, нешаблонный подход, но сохраняя профессионализм
131
+ - Можно использовать метафоры или нестандартные сравнения (если уместно)
132
+ - Покажи творческий подход через структуру или формулировки
133
+ - Подчеркни креативные достижения и проекты
134
+ - Об��ём: 8-10 предложений, можно чуть больше
135
+
136
+ Вакансия:
137
+ {j_ctx}
138
+
139
+ Выдержки резюме:
140
+ {r_ctx}"""
141
+ },
142
+ "minimal": {
143
+ "name": "Минималистичный",
144
+ "description": "Краткое, по существу, без лишних слов",
145
+ "prompt_template": """Напиши минималистичное сопроводительное письмо на русском языке. Требования:
146
+ - Максимально кратко, без вводных слов и шаблонных фраз
147
+ - Только самое важное: соответствие требованиям, ключевой опыт
148
+ - 4-6 предложений, только по существу
149
+ - Прямой стиль, без эмоциональных окрасов
150
+
151
+ Вакансия:
152
+ {j_ctx}
153
+
154
+ Выдержки резюме:
155
+ {r_ctx}"""
156
+ },
157
+ "impact": {
158
+ "name": "Результато-ориентированный",
159
+ "description": "С акцентом на конкретные результаты и достижения",
160
+ "prompt_template": """Напиши сопроводительное письмо на русском с фокусом на результаты и достижения. Требования:
161
+ - Каждый абзац начинай с конкретного результата или достижения
162
+ - Используй метрики и цифры из резюме
163
+ - Связывай свои достижения с потребностями вакансии
164
+ - Структура: ключевой результат, как он достигался, как поможет компании
165
+ - 6-8 предложений
166
+
167
+ Вакансия:
168
+ {j_ctx}
169
+
170
+ Выдержки резюме:
171
+ {r_ctx}"""
172
+ }
173
+ }
174
+
175
  # ============================
176
  # Models (local transformers)
177
  # ============================
 
671
  def generate_cover_letter(
672
  resume_text: str,
673
  job_description_text: str,
674
+ letter_type: str = "modern"
675
  ) -> str:
676
  resume_text = normalize_text(resume_text)
677
  job_description_text = normalize_text(job_description_text)
 
680
  return "❗ Сначала загрузите резюме в первой вкладке."
681
  if not job_description_text:
682
  return "❗ Вставьте вакансию (текстом или файлом) в первой вкладке."
683
+
684
+ # Проверка наличия типа письма
685
+ if letter_type not in COVER_LETTER_TYPES:
686
+ letter_type = "modern"
687
+
688
+ letter_config = COVER_LETTER_TYPES[letter_type]
689
+
690
  r_ctx, j_ctx = select_context_for_llm(resume_text, job_description_text, max_chars=6500)
691
+
692
+ prompt = letter_config["prompt_template"].format(r_ctx=r_ctx, j_ctx=j_ctx)
693
+
 
 
 
 
 
 
 
 
 
 
 
694
  return groq_chat(prompt, "You are an expert in writing tailored cover letters.", LETTER_MODEL, DEFAULT_MAX_TOKENS_LETTER)
695
 
696
 
697
  # ============================
698
  # UI
699
  # ============================
700
+ with gr.Blocks(css="""
701
+ .style-btn { margin-right: 8px; margin-bottom: 8px; }
702
+ .style-btn:hover { transform: translateY(-2px); transition: all 0.2s; }
703
+ .cover-letter-output {
704
+ border: 1px solid #e5e7eb;
705
+ border-radius: 8px;
706
+ padding: 20px;
707
+ background-color: #fafafa;
708
+ white-space: pre-wrap;
709
+ line-height: 1.6;
710
+ }
711
+ """) as demo:
712
  gr.HTML(TITLE)
713
 
714
  with gr.Tab("Resume Analyzer"):
 
753
 
754
  with gr.Tab("Cover Letter Generator"):
755
  gr.HTML(COVER_LETTER_INSTRUCTIONS)
756
+
757
+ with gr.Row():
758
+ with gr.Column(scale=1):
759
+ letter_type = gr.Dropdown(
760
+ choices=[(config["name"], key) for key, config in COVER_LETTER_TYPES.items()],
761
+ value="modern",
762
+ label="Тип сопроводительного письма",
763
+ info="Выберите стиль письма"
764
+ )
765
+
766
+ # Превью описания стиля
767
+ type_description = gr.Markdown(
768
+ value=COVER_LETTER_TYPES["modern"]["description"],
769
+ label="Описание стиля"
770
+ )
771
+
772
+ # Быстрые кнопки выбора ��тиля
773
+ gr.Markdown("**Быстрый выбор:**")
774
+ with gr.Row():
775
+ for key in ["formal", "modern", "technical"]:
776
+ config = COVER_LETTER_TYPES[key]
777
+ btn = gr.Button(
778
+ config["name"],
779
+ size="sm",
780
+ variant="secondary",
781
+ elem_classes="style-btn"
782
+ )
783
+ btn.click(
784
+ lambda k=key: gr.update(value=k),
785
+ inputs=[],
786
+ outputs=[letter_type]
787
+ ).then(
788
+ update_type_description,
789
+ inputs=[letter_type],
790
+ outputs=[type_description]
791
+ )
792
+
793
+ generate_cl_btn = gr.Button("Generate Cover Letter", variant="primary")
794
+
795
+ with gr.Column(scale=2):
796
+ # Быстрые превью стилей
797
+ gr.Markdown("### Быстрый просмотр стилей:")
798
+
799
+ style_previews = []
800
+ for key, config in list(COVER_LETTER_TYPES.items())[:3]: # Показываем первые 3
801
+ style_previews.append(
802
+ f"""
803
+ <div style="border: 1px solid #e5e7eb; border-radius: 8px; padding: 12px; margin-bottom: 10px;">
804
+ <div style="font-weight: 600; color: #374151;">{config['name']}</div>
805
+ <div style="font-size: 13px; color: #6b7280;">{config['description']}</div>
806
+ </div>
807
+ """
808
+ )
809
+ gr.HTML("".join(style_previews))
810
+
811
+ # Обновление описания при смене типа
812
+ def update_type_description(letter_type_key: str):
813
+ config = COVER_LETTER_TYPES.get(letter_type_key, COVER_LETTER_TYPES["modern"])
814
+ return gr.update(value=config["description"])
815
+
816
+ letter_type.change(
817
+ update_type_description,
818
+ inputs=[letter_type],
819
+ outputs=[type_description]
820
+ )
821
+
822
+ # Отдельный блок для вывода с информацией о стиле
823
+ cover_letter_header = gr.Markdown("### Сгенерированное письмо:")
824
+ cover_letter_style_info = gr.Markdown(visible=False)
825
  cover_letter_output = gr.Markdown()
826
+
827
+ # Функция для отображения информации о стиле
828
+ def show_cover_letter_with_style(letter_text: str, letter_type_key: str):
829
+ if letter_text.startswith("❗"):
830
+ return gr.update(value=""), gr.update(visible=False), gr.update(value=letter_text)
831
+
832
+ config = COVER_LETTER_TYPES.get(letter_type_key, COVER_LETTER_TYPES["modern"])
833
+ style_info = f"""
834
+ <div style="border-left: 4px solid #3b82f6; background-color: #f0f9ff; padding: 12px; border-radius: 6px; margin-bottom: 16px;">
835
+ <div style="font-weight: 600; color: #1e40af;">Стиль: {config['name']}</div>
836
+ <div style="font-size: 14px; color: #374151; margin-top: 4px;">{config['description']}</div>
837
+ </div>
838
+ """
839
+ return gr.update(value=style_info), gr.update(visible=True), gr.update(value=letter_text)
840
+
841
+ generate_cl_btn.click(
842
+ show_cover_letter_with_style,
843
+ inputs=[cover_letter_output, letter_type],
844
+ outputs=[cover_letter_style_info, cover_letter_style_info, cover_letter_output]
845
+ ).then(
846
+ generate_cover_letter,
847
+ inputs=[resume_content, job_description, letter_type],
848
+ outputs=[cover_letter_output]
849
+ )
850
 
851
  # NEW TAB: local transformer Q&A
852
  with gr.Tab("Q&A (Local Transformer)"):