267 lines
12 KiB
JavaScript
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, "<").replace(/>/g, ">");
|
|
|
|
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();
|
|
}
|
|
});
|