hr-eval-api-v2 / static /chat.html
KarenYYH
Add: 打字机效果 - AI 回复逐字显示
51aa8ee
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>HR智能对话助手</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 24px;
text-align: center;
}
.header h1 {
font-size: 28px;
margin-bottom: 8px;
}
.header p {
opacity: 0.9;
font-size: 14px;
}
.main-content {
display: flex;
height: calc(100vh - 200px);
min-height: 500px;
}
.chat-section {
flex: 1;
display: flex;
flex-direction: column;
border-right: 1px solid #e5e7eb;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 20px;
background: #f9fafb;
}
.message {
margin-bottom: 16px;
display: flex;
align-items: flex-start;
gap: 12px;
}
.message.user {
flex-direction: row-reverse;
}
.message-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
color: white;
flex-shrink: 0;
}
.message.user .message-avatar {
background: #667eea;
}
.message.assistant .message-avatar {
background: #10b981;
}
.message-content {
max-width: 70%;
padding: 12px 16px;
border-radius: 12px;
line-height: 1.5;
}
.message.user .message-content {
background: #667eea;
color: white;
border-bottom-right-radius: 4px;
}
.message.assistant .message-content {
background: white;
border: 1px solid #e5e7eb;
border-bottom-left-radius: 4px;
}
.chat-input {
padding: 20px;
background: white;
border-top: 1px solid #e5e7eb;
display: flex;
gap: 12px;
}
.chat-input input {
flex: 1;
padding: 12px 16px;
border: 2px solid #e5e7eb;
border-radius: 8px;
font-size: 14px;
transition: border-color 0.2s;
}
.chat-input input:focus {
outline: none;
border-color: #667eea;
}
.chat-input button {
padding: 12px 24px;
background: #667eea;
color: white;
border: none;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
.chat-input button:hover {
background: #5568d3;
}
.chat-input button:disabled {
background: #d1d5db;
cursor: not-allowed;
}
.reasoning-panel {
width: 400px;
overflow-y: auto;
padding: 20px;
background: #f9fafb;
}
.panel-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 16px;
color: #111827;
display: flex;
align-items: center;
gap: 8px;
}
.panel-section {
background: white;
border-radius: 8px;
padding: 16px;
margin-bottom: 12px;
border: 1px solid #e5e7eb;
}
.panel-section-title {
font-weight: 600;
margin-bottom: 8px;
color: #374151;
font-size: 14px;
}
.panel-section-content {
font-size: 13px;
color: #6b7280;
line-height: 1.6;
}
.badge {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
margin-right: 6px;
margin-bottom: 4px;
}
.badge.intent {
background: #dbeafe;
color: #1e40af;
}
.badge.good {
background: #d1fae5;
color: #065f46;
}
.badge.warning {
background: #fef3c7;
color: #92400e;
}
.badge.error {
background: #fee2e2;
color: #991b1b;
}
.score-display {
display: flex;
align-items: center;
gap: 8px;
margin-top: 8px;
}
.score-bar {
flex: 1;
height: 8px;
background: #e5e7eb;
border-radius: 4px;
overflow: hidden;
}
.score-fill {
height: 100%;
border-radius: 4px;
transition: width 0.3s;
}
.score-fill.good {
background: #10b981;
}
.score-fill.warning {
background: #f59e0b;
}
.score-fill.error {
background: #ef4444;
}
.knowledge-item {
padding: 8px;
background: #f3f4f6;
border-radius: 6px;
margin-bottom: 8px;
}
.knowledge-question {
font-weight: 500;
color: #374151;
margin-bottom: 4px;
}
.knowledge-similarity {
font-size: 12px;
color: #10b981;
}
.loading {
text-align: center;
padding: 20px;
color: #6b7280;
}
.spinner {
border: 3px solid #e5e7eb;
border-top: 3px solid #667eea;
border-radius: 50%;
width: 24px;
height: 24px;
animation: spin 1s linear infinite;
margin: 0 auto 8px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.examples {
padding: 16px;
background: #f0fdf4;
border: 1px solid #bbf7d0;
border-radius: 8px;
margin: 20px;
}
.examples-title {
font-weight: 600;
margin-bottom: 8px;
color: #166534;
}
.example-button {
display: inline-block;
padding: 6px 12px;
background: white;
border: 1px solid #86efac;
border-radius: 6px;
margin: 4px;
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
}
.example-button:hover {
background: #f0fdf4;
border-color: #22c55e;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>HR智能对话助手</h1>
<p>基于RAG的检索增强生成 · 自动展示模型判断依据</p>
</div>
<div class="main-content">
<div class="chat-section">
<div class="chat-messages" id="chatMessages">
<div class="examples">
<div class="examples-title">试试这些问题:</div>
<button class="example-button" onclick="askQuestion('我想申请机器学习培训')">我想申请机器学习培训</button>
<button class="example-button" onclick="askQuestion('社保怎么缴纳?')">社保怎么缴纳?</button>
<button class="example-button" onclick="askQuestion('年假有多少天?')">年假有多少天?</button>
<button class="example-button" onclick="askQuestion('我想申请离职')">我想申请离职</button>
</div>
</div>
<div class="chat-input">
<input type="text" id="userInput" placeholder="请输入您的问题..." onkeypress="handleKeyPress(event)">
<button id="sendButton" onclick="sendMessage()">发送</button>
</div>
</div>
<div class="reasoning-panel" id="reasoningPanel">
<div class="panel-title">
<span>模型判断依据</span>
</div>
<div class="panel-section">
<div class="panel-section-content" style="text-align: center; color: #9ca3af; padding: 40px 0;">
发送消息后,这里将展示AI的完整思考过程
</div>
</div>
</div>
</div>
</div>
<script>
// Use relative path to support any port
const API_URL = '/api/v1/chat';
let conversationHistory = [];
let sessionId = 'web_' + Date.now();
function askQuestion(question) {
document.getElementById('userInput').value = question;
sendMessage();
}
function handleKeyPress(event) {
if (event.key === 'Enter') {
sendMessage();
}
}
async function sendMessage() {
const input = document.getElementById('userInput');
const message = input.value.trim();
if (!message) return;
// 添加用户消息
addMessage('user', message);
input.value = '';
// 禁用发送按钮
document.getElementById('sendButton').disabled = true;
// 显示加载中
addLoadingIndicator();
try {
const response = await fetch(API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
question: message,
conversation_history: conversationHistory,
show_reasoning: true,
session_id: sessionId
})
});
const data = await response.json();
// 移除加载中
removeLoadingIndicator();
// 添加助手消息(使用打字机效果)
const answer = data.answer || '抱歉,无法获取回复';
addMessage('assistant', answer, true);
// 更新推理面板
updateReasoningPanel(data.reasoning);
// 更新对话历史
conversationHistory = data.conversation_history || [];
} catch (error) {
removeLoadingIndicator();
addMessage('assistant', '抱歉,发生了错误:' + error.message, true);
console.error('Error:', error);
}
// 启用发送按钮
document.getElementById('sendButton').disabled = false;
}
function addMessage(role, content, animate = false) {
const messagesDiv = document.getElementById('chatMessages');
const messageDiv = document.createElement('div');
messageDiv.className = `message ${role}`;
const avatar = role === 'user' ? '员' : 'HR';
const avatarClass = role === 'user' ? 'user' : 'assistant';
messageDiv.innerHTML = `
<div class="message-avatar ${avatarClass}">${avatar}</div>
<div class="message-content"></div>
`;
messagesDiv.appendChild(messageDiv);
const contentDiv = messageDiv.querySelector('.message-content');
// 助手消息使用打字机效果
if (role === 'assistant' && animate) {
typeWriter(content, contentDiv, messagesDiv);
} else {
contentDiv.innerHTML = content;
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
}
// 打字机效果
function typeWriter(text, element, container) {
let index = 0;
const speed = 15; // 打字速度(毫秒)
function type() {
if (index < text.length) {
element.innerHTML += text.charAt(index);
index++;
container.scrollTop = container.scrollHeight;
setTimeout(type, speed);
}
}
type();
}
function addLoadingIndicator() {
const messagesDiv = document.getElementById('chatMessages');
const loadingDiv = document.createElement('div');
loadingDiv.className = 'message assistant';
loadingDiv.id = 'loadingIndicator';
loadingDiv.innerHTML = `
<div class="message-avatar assistant">HR</div>
<div class="message-content">
<div class="spinner"></div>
<div style="text-align: center; font-size: 12px;">正在思考...</div>
</div>
`;
messagesDiv.appendChild(loadingDiv);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
function removeLoadingIndicator() {
const loading = document.getElementById('loadingIndicator');
if (loading) loading.remove();
}
function updateReasoningPanel(reasoning) {
const panel = document.getElementById('reasoningPanel');
if (!reasoning) {
panel.innerHTML = `
<div class="panel-title">
<span>模型判断依据</span>
</div>
<div class="panel-section">
<div class="panel-section-content">无判断依据数据</div>
</div>
`;
return;
}
// 新数据结构解析
const layer1 = reasoning.layer1_intelligence_analysis || {};
const layer2 = reasoning.layer2_execution || {};
const replyInstruction = reasoning.reply_instruction || {};
// 意图理解
const intentUnderstanding = layer1.intent_understanding || {};
// 场景识别
const scenarioRecognition = layer1.scenario_recognition || {};
// 信息提取
const informationExtraction = layer1.information_extraction || {};
// 情绪分析
const emotionAnalysis = layer1.emotion_analysis || {};
// 风险评估
const riskAssessment = layer1.risk_assessment || {};
// 正确性检查
const correctnessCheck = layer2.correctness_check || {};
// 合规性检查
const complianceCheck = layer2.compliance_check || {};
// 质量分数
const qualityScore = layer2.quality_score || 0;
panel.innerHTML = `
<div class="panel-title">
<span>模型判断依据</span>
<span style="float: right; font-size: 14px; font-weight: normal; color: #6b7280;">
质量分数: <strong style="color: ${qualityScore >= 80 ? '#10b981' : qualityScore >= 60 ? '#f59e0b' : '#ef4444'}">${qualityScore}</strong>
</span>
</div>
<!-- 意图理解 -->
<div class="panel-section">
<div class="panel-section-title">
<span class="badge intent">意图理解</span>
</div>
<div class="panel-section-content">
<strong>识别意图:</strong> ${intentUnderstanding.detected_intent || 'unknown'}<br>
<strong>置信度:</strong> ${(intentUnderstanding.confidence * 100 || 0).toFixed(1)}%
</div>
</div>
<!-- 场景识别 -->
<div class="panel-section">
<div class="panel-section-title">
<span class="badge intent">场景识别</span>
</div>
<div class="panel-section-content">
<strong>场景:</strong> ${scenarioRecognition.identified_scenario || 'unknown'}<br>
<strong>置信度:</strong> ${(scenarioRecognition.confidence * 100 || 0).toFixed(1)}%<br>
<strong>描述:</strong> ${scenarioRecognition.description || 'N/A'}
</div>
</div>
<!-- 信息提取 -->
${Object.keys(informationExtraction).length > 0 ? `
<div class="panel-section">
<div class="panel-section-title">
<span class="badge intent">信息提取</span>
</div>
<div class="panel-section-content">
${informationExtraction.extracted_data ? Object.entries(informationExtraction.extracted_data).map(([key, value]) => `
<div style="padding: 4px 0; border-bottom: 1px solid #f3f4f6;">
<strong>${key}:</strong> ${value}
</div>
`).join('') : ''}
<div style="margin-top: 8px; font-size: 12px;">
提取字段: ${(informationExtraction.extracted_fields || []).join(', ') || 'N/A'}
</div>
</div>
</div>
` : ''}
<!-- 情绪分析 -->
<div class="panel-section">
<div class="panel-section-title">
<span class="badge ${emotionAnalysis.emotion === 'positive' ? 'good' : emotionAnalysis.emotion === 'negative' ? 'error' : 'warning'}">
情绪分析
</span>
</div>
<div class="panel-section-content">
<strong>情绪:</strong> ${emotionAnalysis.emotion || 'neutral'}<br>
<strong>强度:</strong> ${(emotionAnalysis.intensity * 100 || 0).toFixed(0)}%
</div>
</div>
<!-- 正确性检查 -->
<div class="panel-section">
<div class="panel-section-title">
<span class="badge ${correctnessCheck.is_correct ? 'good' : 'warning'}">
正确性检查
</span>
</div>
<div class="panel-section-content">
${correctnessCheck.is_question ? `
<strong>类型:</strong> 追问<br>
<strong>状态:</strong> <span class="badge good">合理</span>
` : `
<strong>相似度分数:</strong> ${((correctnessCheck.similarity_score || 0) * 100).toFixed(1)}%<br>
<strong>等级:</strong> ${correctnessCheck.level || 'unknown'}
`}
</div>
</div>
<!-- 合规性检查 -->
<div class="panel-section">
<div class="panel-section-title">
<span class="badge ${complianceCheck.is_compliant ? 'good' : 'error'}">
合规性检查
</span>
</div>
<div class="panel-section-content">
<strong>状态:</strong> ${complianceCheck.is_compliant ? '<span class="badge good">合规</span>' : '<span class="badge error">违规</span>'}
${complianceCheck.violations && complianceCheck.violations.length > 0 ? `
<div style="margin-top: 8px; padding: 8px; background: #fee2e2; border-radius: 4px;">
<strong style="color: #991b1b;">检测到违规:</strong>
<ul style="margin: 4px 0 0 20px; color: #991b1b;">
${complianceCheck.violations.map(v => `<li>${v.type || v.category || '违规'}: ${v.text || v.word || 'N/A'}</li>`).join('')}
</ul>
</div>
` : ''}
</div>
</div>
`;
}
</script>
</body>
</html>