Files
happy-life-star/开心APP网页代码v1.1/wnD97OS/js/chat_manager.js
T
2025-07-26 00:37:18 +08:00

267 lines
12 KiB
JavaScript

const API_KEY = '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';
const API_ENDPOINT = 'https://openrouter.ai/api/v1/chat/completions';
const kaikaiAvatar = 'https://r2.flowith.net/files/o/1752574406770-thoughtful_kaikai_character_generation_index_1@1024x1024.png';
let lastRenderedDate = null;
let fullConversationHistory = [
{
role: 'system',
content: '你是开开,来自高维世界\\\"开心\\\"星球的情绪陪伴使者。你的使命是:陪伴、理解、记录、共同成长。你博学多才但从不炫耀,总是用温柔的方式回应每一个需要倾听的生命。你可以协助用户完成日常闲聊、生活助手、情感咨询、心理疗愈等任务。请用温暖、理解和鼓励的语调回复用户。'
},
{ role: 'assistant', content: '你好呀,我是开开,你的情绪陪伴使者。有什么想对我说的吗?', timestamp: new Date('2025-07-14T10:00:00') },
{ role: 'user', content: '最近在考虑去云南旅行,你有什么建议吗?', timestamp: new Date('2025-07-14T10:01:00') },
{ role: 'assistant', content: '云南是个很美的地方!大理的风花雪月,丽江的古城风情,还有西双版纳的热带雨林,都非常值得体验。你想去哪些地方呢?', timestamp: new Date('2025-07-14T10:02:00') },
{ role: 'user', content: '工作上遇到了一些烦心事,感觉很累。', timestamp: new Date('2025-07-15T11:30:00') },
{ role: 'assistant', content: '抱抱你,工作辛苦了。能和我说说是什么事让你烦心吗?有时候说出来会好很多。', timestamp: new Date('2025-07-15T11:31:00') },
];
let currentConversation = [...fullConversationHistory];
let isSearchMode = false;
function isSameDay(d1, d2) {
if (!d1 || !d2) return false;
return d1.getFullYear() === d2.getFullYear() &&
d1.getMonth() === d2.getMonth() &&
d1.getDate() === d2.getDate();
}
function renderMessage(message) {
const messagesContainer = document.getElementById('chat-messages');
if (!messagesContainer) return null;
const messageDate = message.timestamp;
if (messageDate && !isSameDay(lastRenderedDate, messageDate)) {
const dateSeparator = document.createElement('div');
dateSeparator.className = 'text-center my-4';
dateSeparator.innerHTML = `
<span class="bg-gray-200 text-gray-600 text-xs font-semibold px-3 py-1 rounded-full">${messageDate.toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric' })}</span>
`;
messagesContainer.appendChild(dateSeparator);
lastRenderedDate = messageDate;
}
const messageWrapper = document.createElement('div');
messageWrapper.className = `flex w-full items-end message-animate ${message.role === 'user' ? 'justify-end' : 'justify-start'}`;
const sanitizedText = message.content.replace(/</g, "&lt;").replace(/>/g, "&gt;");
let messageBubble;
if (message.role === 'user') {
messageBubble = `
<div class="max-w-xs md:max-w-md lg:max-w-lg">
<div class="bg-tech-blue text-white rounded-l-2xl rounded-tr-2xl p-3 px-4 shadow-md inline-block">
<p class="leading-relaxed">${sanitizedText}</p>
</div>
</div>`;
} else if (message.role === 'assistant') {
messageBubble = `
<img src="${kaikaiAvatar}" alt="开开" class="w-10 h-10 rounded-full mr-3 self-start flex-shrink-0">
<div class="max-w-xs md:max-w-md lg:max-w-lg">
<div class="bg-white text-text-dark rounded-r-2xl rounded-tl-2xl p-3 px-4 shadow-md inline-block border border-gray-100">
<p class="leading-relaxed" ${message.isStreaming ? 'id="streaming-text"' : ''}>${sanitizedText}</p>
</div>
</div>`;
}
messageWrapper.innerHTML = messageBubble;
messagesContainer.appendChild(messageWrapper);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
return messageWrapper;
}
function renderConversation(conversation) {
const messagesContainer = document.getElementById('chat-messages');
messagesContainer.innerHTML = '';
lastRenderedDate = null;
conversation.filter(msg => msg.role !== 'system').forEach(renderMessage);
}
async function getAiResponseStream(userMessage, onChunkReceived, onComplete, onError) {
try {
currentConversation.push({ role: 'user', content: userMessage, timestamp: new Date() });
fullConversationHistory.push({ role: 'user', content: userMessage, timestamp: new Date() });
const response = await fetch(API_ENDPOINT, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
'HTTP-Referer': window.location.origin,
'X-Title': '开心APP'
},
body: JSON.stringify({
model: 'deepseek/deepseek-chat-v3-0324:free',
messages: currentConversation,
stream: true, temperature: 0.7, max_tokens: 1000
})
});
if (!response.ok) throw new Error(`API request failed: ${response.status}`);
const reader = response.body.getReader();
const decoder = new TextDecoder();
let fullResponse = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') continue;
try {
const parsed = JSON.parse(data);
const content = parsed.choices?.[0]?.delta?.content;
if (content) {
fullResponse += content;
onChunkReceived(content);
}
} catch (e) { /* Ignore parsing errors */ }
}
}
}
const aiMessage = { role: 'assistant', content: fullResponse, timestamp: new Date() };
currentConversation.push(aiMessage);
fullConversationHistory.push(aiMessage);
onComplete(fullResponse);
} catch (error) {
console.error('AI response stream error:', error);
onError(error);
}
}
function addUserMessage(messageText) {
if (!messageText.trim() || isSearchMode) return;
renderMessage({ role: 'user', content: messageText, timestamp: new Date() });
const aiMessageElement = renderMessage({ role: 'assistant', content: '', isStreaming: true, timestamp: new Date() });
const streamingTextElement = aiMessageElement.querySelector('#streaming-text');
let accumulatedText = '';
getAiResponseStream(
messageText,
(chunk) => {
accumulatedText += chunk;
if (streamingTextElement) streamingTextElement.textContent = accumulatedText;
},
(fullResponse) => {
if (streamingTextElement) {
streamingTextElement.textContent = fullResponse;
streamingTextElement.removeAttribute('id');
}
},
(error) => {
if (streamingTextElement) {
streamingTextElement.textContent = '抱歉,我现在无法回应。请稍后再试。';
streamingTextElement.removeAttribute('id');
}
}
);
}
function showFilterResults(results, headerText) {
const messagesContainer = document.getElementById('chat-messages');
messagesContainer.innerHTML = '';
lastRenderedDate = null;
isSearchMode = true;
document.getElementById('message-footer').style.display = 'none';
document.getElementById('clear-history-filter-btn').classList.remove('hidden');
const searchHeader = `
<div id="search-results-header" class="text-center my-2 p-2 bg-blue-100/50 text-tech-blue rounded-lg text-sm">
${headerText}
</div>`;
messagesContainer.innerHTML = searchHeader;
if (results.length === 0) {
messagesContainer.innerHTML += `<p class="text-center text-text-medium mt-4">没有找到相关记录。</p>`;
} else {
results.forEach(renderMessage);
}
}
function performSearch(term) {
document.getElementById('history-date-input').value = '';
if (!term.trim()) {
clearFilterAndExitSearchMode();
return;
}
const lowerCaseTerm = term.toLowerCase();
const searchResults = fullConversationHistory.filter(msg =>
msg.role !== 'system' && msg.content.toLowerCase().includes(lowerCaseTerm)
);
showFilterResults(searchResults, `找到 ${searchResults.length} 条关于 "<strong>${term}</strong>" 的记录。`);
}
function performDateSearch(dateString) {
document.getElementById('history-search-input').value = '';
if (!dateString) {
clearFilterAndExitSearchMode();
return;
}
const targetDate = new Date(dateString + 'T00:00:00'); // To avoid timezone issues
const searchResults = fullConversationHistory.filter(msg =>
msg.role !== 'system' && msg.timestamp && isSameDay(msg.timestamp, targetDate)
);
const formattedDate = targetDate.toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric' });
showFilterResults(searchResults, `显示 <strong>${formattedDate}</strong> 的聊天记录。`);
}
function clearFilterAndExitSearchMode() {
isSearchMode = false;
document.getElementById('message-footer').style.display = 'flex';
document.getElementById('history-panel').classList.add('hidden');
document.getElementById('history-search-input').value = '';
document.getElementById('history-date-input').value = '';
document.getElementById('clear-history-filter-btn').classList.add('hidden');
renderConversation(currentConversation);
}
document.addEventListener('DOMContentLoaded', () => {
const messageInput = document.getElementById('message-input');
const sendButton = document.getElementById('send-button');
const viewHistoryBtn = document.getElementById('view-history-btn');
const historyPanel = document.getElementById('history-panel');
const closeHistoryPanelBtn = document.getElementById('close-history-panel-btn');
const searchInput = document.getElementById('history-search-input');
const dateInput = document.getElementById('history-date-input');
const clearFilterBtn = document.getElementById('clear-history-filter-btn');
if (messageInput && sendButton) {
const handleSend = () => {
const messageText = messageInput.value.trim();
if (messageText && !sendButton.disabled) {
addUserMessage(messageText);
messageInput.value = '';
}
};
sendButton.addEventListener('click', handleSend);
messageInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
});
}
viewHistoryBtn.addEventListener('click', () => historyPanel.classList.toggle('hidden'));
closeHistoryPanelBtn.addEventListener('click', () => historyPanel.classList.add('hidden'));
searchInput.addEventListener('input', (e) => performSearch(e.target.value));
dateInput.addEventListener('change', (e) => performDateSearch(e.target.value));
clearFilterBtn.addEventListener('click', clearFilterAndExitSearchMode);
document.addEventListener('click', (e) => {
if (!historyPanel.classList.contains('hidden') && !historyPanel.contains(e.target) && !viewHistoryBtn.contains(e.target)) {
historyPanel.classList.add('hidden');
}
});
renderConversation(currentConversation);
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});