cold-msg 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cold_msg/__init__.py +7 -0
- cold_msg/cli.py +282 -0
- cold_msg/core/__init__.py +8 -0
- cold_msg/core/config.py +215 -0
- cold_msg/core/generator.py +129 -0
- cold_msg/core/llm.py +78 -0
- cold_msg/core/prompt.py +128 -0
- cold_msg/web/__init__.py +1 -0
- cold_msg/web/app.py +279 -0
- cold_msg/web/templates/config.html +357 -0
- cold_msg/web/templates/index.html +276 -0
- cold_msg/web/templates/jobs.html +518 -0
- cold_msg-0.1.0.dist-info/METADATA +244 -0
- cold_msg-0.1.0.dist-info/RECORD +17 -0
- cold_msg-0.1.0.dist-info/WHEEL +5 -0
- cold_msg-0.1.0.dist-info/entry_points.txt +2 -0
- cold_msg-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>ColdMsg - 邮件模板配置</title>
|
|
7
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
8
|
+
<style>
|
|
9
|
+
.template-field { transition: border-color 0.2s; }
|
|
10
|
+
.template-field:focus { border-color: #3b82f6; box-shadow: 0 0 0 2px rgba(59,130,246,0.15); }
|
|
11
|
+
</style>
|
|
12
|
+
</head>
|
|
13
|
+
<body class="bg-gray-50 min-h-screen">
|
|
14
|
+
<!-- 导航栏 -->
|
|
15
|
+
<nav class="bg-white border-b border-gray-200 shadow-sm">
|
|
16
|
+
<div class="max-w-6xl mx-auto px-4 py-3 flex items-center justify-between">
|
|
17
|
+
<div class="flex items-center space-x-3">
|
|
18
|
+
<div class="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center">
|
|
19
|
+
<span class="text-white font-bold text-sm">CM</span>
|
|
20
|
+
</div>
|
|
21
|
+
<span class="text-xl font-semibold text-gray-800">ColdMsg</span>
|
|
22
|
+
<span class="text-xs text-gray-400 ml-1">招聘冷邮件生成</span>
|
|
23
|
+
</div>
|
|
24
|
+
<div class="flex space-x-4">
|
|
25
|
+
<a href="/" class="text-gray-500 hover:text-gray-700 text-sm">生成邮件</a>
|
|
26
|
+
<a href="/jobs" class="text-gray-500 hover:text-gray-700 text-sm">职位管理</a>
|
|
27
|
+
<a href="/config" class="text-blue-600 font-medium text-sm">邮件模板配置</a>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</nav>
|
|
31
|
+
|
|
32
|
+
<div class="max-w-5xl mx-auto px-4 py-6 space-y-6">
|
|
33
|
+
<!-- 邮件模板配置 -->
|
|
34
|
+
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-5">
|
|
35
|
+
<div class="flex items-center justify-between mb-4">
|
|
36
|
+
<h2 class="text-base font-semibold text-gray-700">邮件模板配置</h2>
|
|
37
|
+
<button id="resetTemplateBtn"
|
|
38
|
+
class="text-xs text-gray-400 hover:text-red-500 transition-colors">
|
|
39
|
+
恢复默认
|
|
40
|
+
</button>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<!-- 发件人信息 -->
|
|
44
|
+
<div class="mb-5">
|
|
45
|
+
<h3 class="text-sm font-medium text-gray-600 mb-3">发件人信息</h3>
|
|
46
|
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
|
|
47
|
+
<div>
|
|
48
|
+
<label class="block text-xs text-gray-500 mb-1">发件人姓名</label>
|
|
49
|
+
<input id="tpl_sender_name" type="text" value="{{ cfg.email_template.sender_name }}"
|
|
50
|
+
class="template-field w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
51
|
+
data-field="sender_name">
|
|
52
|
+
</div>
|
|
53
|
+
<div>
|
|
54
|
+
<label class="block text-xs text-gray-500 mb-1">招聘团队名称</label>
|
|
55
|
+
<input id="tpl_team_name" type="text" value="{{ cfg.email_template.team_name }}"
|
|
56
|
+
class="template-field w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
57
|
+
data-field="team_name">
|
|
58
|
+
</div>
|
|
59
|
+
<div>
|
|
60
|
+
<label class="block text-xs text-gray-500 mb-1">公司描述</label>
|
|
61
|
+
<input id="tpl_company_desc" type="text" value="{{ cfg.email_template.company_desc }}"
|
|
62
|
+
class="template-field w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
63
|
+
data-field="company_desc">
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<!-- 邮件正文模板 -->
|
|
69
|
+
<div>
|
|
70
|
+
<h3 class="text-sm font-medium text-gray-600 mb-2">邮件正文模板</h3>
|
|
71
|
+
|
|
72
|
+
<div class="space-y-3">
|
|
73
|
+
<div>
|
|
74
|
+
<label class="block text-xs text-gray-500 mb-1">称呼 / 开头</label>
|
|
75
|
+
<input id="tpl_greeting" type="text" value="{{ cfg.email_template.greeting }}"
|
|
76
|
+
class="template-field w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
77
|
+
data-field="greeting">
|
|
78
|
+
<p class="text-xs text-gray-400 mt-1">邮件开头称呼,通常使用 <code class="bg-gray-100 px-1 rounded">{FullName}</code> 插入候选人姓名</p>
|
|
79
|
+
</div>
|
|
80
|
+
<div>
|
|
81
|
+
<label class="block text-xs text-gray-500 mb-1">自我介绍</label>
|
|
82
|
+
<textarea id="tpl_intro" rows="2"
|
|
83
|
+
class="template-field w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-y"
|
|
84
|
+
data-field="intro">{{ cfg.email_template.intro }}</textarea>
|
|
85
|
+
<p class="text-xs text-gray-400 mt-1">介绍来意,通常使用 <code class="bg-gray-100 px-1 rounded">{TeamName}</code> 插入团队名称</p>
|
|
86
|
+
</div>
|
|
87
|
+
<div>
|
|
88
|
+
<label class="block text-xs text-gray-500 mb-1">职位推荐正文</label>
|
|
89
|
+
<textarea id="tpl_body" rows="3"
|
|
90
|
+
class="template-field w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-y"
|
|
91
|
+
data-field="body">{{ cfg.email_template.body }}</textarea>
|
|
92
|
+
<p class="text-xs text-gray-400 mt-1">推荐职位的核心内容,通常使用 <code class="bg-gray-100 px-1 rounded">{CompanyDesc}</code> 和 <code class="bg-gray-100 px-1 rounded">{JobTitle}</code></p>
|
|
93
|
+
</div>
|
|
94
|
+
<div>
|
|
95
|
+
<label class="block text-xs text-gray-500 mb-1">职位地点行</label>
|
|
96
|
+
<input id="tpl_location_line" type="text" value="{{ cfg.email_template.location_line }}"
|
|
97
|
+
class="template-field w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
98
|
+
data-field="location_line">
|
|
99
|
+
<p class="text-xs text-gray-400 mt-1">展示职位地点,通常使用 <code class="bg-gray-100 px-1 rounded">{Location}</code> 插入工作城市</p>
|
|
100
|
+
</div>
|
|
101
|
+
<div>
|
|
102
|
+
<label class="block text-xs text-gray-500 mb-1">结尾联系语</label>
|
|
103
|
+
<textarea id="tpl_closing" rows="2"
|
|
104
|
+
class="template-field w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-y"
|
|
105
|
+
data-field="closing">{{ cfg.email_template.closing }}</textarea>
|
|
106
|
+
<p class="text-xs text-gray-400 mt-1">引导候选人留下联系方式,无需占位符</p>
|
|
107
|
+
</div>
|
|
108
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
109
|
+
<div>
|
|
110
|
+
<label class="block text-xs text-gray-500 mb-1">祝颂语</label>
|
|
111
|
+
<input id="tpl_sign_off" type="text" value="{{ cfg.email_template.sign_off }}"
|
|
112
|
+
class="template-field w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
113
|
+
data-field="sign_off">
|
|
114
|
+
</div>
|
|
115
|
+
<div>
|
|
116
|
+
<label class="block text-xs text-gray-500 mb-1">签名</label>
|
|
117
|
+
<input id="tpl_signature" type="text" value="{{ cfg.email_template.signature }}"
|
|
118
|
+
class="template-field w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
119
|
+
data-field="signature">
|
|
120
|
+
<p class="text-xs text-gray-400 mt-1">默认使用上方「发件人姓名」的值,也可输入自定义签名</p>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<!-- 模板预览 -->
|
|
127
|
+
<div class="mt-5 pt-4 border-t border-gray-100">
|
|
128
|
+
<div class="flex items-center justify-between mb-2">
|
|
129
|
+
<h3 class="text-sm font-medium text-gray-600">模板预览</h3>
|
|
130
|
+
<span class="text-xs text-gray-400">黄色高亮为待替换的占位符</span>
|
|
131
|
+
</div>
|
|
132
|
+
<div id="templatePreview" class="bg-gray-50 rounded-lg p-4 text-sm text-gray-600 leading-relaxed border border-gray-100 whitespace-pre-wrap"></div>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<!-- 职位列表(已迁移到独立页面) -->
|
|
137
|
+
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-5">
|
|
138
|
+
<div class="flex items-center justify-between">
|
|
139
|
+
<div>
|
|
140
|
+
<h2 class="text-base font-semibold text-gray-700">职位列表</h2>
|
|
141
|
+
<p class="text-xs text-gray-400 mt-1">当前 {{ cfg.jobs|length }} 个职位</p>
|
|
142
|
+
</div>
|
|
143
|
+
<a href="/jobs"
|
|
144
|
+
class="bg-blue-600 hover:bg-blue-700 text-white text-xs font-medium py-2 px-4 rounded-lg transition-colors">
|
|
145
|
+
管理职位
|
|
146
|
+
</a>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<!-- 保存按钮 -->
|
|
151
|
+
<div class="flex justify-end space-x-3">
|
|
152
|
+
<button id="saveBtn"
|
|
153
|
+
class="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2.5 px-6 rounded-lg transition-colors text-sm">
|
|
154
|
+
保存配置
|
|
155
|
+
</button>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<!-- 配置说明 -->
|
|
159
|
+
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-5">
|
|
160
|
+
<h2 class="text-sm font-semibold text-gray-600 mb-3">配置说明</h2>
|
|
161
|
+
|
|
162
|
+
<!-- 占位符说明 -->
|
|
163
|
+
<div class="mb-4">
|
|
164
|
+
<h3 class="text-xs font-medium text-gray-500 mb-2">模板占位符</h3>
|
|
165
|
+
<p class="text-xs text-gray-400 mb-2">在模板字段中使用 <code class="bg-gray-100 px-1 rounded">{占位符}</code> 表示需要自动替换的内容,系统生成邮件时会自动替换为实际信息。</p>
|
|
166
|
+
<table class="w-full text-xs border-collapse">
|
|
167
|
+
<thead>
|
|
168
|
+
<tr class="border-b border-gray-200">
|
|
169
|
+
<th class="text-gray-500 text-left py-1.5 pr-3 font-medium w-24">占位符</th>
|
|
170
|
+
<th class="text-gray-500 text-left py-1.5 pr-3 font-medium">含义</th>
|
|
171
|
+
<th class="text-gray-500 text-left py-1.5 font-medium w-28">替换来源</th>
|
|
172
|
+
</tr>
|
|
173
|
+
</thead>
|
|
174
|
+
<tbody class="text-gray-400">
|
|
175
|
+
<tr class="border-b border-gray-100">
|
|
176
|
+
<td><code class="bg-gray-100 px-1 rounded font-mono">{FullName}</code></td>
|
|
177
|
+
<td>候选人全名</td>
|
|
178
|
+
<td>AI 从简历自动识别</td>
|
|
179
|
+
</tr>
|
|
180
|
+
<tr class="border-b border-gray-100">
|
|
181
|
+
<td><code class="bg-gray-100 px-1 rounded font-mono">{TeamName}</code></td>
|
|
182
|
+
<td>招聘团队名称</td>
|
|
183
|
+
<td>上方「招聘团队名称」</td>
|
|
184
|
+
</tr>
|
|
185
|
+
<tr class="border-b border-gray-100">
|
|
186
|
+
<td><code class="bg-gray-100 px-1 rounded font-mono">{CompanyDesc}</code></td>
|
|
187
|
+
<td>公司描述</td>
|
|
188
|
+
<td>上方「公司描述」</td>
|
|
189
|
+
</tr>
|
|
190
|
+
<tr class="border-b border-gray-100">
|
|
191
|
+
<td><code class="bg-gray-100 px-1 rounded font-mono">{JobTitle}</code></td>
|
|
192
|
+
<td>推荐职位名称</td>
|
|
193
|
+
<td>AI 根据匹配结果填入</td>
|
|
194
|
+
</tr>
|
|
195
|
+
<tr>
|
|
196
|
+
<td><code class="bg-gray-100 px-1 rounded font-mono">{Location}</code></td>
|
|
197
|
+
<td>职位工作地点</td>
|
|
198
|
+
<td>AI 根据匹配职位填入</td>
|
|
199
|
+
</tr>
|
|
200
|
+
</tbody>
|
|
201
|
+
</table>
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
<!-- 字段说明 -->
|
|
205
|
+
<div class="mb-4">
|
|
206
|
+
<h3 class="text-xs font-medium text-gray-500 mb-2">字段说明</h3>
|
|
207
|
+
<ul class="text-xs text-gray-400 space-y-1 list-disc list-inside">
|
|
208
|
+
<li><strong class="text-gray-500">发件人姓名</strong> — 邮件签名中显示的姓名,默认填入「签名」字段</li>
|
|
209
|
+
<li><strong class="text-gray-500">招聘团队名称</strong> — 自我介绍中提到的团队名,通过 <code class="bg-gray-100 px-1 rounded">{TeamName}</code> 引用</li>
|
|
210
|
+
<li><strong class="text-gray-500">公司描述</strong> — 推荐正文中提到的公司信息,通过 <code class="bg-gray-100 px-1 rounded">{CompanyDesc}</code> 引用</li>
|
|
211
|
+
<li><strong class="text-gray-500">称呼 / 开头</strong> — 邮件第一行,通常为"您好{FullName},"</li>
|
|
212
|
+
<li><strong class="text-gray-500">自我介绍</strong> — 说明来意和了解候选人的途径</li>
|
|
213
|
+
<li><strong class="text-gray-500">职位推荐正文</strong> — 推荐具体职位的核心段落</li>
|
|
214
|
+
<li><strong class="text-gray-500">职位地点行</strong> — 展示工作地点,通常为"职位地点:{Location}"</li>
|
|
215
|
+
<li><strong class="text-gray-500">结尾联系语</strong> — 引导候选人留下联系方式的句子</li>
|
|
216
|
+
<li><strong class="text-gray-500">祝颂语</strong> — 邮件结尾的客套语,如"祝颂商祺"</li>
|
|
217
|
+
<li><strong class="text-gray-500">签名</strong> — 邮件末尾署名,默认使用发件人姓名</li>
|
|
218
|
+
</ul>
|
|
219
|
+
</div>
|
|
220
|
+
|
|
221
|
+
<!-- 使用流程 -->
|
|
222
|
+
<div>
|
|
223
|
+
<h3 class="text-xs font-medium text-gray-500 mb-2">使用流程</h3>
|
|
224
|
+
<ol class="text-xs text-gray-400 space-y-1 list-decimal list-inside">
|
|
225
|
+
<li>在「职位管理」页面添加或导入职位列表</li>
|
|
226
|
+
<li>在本页面配置邮件模板(发件人信息 + 正文模板)</li>
|
|
227
|
+
<li>在「生成邮件」页面粘贴候选人简历文本,点击生成</li>
|
|
228
|
+
<li>AI 自动匹配职位并生成完整邮件,复制即可使用</li>
|
|
229
|
+
</ol>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
<!-- 提示消息 -->
|
|
234
|
+
<div id="toast" class="fixed top-4 right-4 bg-green-600 text-white px-4 py-2 rounded-lg shadow-lg text-sm hidden transition-all">
|
|
235
|
+
配置已保存
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
<script>
|
|
240
|
+
const saveBtn = document.getElementById('saveBtn');
|
|
241
|
+
const toast = document.getElementById('toast');
|
|
242
|
+
const resetTemplateBtn = document.getElementById('resetTemplateBtn');
|
|
243
|
+
const templatePreview = document.getElementById('templatePreview');
|
|
244
|
+
|
|
245
|
+
// 默认模板值
|
|
246
|
+
const DEFAULT_TEMPLATE = {
|
|
247
|
+
sender_name: 'Michael',
|
|
248
|
+
team_name: '高端招聘团队',
|
|
249
|
+
company_desc: '国际知名大型科技公司',
|
|
250
|
+
greeting: '您好{FullName},',
|
|
251
|
+
intro: '我来自{TeamName},很冒昧的给您发邮件。通过Linkedin了解到您,了解到您在XXX, XXX等方向有丰富经验。',
|
|
252
|
+
body: '我负责{CompanyDesc}招聘,非常希望邀请到您这样的人才在此向您推荐"{JobTitle}"的职位。负责带领技术团队攻克更多XXX,XXX技术难题,进一步提升您在行业内的影响力。',
|
|
253
|
+
location_line: '职位地点: {Location}',
|
|
254
|
+
closing: '如果您有兴趣具体了解的话,可以留个电话或者微信,给您详细介绍一下,期待与您的沟通~',
|
|
255
|
+
sign_off: '祝颂商祺',
|
|
256
|
+
signature: 'Michael',
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
// 模板字段列表
|
|
260
|
+
const TEMPLATE_FIELDS = [
|
|
261
|
+
'sender_name', 'team_name', 'company_desc',
|
|
262
|
+
'greeting', 'intro', 'body', 'location_line',
|
|
263
|
+
'closing', 'sign_off', 'signature',
|
|
264
|
+
];
|
|
265
|
+
|
|
266
|
+
// 获取当前模板数据
|
|
267
|
+
function getTemplateData() {
|
|
268
|
+
const data = {};
|
|
269
|
+
TEMPLATE_FIELDS.forEach(field => {
|
|
270
|
+
const el = document.getElementById('tpl_' + field);
|
|
271
|
+
if (el) data[field] = el.value;
|
|
272
|
+
});
|
|
273
|
+
return data;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// 渲染模板预览
|
|
277
|
+
function renderPreview() {
|
|
278
|
+
const tpl = getTemplateData();
|
|
279
|
+
const parts = [
|
|
280
|
+
tpl.greeting,
|
|
281
|
+
tpl.intro,
|
|
282
|
+
'',
|
|
283
|
+
tpl.body,
|
|
284
|
+
'',
|
|
285
|
+
tpl.location_line,
|
|
286
|
+
'',
|
|
287
|
+
tpl.closing,
|
|
288
|
+
'',
|
|
289
|
+
tpl.sign_off,
|
|
290
|
+
tpl.signature,
|
|
291
|
+
];
|
|
292
|
+
let text = parts.join('\n');
|
|
293
|
+
|
|
294
|
+
// 替换配置提供的占位符
|
|
295
|
+
text = text.replace(/\{TeamName\}/g, tpl.team_name);
|
|
296
|
+
text = text.replace(/\{CompanyDesc\}/g, tpl.company_desc);
|
|
297
|
+
text = text.replace(/\{SenderName\}/g, tpl.sender_name);
|
|
298
|
+
|
|
299
|
+
// 高亮剩余占位符
|
|
300
|
+
text = text.replace(/\{(\w+)\}/g, '<span class="bg-yellow-100 text-yellow-700 px-1 rounded font-medium">{$1}</span>');
|
|
301
|
+
|
|
302
|
+
templatePreview.innerHTML = text;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// 监听模板字段变化,实时更新预览
|
|
306
|
+
TEMPLATE_FIELDS.forEach(field => {
|
|
307
|
+
const el = document.getElementById('tpl_' + field);
|
|
308
|
+
if (el) {
|
|
309
|
+
el.addEventListener('input', renderPreview);
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// 恢复默认模板
|
|
314
|
+
resetTemplateBtn.addEventListener('click', () => {
|
|
315
|
+
if (!confirm('确定恢复邮件模板为默认值?')) return;
|
|
316
|
+
TEMPLATE_FIELDS.forEach(field => {
|
|
317
|
+
const el = document.getElementById('tpl_' + field);
|
|
318
|
+
if (el && DEFAULT_TEMPLATE[field] !== undefined) {
|
|
319
|
+
el.value = DEFAULT_TEMPLATE[field];
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
renderPreview();
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// 初始渲染预览
|
|
326
|
+
renderPreview();
|
|
327
|
+
|
|
328
|
+
function showToast(msg, isError = false) {
|
|
329
|
+
toast.textContent = msg;
|
|
330
|
+
toast.className = `fixed top-4 right-4 ${isError ? 'bg-red-600' : 'bg-green-600'} text-white px-4 py-2 rounded-lg shadow-lg text-sm transition-all`;
|
|
331
|
+
setTimeout(() => { toast.classList.add('hidden'); }, 2000);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
saveBtn.addEventListener('click', async () => {
|
|
335
|
+
const data = {
|
|
336
|
+
email_template: getTemplateData(),
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
try {
|
|
340
|
+
const resp = await fetch('/api/config', {
|
|
341
|
+
method: 'POST',
|
|
342
|
+
headers: { 'Content-Type': 'application/json' },
|
|
343
|
+
body: JSON.stringify(data),
|
|
344
|
+
});
|
|
345
|
+
const result = await resp.json();
|
|
346
|
+
if (resp.ok) {
|
|
347
|
+
showToast(result.message || '配置已保存');
|
|
348
|
+
} else {
|
|
349
|
+
showToast(result.error || '保存失败', true);
|
|
350
|
+
}
|
|
351
|
+
} catch (err) {
|
|
352
|
+
showToast('网络错误: ' + err.message, true);
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
</script>
|
|
356
|
+
</body>
|
|
357
|
+
</html>
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>ColdMsg - 招聘冷邮件生成</title>
|
|
7
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
8
|
+
<style>
|
|
9
|
+
.loading-spinner {
|
|
10
|
+
border: 3px solid #e5e7eb;
|
|
11
|
+
border-top: 3px solid #3b82f6;
|
|
12
|
+
border-radius: 50%;
|
|
13
|
+
width: 24px;
|
|
14
|
+
height: 24px;
|
|
15
|
+
animation: spin 0.8s linear infinite;
|
|
16
|
+
}
|
|
17
|
+
@keyframes spin {
|
|
18
|
+
0% { transform: rotate(0deg); }
|
|
19
|
+
100% { transform: rotate(360deg); }
|
|
20
|
+
}
|
|
21
|
+
.fade-in {
|
|
22
|
+
animation: fadeIn 0.3s ease-in;
|
|
23
|
+
}
|
|
24
|
+
@keyframes fadeIn {
|
|
25
|
+
from { opacity: 0; transform: translateY(10px); }
|
|
26
|
+
to { opacity: 1; transform: translateY(0); }
|
|
27
|
+
}
|
|
28
|
+
</style>
|
|
29
|
+
</head>
|
|
30
|
+
<body class="bg-gray-50 min-h-screen flex flex-col">
|
|
31
|
+
<!-- 导航栏 -->
|
|
32
|
+
<nav class="bg-white border-b border-gray-200 shadow-sm flex-shrink-0">
|
|
33
|
+
<div class="max-w-7xl mx-auto px-4 py-3 flex items-center justify-between">
|
|
34
|
+
<div class="flex items-center space-x-3">
|
|
35
|
+
<div class="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center">
|
|
36
|
+
<span class="text-white font-bold text-sm">CM</span>
|
|
37
|
+
</div>
|
|
38
|
+
<span class="text-xl font-semibold text-gray-800">ColdMsg</span>
|
|
39
|
+
<span class="text-xs text-gray-400 ml-1">招聘冷邮件生成</span>
|
|
40
|
+
</div>
|
|
41
|
+
<div class="flex space-x-4">
|
|
42
|
+
<a href="/" class="text-blue-600 font-medium text-sm">生成邮件</a>
|
|
43
|
+
<a href="/jobs" class="text-gray-500 hover:text-gray-700 text-sm">职位管理</a>
|
|
44
|
+
<a href="/config" class="text-gray-500 hover:text-gray-700 text-sm">邮件模板配置</a>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</nav>
|
|
48
|
+
|
|
49
|
+
<!-- 顶部操作栏 -->
|
|
50
|
+
<div class="bg-white border-b border-gray-100 flex-shrink-0">
|
|
51
|
+
<div class="max-w-7xl mx-auto px-4 py-3 flex items-center justify-between">
|
|
52
|
+
<div class="flex items-center space-x-4">
|
|
53
|
+
<span class="text-sm text-gray-500">当前 <strong class="text-gray-700">{{ cfg.jobs|length }}</strong> 个职位</span>
|
|
54
|
+
<a href="/jobs" class="text-xs text-blue-500 hover:text-blue-700">管理职位</a>
|
|
55
|
+
<span class="text-gray-200">|</span>
|
|
56
|
+
<a href="/config" class="text-xs text-blue-500 hover:text-blue-700">修改模板</a>
|
|
57
|
+
</div>
|
|
58
|
+
<button id="generateBtn"
|
|
59
|
+
class="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-6 rounded-lg transition-colors text-sm flex items-center space-x-2">
|
|
60
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
61
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
|
|
62
|
+
</svg>
|
|
63
|
+
<span>生成冷邮件</span>
|
|
64
|
+
</button>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<!-- 主内容区 -->
|
|
69
|
+
<div class="flex-1 max-w-7xl w-full mx-auto px-4 py-4">
|
|
70
|
+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 h-full" style="min-height: calc(100vh - 140px);">
|
|
71
|
+
<!-- 左侧:候选人简历文本 -->
|
|
72
|
+
<div class="flex flex-col">
|
|
73
|
+
<div class="bg-white rounded-lg shadow-sm border border-gray-200 flex flex-col flex-1">
|
|
74
|
+
<div class="flex items-center justify-between px-5 py-3 border-b border-gray-100">
|
|
75
|
+
<h2 class="text-sm font-semibold text-gray-700">候选人简历文本</h2>
|
|
76
|
+
<div class="flex items-center space-x-3">
|
|
77
|
+
<span id="charCount" class="text-xs text-gray-400">0 字符</span>
|
|
78
|
+
<button id="clearBtn"
|
|
79
|
+
class="text-xs text-gray-400 hover:text-red-500 transition-colors">清空</button>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
<textarea id="resumeInput"
|
|
83
|
+
class="flex-1 w-full p-4 text-sm text-gray-700 focus:outline-none resize-none placeholder-gray-400"
|
|
84
|
+
style="min-height: 400px;"
|
|
85
|
+
placeholder="请粘贴候选人的简历文本信息... 例如: 张三,10年AI研发经验,精通大模型训练、推理优化,曾在某大厂担任技术总监..."></textarea>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<!-- 右侧:生成结果 -->
|
|
90
|
+
<div class="flex flex-col">
|
|
91
|
+
<!-- 生成结果 -->
|
|
92
|
+
<div id="resultPanel" class="bg-white rounded-lg shadow-sm border border-gray-200 flex flex-col flex-1 hidden">
|
|
93
|
+
<div class="flex items-center justify-between px-5 py-3 border-b border-gray-100">
|
|
94
|
+
<div class="flex items-center space-x-3">
|
|
95
|
+
<h2 class="text-sm font-semibold text-gray-700">生成结果</h2>
|
|
96
|
+
<div id="matchBadge">
|
|
97
|
+
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
|
98
|
+
已匹配
|
|
99
|
+
</span>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
<div class="flex space-x-2">
|
|
103
|
+
<button id="copyBtn"
|
|
104
|
+
class="text-xs text-gray-500 hover:text-blue-600 transition-colors px-2 py-1 rounded hover:bg-gray-50">
|
|
105
|
+
复制邮件
|
|
106
|
+
</button>
|
|
107
|
+
<button id="copyJsonBtn"
|
|
108
|
+
class="text-xs text-gray-500 hover:text-blue-600 transition-colors px-2 py-1 rounded hover:bg-gray-50">
|
|
109
|
+
复制JSON
|
|
110
|
+
</button>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
<div id="recommendTitle" class="px-5 pt-3 text-sm text-blue-600 font-medium"></div>
|
|
114
|
+
<div id="emailContent" class="flex-1 px-5 py-3 text-sm text-gray-700 leading-relaxed whitespace-pre-wrap overflow-y-auto"></div>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<!-- 无匹配结果 -->
|
|
118
|
+
<div id="noMatchPanel" class="bg-white rounded-lg shadow-sm border border-gray-200 flex-1 hidden">
|
|
119
|
+
<div class="flex flex-col items-center justify-center h-full py-16">
|
|
120
|
+
<div class="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mb-4">
|
|
121
|
+
<svg class="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
122
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
123
|
+
</svg>
|
|
124
|
+
</div>
|
|
125
|
+
<p class="text-gray-500 text-sm">候选人未匹配到合适职位</p>
|
|
126
|
+
<p class="text-gray-400 text-xs mt-1">请检查职位列表是否已添加相关职位</p>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
<!-- 加载中 -->
|
|
131
|
+
<div id="loadingPanel" class="bg-white rounded-lg shadow-sm border border-gray-200 flex-1 hidden">
|
|
132
|
+
<div class="flex flex-col items-center justify-center h-full py-16">
|
|
133
|
+
<div class="loading-spinner mb-4"></div>
|
|
134
|
+
<span class="text-gray-500 text-sm">正在生成邮件,请稍候...</span>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<!-- 错误提示 -->
|
|
139
|
+
<div id="errorPanel" class="bg-white rounded-lg shadow-sm border border-red-200 flex-1 hidden">
|
|
140
|
+
<div class="flex flex-col items-center justify-center h-full py-16">
|
|
141
|
+
<div class="w-16 h-16 bg-red-50 rounded-full flex items-center justify-center mb-4">
|
|
142
|
+
<svg class="w-8 h-8 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
143
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 9v2m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
144
|
+
</svg>
|
|
145
|
+
</div>
|
|
146
|
+
<p class="text-red-700 text-sm font-medium">生成失败</p>
|
|
147
|
+
<p id="errorMessage" class="text-red-500 text-xs mt-1"></p>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<!-- 空状态 -->
|
|
152
|
+
<div id="emptyPanel" class="bg-white rounded-lg shadow-sm border border-gray-200 flex-1">
|
|
153
|
+
<div class="flex flex-col items-center justify-center h-full py-16">
|
|
154
|
+
<div class="w-20 h-20 bg-gray-50 rounded-full flex items-center justify-center mb-5">
|
|
155
|
+
<svg class="w-10 h-10 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
156
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
|
157
|
+
</svg>
|
|
158
|
+
</div>
|
|
159
|
+
<p class="text-gray-400 text-sm">在左侧输入候选人简历文本</p>
|
|
160
|
+
<p class="text-gray-400 text-sm">点击「生成冷邮件」开始</p>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
<script>
|
|
168
|
+
const resumeInput = document.getElementById('resumeInput');
|
|
169
|
+
const charCount = document.getElementById('charCount');
|
|
170
|
+
const clearBtn = document.getElementById('clearBtn');
|
|
171
|
+
const generateBtn = document.getElementById('generateBtn');
|
|
172
|
+
|
|
173
|
+
const resultPanel = document.getElementById('resultPanel');
|
|
174
|
+
const noMatchPanel = document.getElementById('noMatchPanel');
|
|
175
|
+
const loadingPanel = document.getElementById('loadingPanel');
|
|
176
|
+
const errorPanel = document.getElementById('errorPanel');
|
|
177
|
+
const emptyPanel = document.getElementById('emptyPanel');
|
|
178
|
+
const matchBadge = document.getElementById('matchBadge');
|
|
179
|
+
const recommendTitle = document.getElementById('recommendTitle');
|
|
180
|
+
const emailContent = document.getElementById('emailContent');
|
|
181
|
+
const copyBtn = document.getElementById('copyBtn');
|
|
182
|
+
const copyJsonBtn = document.getElementById('copyJsonBtn');
|
|
183
|
+
const errorMessage = document.getElementById('errorMessage');
|
|
184
|
+
|
|
185
|
+
let lastResult = null;
|
|
186
|
+
|
|
187
|
+
function hideAllPanels() {
|
|
188
|
+
[resultPanel, noMatchPanel, loadingPanel, errorPanel, emptyPanel].forEach(p => p.classList.add('hidden'));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function showPanel(panel) {
|
|
192
|
+
hideAllPanels();
|
|
193
|
+
panel.classList.remove('hidden');
|
|
194
|
+
panel.classList.add('fade-in');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 字符计数
|
|
198
|
+
resumeInput.addEventListener('input', () => {
|
|
199
|
+
charCount.textContent = `${resumeInput.value.length} 字符`;
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// 清空
|
|
203
|
+
clearBtn.addEventListener('click', () => {
|
|
204
|
+
resumeInput.value = '';
|
|
205
|
+
charCount.textContent = '0 字符';
|
|
206
|
+
showPanel(emptyPanel);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// 生成
|
|
210
|
+
generateBtn.addEventListener('click', async () => {
|
|
211
|
+
const resumeText = resumeInput.value.trim();
|
|
212
|
+
if (!resumeText) {
|
|
213
|
+
alert('请输入候选人简历文本');
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
showPanel(loadingPanel);
|
|
218
|
+
generateBtn.disabled = true;
|
|
219
|
+
generateBtn.classList.add('opacity-50');
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
const body = { resume_text: resumeText };
|
|
223
|
+
|
|
224
|
+
const resp = await fetch('/api/generate', {
|
|
225
|
+
method: 'POST',
|
|
226
|
+
headers: { 'Content-Type': 'application/json' },
|
|
227
|
+
body: JSON.stringify(body),
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const data = await resp.json();
|
|
231
|
+
|
|
232
|
+
if (!resp.ok) {
|
|
233
|
+
throw new Error(data.error || '生成失败');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
lastResult = data;
|
|
237
|
+
|
|
238
|
+
if (data.match_job_title) {
|
|
239
|
+
recommendTitle.textContent = `推荐职位: ${data.recommend_title}`;
|
|
240
|
+
emailContent.textContent = data.email_content;
|
|
241
|
+
matchBadge.classList.remove('hidden');
|
|
242
|
+
showPanel(resultPanel);
|
|
243
|
+
} else {
|
|
244
|
+
showPanel(noMatchPanel);
|
|
245
|
+
}
|
|
246
|
+
} catch (err) {
|
|
247
|
+
errorMessage.textContent = err.message;
|
|
248
|
+
showPanel(errorPanel);
|
|
249
|
+
} finally {
|
|
250
|
+
generateBtn.disabled = false;
|
|
251
|
+
generateBtn.classList.remove('opacity-50');
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// 复制邮件内容
|
|
256
|
+
copyBtn.addEventListener('click', () => {
|
|
257
|
+
if (lastResult && lastResult.email_content) {
|
|
258
|
+
navigator.clipboard.writeText(lastResult.email_content).then(() => {
|
|
259
|
+
copyBtn.textContent = '已复制!';
|
|
260
|
+
setTimeout(() => { copyBtn.textContent = '复制邮件'; }, 1500);
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// 复制 JSON
|
|
266
|
+
copyJsonBtn.addEventListener('click', () => {
|
|
267
|
+
if (lastResult) {
|
|
268
|
+
navigator.clipboard.writeText(JSON.stringify(lastResult, null, 2)).then(() => {
|
|
269
|
+
copyJsonBtn.textContent = '已复制!';
|
|
270
|
+
setTimeout(() => { copyJsonBtn.textContent = '复制JSON'; }, 1500);
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
</script>
|
|
275
|
+
</body>
|
|
276
|
+
</html>
|