2 Commits
Author SHA1 Message Date
hubian bcb0fbb384 feat: 支持文字输入对话 2026-04-21 18:51:08 +08:00
hubian 0dced68876 feat: 用户语音消息支持点击播放 2026-04-21 18:45:42 +08:00
4 changed files with 221 additions and 7 deletions
Binary file not shown.
BIN
View File
Binary file not shown.
+41
View File
@@ -130,6 +130,47 @@ async def voice_chat(
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
@app.post("/voice/text", response_model=VoiceResponse)
async def text_chat(
text: str = Form(..., description="文本消息"),
conversation_id: Optional[str] = Form(None, description="对话ID")
):
"""
文字聊天接口
转发到模型服务
"""
try:
async with aiohttp.ClientSession() as session:
form = aiohttp.FormData()
form.add_field('text', text)
if conversation_id:
form.add_field('conversation_id', conversation_id)
async with session.post(
f"{MODEL_SERVICE_URL}/api/voice/text",
data=form,
timeout=aiohttp.ClientTimeout(total=120)
) as resp:
if resp.status != 200:
error_text = await resp.text()
logger.error(f"Model service error: {error_text}")
raise HTTPException(status_code=resp.status, detail=error_text)
data = await resp.json()
return VoiceResponse(
reply=data["reply"],
conversation_id=data["conversation_id"],
timestamp=data.get("timestamp", datetime.now().isoformat())
)
except aiohttp.ClientError as e:
logger.error(f"Connection error: {e}")
raise HTTPException(status_code=503, detail="模型服务连接失败")
except Exception as e:
logger.error(f"Text chat error: {e}", exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
@app.delete("/conversation/{conversation_id}") @app.delete("/conversation/{conversation_id}")
async def delete_conversation(conversation_id: str): async def delete_conversation(conversation_id: str):
"""删除对话""" """删除对话"""
+180 -7
View File
@@ -127,6 +127,50 @@
font-weight: bold; font-weight: bold;
} }
.text-section {
margin: 20px 0;
}
.text-input-wrapper {
display: flex;
gap: 10px;
}
.text-input {
flex: 1;
padding: 12px 15px;
border: 2px solid #eee;
border-radius: 10px;
font-size: 15px;
outline: none;
transition: border-color 0.2s;
}
.text-input:focus {
border-color: #667eea;
}
.send-text-btn {
padding: 12px 20px;
border: none;
border-radius: 10px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
font-size: 15px;
cursor: pointer;
transition: all 0.2s;
}
.send-text-btn:hover {
transform: scale(1.05);
}
.send-text-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.waveform { .waveform {
display: flex; display: flex;
justify-content: center; justify-content: center;
@@ -196,6 +240,40 @@
line-height: 1.5; line-height: 1.5;
} }
.audio-content {
display: flex;
align-items: center;
}
.play-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 15px;
border-radius: 20px;
border: none;
background: rgba(255,255,255,0.2);
cursor: pointer;
transition: all 0.2s;
font-size: 14px;
}
.play-btn:hover {
background: rgba(255,255,255,0.3);
}
.play-btn.playing {
background: rgba(255,255,255,0.4);
}
.play-icon {
font-size: 16px;
}
.duration {
color: rgba(255,255,255,0.8);
}
.loading { .loading {
display: flex; display: flex;
justify-content: center; justify-content: center;
@@ -285,6 +363,13 @@
</div> </div>
</div> </div>
<div class="text-section">
<div class="text-input-wrapper">
<input type="text" id="textInput" placeholder="输入文字消息..." class="text-input">
<button id="sendTextBtn" class="send-text-btn">发送</button>
</div>
</div>
<div class="chat-section" id="chatSection"> <div class="chat-section" id="chatSection">
<div class="hint">开始你的第一次语音对话吧!</div> <div class="hint">开始你的第一次语音对话吧!</div>
</div> </div>
@@ -316,6 +401,46 @@
const clearBtn = document.getElementById('clearBtn'); const clearBtn = document.getElementById('clearBtn');
const statusDot = document.getElementById('statusDot'); const statusDot = document.getElementById('statusDot');
const statusText = document.getElementById('statusText'); const statusText = document.getElementById('statusText');
const textInput = document.getElementById('textInput');
const sendTextBtn = document.getElementById('sendTextBtn');
// 发送文字消息
async function sendText(text) {
if (!text.trim()) return;
try {
showLoading();
const formData = new FormData();
formData.append('text', text);
if (conversationId) {
formData.append('conversation_id', conversationId);
}
const resp = await fetch(`${API_URL}/voice/text`, {
method: 'POST',
body: formData
});
if (!resp.ok) {
const error = await resp.text();
throw new Error(error);
}
const data = await resp.json();
conversationId = data.conversation_id;
// 显示消息
addMessage('user', text);
addMessage('assistant', data.reply);
textInput.value = '';
} catch (e) {
console.error('发送失败:', e);
showError('发送失败: ' + e.message);
}
}
// 检查服务状态 // 检查服务状态
async function checkStatus() { async function checkStatus() {
@@ -470,6 +595,9 @@
try { try {
showLoading(); showLoading();
// 计算音频时长
const duration = Math.round(recordedBuffers.reduce((acc, buf) => acc + buf.length, 0) / 16000);
const formData = new FormData(); const formData = new FormData();
formData.append('audio', audioBlob, 'recording.wav'); formData.append('audio', audioBlob, 'recording.wav');
if (conversationId) { if (conversationId) {
@@ -489,8 +617,8 @@
const data = await resp.json(); const data = await resp.json();
conversationId = data.conversation_id; conversationId = data.conversation_id;
// 显示消息 // 显示消息(带音频播放)
addMessage('user', '🎵 语音消息'); addMessage('user', audioBlob, duration);
addMessage('assistant', data.reply); addMessage('assistant', data.reply);
recordStatus.textContent = '点击按钮开始录音'; recordStatus.textContent = '点击按钮开始录音';
@@ -503,7 +631,7 @@
} }
// 添加消息 // 添加消息
function addMessage(role, content) { function addMessage(role, content, audioDuration = null) {
// 移除提示 // 移除提示
const hint = chatSection.querySelector('.hint'); const hint = chatSection.querySelector('.hint');
if (hint) hint.remove(); if (hint) hint.remove();
@@ -514,16 +642,50 @@
const msg = document.createElement('div'); const msg = document.createElement('div');
msg.className = `message ${role}`; msg.className = `message ${role}`;
msg.innerHTML = `
<div class="role">${role === 'user' ? '我' : 'AI'}</div> // 用户消息可能是音频
<div class="content">${content}</div> if (role === 'user' && content instanceof Blob) {
`; const audioUrl = URL.createObjectURL(content);
const durationText = audioDuration ? `${audioDuration}s` : '';
msg.innerHTML = `
<div class="role">我</div>
<div class="content audio-content">
<button class="play-btn" onclick="playAudio('${audioUrl}', this)">
<span class="play-icon">▶️</span>
<span class="duration">${durationText}</span>
</button>
</div>
`;
} else {
msg.innerHTML = `
<div class="role">${role === 'user' ? '我' : 'AI'}</div>
<div class="content">${content}</div>
`;
}
chatSection.appendChild(msg); chatSection.appendChild(msg);
// 滚动到底部 // 滚动到底部
chatSection.scrollTop = chatSection.scrollHeight; chatSection.scrollTop = chatSection.scrollHeight;
} }
// 播放音频
function playAudio(audioUrl, btn) {
const audio = new Audio(audioUrl);
const icon = btn.querySelector('.play-icon');
audio.onplay = () => {
icon.textContent = '🔊';
btn.classList.add('playing');
};
audio.onended = () => {
icon.textContent = '▶️';
btn.classList.remove('playing');
};
audio.play();
}
// 显示加载 // 显示加载
function showLoading() { function showLoading() {
const hint = chatSection.querySelector('.hint'); const hint = chatSection.querySelector('.hint');
@@ -579,6 +741,17 @@
clearBtn.addEventListener('click', clearChat); clearBtn.addEventListener('click', clearChat);
// 文字输入事件
sendTextBtn.addEventListener('click', () => {
sendText(textInput.value);
});
textInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
sendText(textInput.value);
}
});
// 初始化 // 初始化
checkStatus(); checkStatus();
setInterval(checkStatus, 10000); // 每10秒检查状态 setInterval(checkStatus, 10000); // 每10秒检查状态