|
|
<!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> |
|
|
|