${isUser?'你':'AI'}${ragTag}
${m.content.replace(/
`;
}).join('');
el.scrollTop = el.scrollHeight;
}
function buildCurrentProfContext() {
if (!document.getElementById('chat-include-context')?.checked) return '';
const p = readerState.filtered[readerState.index];
if (!p) return '';
const ctx = {
name: p.name, school: p.school, department: p.department,
primary_direction: p.primary_direction, research_areas: p.research_areas,
reason: p.reason_to_apply || p.reason_to_note,
notable_recent_paper: p.notable_recent_paper,
current_projects: p.current_projects,
junior_pi: p.junior_pi, pi_start_year: p.pi_start_year,
summer_availability_score: summerAvailability(p),
overall_priority_score: overallScore(p),
website: p.website, email: p.email,
fit_score: p.fit_score, priority: p.priority,
christina_overlap: p.christina_overlap,
cold_email_hooks: p.cold_email_hooks,
key_papers: p.key_papers_chronological
};
return '\n\n---\n你当前在精读的教授档案:\n```json\n' + JSON.stringify(ctx, null, 2) + '\n```\n';
}
function buildRAGContext(query) {
if (!document.getElementById('chat-use-rag')?.checked) return { text:'', count:0 };
const k = parseInt(document.getElementById('chat-rag-k')?.value || '15', 10);
const hits = ragSearch(query, k);
if (!hits.length) return { text:'', count:0 };
const text = '\n\n---\n🔎 本地 RAG 检索 — 你的 tracker 内最相关的 ' + hits.length + ' 条(按相关度排序):\n\n' +
hits.map((d, i) => {
let summary;
if (d.kind === 'prof') {
const p = d.ref;
summary = `【教授】${p.name||''} · ${p.school||''} · ${p.department||''} · ${p.primary_direction||''}
研究:${(p.research_areas||[]).join(', ')}
状态:${p.recruiting_status||''} · junior=${p.junior_pi||false} · 暑期=${summerAvailability(p)}/10 · 综合=${overallScore(p)}
关键:${(p.reason_to_apply || p.reason_to_note || '').substring(0,300)}
论文:${(p.notable_recent_paper||'').substring(0,200)}
邮箱:${p.email||''}`;
} else if (d.kind === 'paper') {
const r = d.ref;
summary = `【论文】${r.title} (${r.venue} ${r.year} ${r.country||''})
作者:${(r.authors||[]).slice(0,4).join(', ')}
机构:${r.institution||''} · 方向:${r.direction||''}
摘要:${r.summary||''}
价值:${r.why_important||''}
链接:${r.link||''}`;
} else if (d.kind === 'industry') {
const r = d.ref;
summary = `【工业发布】${r.year}-${r.month||''} ${r.org}: ${r.name}
类型:${r.type||''}
说明:${r.summary||''}
链接:${r.link||''}`;
} else if (d.kind === 'venue') {
const v = d.ref;
summary = `【会议】${v.key} (${v.full}) · ${v.area} · ${v.tier} · accept ${v.accept_rate||''} · ddl ${v.deadline||''}
趋势:${v.trend||''}`;
} else if (d.kind === 'comparison') {
const c = d.ref;
summary = `【中外对比】${c.direction}
美国:${c.us_strength||''}
国内:${c.china_strength||''}
差距:${c.gap||''}
代表 US:${(c.examples_us||[]).join(', ')}
代表 CN:${(c.examples_cn||[]).join(', ')}`;
} else {
summary = JSON.stringify(d.ref);
}
return `[${i+1}] ${summary}`;
}).join('\n\n');
return { text, count: hits.length };
}
async function sendChat() {
const input = document.getElementById('chat-input');
const sendBtn = document.getElementById('chat-send');
const userMsg = input.value.trim();
if (!userMsg) return;
const key = getChatKey();
if (!key) return;
const model = document.getElementById('chat-model').value;
const ragCtx = buildRAGContext(userMsg);
const useWebUI = document.getElementById('chat-use-web')?.checked;
const fullUser = userMsg + ragCtx.text + buildCurrentProfContext();
chatHistory.push({ role:'user', content: userMsg, _rag: ragCtx.count, _web: useWebUI });
let status = '⏳ ';
if (useWebUI) status += '联网搜索中';
else if (ragCtx.count) status += `本地检索 ${ragCtx.count} 条,正在思考`;
else status += '思考中';
const placeholder = { role:'assistant', content: status + '…' };
chatHistory.push(placeholder);
renderChat();
input.value = '';
sendBtn.disabled = true;
// Use the RAG-augmented user message for LAST user turn only;
// older turns keep their original short content (saves tokens)
const lastUserIdx = chatHistory.length - 2;
const messages = [
{ role:'system', content: CHAT_SYS_PROMPT + (ragCtx.count ? '\n\n你将收到 RAG 检索结果,请优先基于这些条目回答,引用时用 [编号]。如果检索结果不够,明说"我的库里没有,以下是推测"。' : '') },
...chatHistory.slice(0,-1).map((m, i) => {
if (i === lastUserIdx && m.role === 'user') {
return { role:'user', content: fullUser };
}
return { role: m.role, content: m.content };
})
];
try {
const useWeb = document.getElementById('chat-use-web')?.checked;
const isReasoner = /^o[0-9]/.test(model);
// If web search requested, route to search-preview models (they alone do native web search)
let actualModel = model;
if (useWeb) {
if (model.startsWith('gpt-4o-mini')) actualModel = 'gpt-4o-mini-search-preview';
else if (model.startsWith('gpt-4o') || model.startsWith('gpt-4.1') || isReasoner) actualModel = 'gpt-4o-search-preview';
}
const isSearchModel = actualModel.includes('-search-preview');
const body = { model: actualModel, messages };
if (!isReasoner && !isSearchModel) body.temperature = 0.4;
if (isReasoner) body.max_completion_tokens = 4000;
else body.max_tokens = 2500;
// search-preview models don't accept temperature; they accept web_search_options
if (isSearchModel) {
body.web_search_options = { search_context_size: 'medium' };
}
const resp = await fetch('https://api.openai.com/v1/chat/completions', {
method:'POST',
headers:{'Content-Type':'application/json','Authorization':'Bearer '+key},
body: JSON.stringify(body)
});
if (!resp.ok) {
const err = await resp.text();
placeholder.content = '❌ API 错误 ' + resp.status + ' (model=' + actualModel + '):\n' + err.substring(0, 600);
} else {
const data = await resp.json();
const msg = data.choices?.[0]?.message;
let content = msg?.content || '(空响应)';
// Append citations if present (search-preview returns annotations)
const annots = msg?.annotations || [];
if (annots.length) {
const cites = annots
.filter(a => a.type === 'url_citation' && a.url_citation?.url)
.map((a,i) => `[${i+1}] ${a.url_citation.title || a.url_citation.url}\n ${a.url_citation.url}`);
if (cites.length) content += '\n\n📎 来源:\n' + cites.join('\n');
}
content += '\n\n— 模型: ' + actualModel + (isSearchModel ? ' 🌐' : '');
placeholder.content = content;
}
} catch (e) {
placeholder.content = '❌ 网络错误: ' + e.message;
}
saveChatHist();
renderChat();
sendBtn.disabled = false;
}
// Event listeners
document.getElementById('chat-send')?.addEventListener('click', sendChat);
document.getElementById('chat-input')?.addEventListener('keydown', e => {
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
e.preventDefault();
sendChat();
}
});
document.getElementById('chat-clear-key')?.addEventListener('click', () => {
if (confirm('清除浏览器内保存的 API key?')) {
localStorage.removeItem(CHAT_KEY_STORE);
alert('已清除。下次问问题会重新提示输入。');
}
});
document.getElementById('chat-clear-hist')?.addEventListener('click', () => {
if (confirm('清空对话历史?')) {
chatHistory = [];
saveChatHist();
renderChat();
}
});
document.getElementById('chat-model')?.addEventListener('change', e => {
document.getElementById('chat-model-label').textContent = '· ' + e.target.value;
localStorage.setItem('prof_tracker_chat_model', e.target.value);
});
// Restore last-used model
const savedModel = localStorage.getItem('prof_tracker_chat_model');
if (savedModel) {
const sel = document.getElementById('chat-model');
if (sel && [...sel.options].some(o=>o.value===savedModel)) {
sel.value = savedModel;
document.getElementById('chat-model-label').textContent = '· ' + savedModel;
}
}
renderChat();
// Simple modal for a Chinese prof
function openChinaModal(p) {
const body = document.getElementById('modal-body');
body.innerHTML = `
${p.name || p.id} · ${p.school || ''}
${p.department || ''}
${p.website ? `
${p.website}
` : ''}
${p.email ? `
📧 ${p.email}
` : ''}
${p.research_areas ? `
研究方向
${p.research_areas.map(a=>`${a}`).join('')}
` : ''}
${p.reason_to_note ? `
关注理由
${p.reason_to_note}
` : ''}
${p.notable_recent_paper ? `
近期重要论文
${p.notable_recent_paper}
` : ''}
${p.current_projects ? `
当前项目
${p.current_projects.map(x=>`- ${x}
`).join('')}
` : ''}
${p.notes ? `
备注
${p.notes}
` : ''}
`;
document.getElementById('modal').classList.add('open');
}