/** * 聊天页功能面板 + 展开输入(语音/多条消息/混合消息) */ import { calculateVoiceDuration, escapeHtml, sleep } from './utils.js'; import { showToast } from './toast.js'; import { sendMessage, sendPhotoMessage, sendBatchMessages, appendMusicCardMessage, currentChatIndex, appendMessage, showTypingIndicator, hideTypingIndicator, parseAiQuoteMessage, detectAiCallRequest } from './chat.js'; import { isInGroupChat, sendGroupMessage, sendGroupPhotoMessage, sendGroupBatchMessages, getCurrentGroupIndex, appendGroupMessage, showGroupTypingIndicator, hideGroupTypingIndicator, callGroupAI, enforceGroupChatMemberLimit, appendGroupMusicCardMessage } from './group-chat.js'; import { startVoiceCall } from './voice-call.js'; import { startVideoCall } from './video-call.js'; import { showMusicPanel, initMusicEvents } from './music.js'; import { getSettings, splitAIMessages } from './config.js'; import { refreshChatList } from './ui.js'; import { saveSettingsDebounced } from '../../../../script.js'; import { callAI } from './ai.js'; let expandMode = null; // 'voice' | 'multi' | null // 混合消息项: { type: 'text' | 'voice' | 'sticker' | 'photo', content: string } let expandMsgItems = [{ type: 'text', content: '' }]; let funcPanelPage = 0; let funcPanelInited = false; // 临时存储待插入的表情URL let pendingStickerIndex = -1; let musicShareListenerInited = false; function safeText(value) { return value == null ? '' : String(value).trim(); } function clipText(text, maxChars) { const raw = safeText(text); if (!raw) return ''; if (raw.length <= maxChars) return raw; return raw.slice(0, maxChars - 1) + '…'; } function clipLyrics(lyrics) { const raw = safeText(lyrics); if (!raw) return ''; // 移除时间标签,只保留歌词文本 const lines = raw.split(/\r?\n/) .map(line => line.replace(/^\[\d{2}:\d{2}[.\d]*\]/g, '').trim()) .filter(line => line); const limitedLines = lines.slice(0, 30).join('\n'); return clipText(limitedLines, 800); } function formatMusicShareMessage(song) { const name = safeText(song?.name) || '未知歌曲'; const artist = safeText(song?.artist); const lyrics = clipLyrics(song?.lyrics); let message = `[分享音乐] ${name}`; if (artist) message += ` - ${artist}`; if (lyrics) message += `\n\n${lyrics}`; return message; } function initMusicShareListener() { if (musicShareListenerInited) return; musicShareListenerInited = true; document.addEventListener('music-share', async (e) => { const song = e?.detail; if (!song) return; const settings = getSettings(); const groupIndex = getCurrentGroupIndex(); // 构建给AI的消息(包含歌名歌手和歌词) const name = safeText(song?.name) || '未知歌曲'; const artist = safeText(song?.artist); const lyrics = clipLyrics(song?.lyrics); let aiMessage = `[分享音乐] ${name}`; if (artist) aiMessage += ` - ${artist}`; if (lyrics) aiMessage += `\n歌词:\n${lyrics}`; const now = new Date(); const timeStr = `${now.getFullYear()}-${(now.getMonth()+1).toString().padStart(2,'0')}-${now.getDate().toString().padStart(2,'0')} ${now.getHours().toString().padStart(2,'0')}:${now.getMinutes().toString().padStart(2,'0')}`; // 群聊分享音乐 if (groupIndex >= 0) { const groupChat = settings.groupChats?.[groupIndex]; if (!groupChat) return; if (!Array.isArray(groupChat.chatHistory)) { groupChat.chatHistory = []; } // 显示音乐卡片 appendGroupMusicCardMessage('user', song); // 保存到聊天历史 groupChat.chatHistory.push({ role: 'user', content: aiMessage, time: timeStr, timestamp: Date.now(), isMusic: true, musicInfo: { name: song.name, artist: song.artist, platform: song.platform, cover: song.cover, id: song.id } }); groupChat.lastMessage = `[音乐] ${name}`; groupChat.lastMessageTime = Date.now(); saveSettingsDebounced(); refreshChatList(); // 获取成员信息 const { memberIds } = enforceGroupChatMemberLimit(groupChat); const members = memberIds.map(id => settings.contacts.find(c => c.id === id)).filter(Boolean); if (members.length === 0) { showToast('群聊成员不存在', '⚠️'); return; } // 显示打字指示器 showGroupTypingIndicator(members[0]?.name, members[0]?.id); try { // 调用群聊AI const responses = await callGroupAI(groupChat, members, aiMessage, []); hideGroupTypingIndicator(); // 逐条显示AI回复 for (let i = 0; i < responses.length; i++) { const resp = responses[i]; // 显示typing指示器并等待 showGroupTypingIndicator(resp.characterName, resp.characterId); await sleep(800 + Math.random() * 400); hideGroupTypingIndicator(); // 保存并显示消息 groupChat.chatHistory.push({ role: 'assistant', content: resp.content, time: timeStr, timestamp: Date.now(), characterName: resp.characterName, characterId: resp.characterId }); appendGroupMessage('assistant', resp.content, resp.characterName, resp.characterId); } if (responses.length > 0) { const lastResp = responses[responses.length - 1]; groupChat.lastMessage = lastResp.content.length > 20 ? lastResp.content.substring(0, 20) + '...' : lastResp.content; groupChat.lastMessageTime = Date.now(); } saveSettingsDebounced(); refreshChatList(); } catch (err) { hideGroupTypingIndicator(); console.error('[可乐] 群聊音乐分享AI回复失败:', err); } return; } // 单聊分享音乐 if (currentChatIndex < 0) return; const contactIndex = currentChatIndex; const contact = settings.contacts[contactIndex]; if (!contact) return; if (!contact.chatHistory) { contact.chatHistory = []; } // 显示音乐卡片 appendMusicCardMessage('user', song, contact); // 保存到聊天历史 contact.chatHistory.push({ role: 'user', content: aiMessage, time: timeStr, timestamp: Date.now(), isMusic: true, musicInfo: { name: song.name, artist: song.artist, platform: song.platform, cover: song.cover, id: song.id } }); contact.lastMessage = `[音乐] ${name}`; saveSettingsDebounced(); refreshChatList(); // 调用AI回复 showTypingIndicator(contact); try { const aiReply = await callAI(contact, aiMessage); hideTypingIndicator(); if (aiReply) { // 使用 splitAIMessages 分割AI回复 const aiMessages = splitAIMessages(aiReply); let lastShownMessage = null; for (let i = 0; i < aiMessages.length; i++) { const rawMsg = aiMessages[i]; // 兼容 AI 发起通话请求(如:[通话请求] / [语音通话请求] / [视频通话请求]),不显示为文本 const callRequestType = detectAiCallRequest(rawMsg); if (callRequestType === 'voice') { startVoiceCall('ai', contactIndex); break; // 通话请求必须单独一条 } if (callRequestType === 'video') { startVideoCall('ai', contactIndex); break; // 通话请求必须单独一条 } // 解析 [回复:xxx] 引用格式,避免把标记直接显示出来 const parsed = parseAiQuoteMessage(rawMsg, contact); const msg = (parsed?.content || '').toString().trim(); const quote = parsed?.quote || null; if (!msg) continue; contact.chatHistory.push({ role: 'assistant', content: msg, time: timeStr, timestamp: Date.now(), quote: quote || undefined }); appendMessage('assistant', msg, contact, false, quote); lastShownMessage = msg; } if (lastShownMessage) { contact.lastMessage = lastShownMessage.length > 20 ? lastShownMessage.substring(0, 20) + '...' : lastShownMessage; } saveSettingsDebounced(); refreshChatList(); } } catch (err) { hideTypingIndicator(); console.error('[可乐] 音乐分享AI回复失败:', err); } }); } export function showExpandVoice() { expandMode = 'voice'; const panel = document.getElementById('wechat-expand-input'); const title = document.getElementById('wechat-expand-title'); const body = document.getElementById('wechat-expand-body'); if (!panel || !title || !body) return; title.textContent = '语音消息'; body.innerHTML = `
输入语音内容,系统会根据字数计算时长
预计时长: 0"
`; panel.classList.remove('hidden'); const textarea = document.getElementById('wechat-expand-voice-text'); textarea?.addEventListener('input', updateExpandVoiceDuration); setTimeout(() => textarea?.focus(), 50); } // 显示照片描述输入面板 export function showExpandPhoto() { expandMode = 'photo'; const panel = document.getElementById('wechat-expand-input'); const title = document.getElementById('wechat-expand-title'); const body = document.getElementById('wechat-expand-body'); if (!panel || !title || !body) return; title.textContent = '发送照片'; body.innerHTML = ` `; panel.classList.remove('hidden'); const textarea = document.getElementById('wechat-expand-photo-text'); setTimeout(() => textarea?.focus(), 50); } function updateExpandVoiceDuration() { const textarea = document.getElementById('wechat-expand-voice-text'); const durationEl = document.getElementById('wechat-expand-voice-duration'); if (!textarea || !durationEl) return; const content = textarea.value.trim(); const duration = content ? calculateVoiceDuration(content) : 0; durationEl.textContent = duration + '"'; } export function showExpandMulti() { expandMode = 'multi'; expandMsgItems = [{ type: 'text', content: '' }]; const panel = document.getElementById('wechat-expand-input'); const title = document.getElementById('wechat-expand-title'); if (!panel || !title) return; title.textContent = '混合消息'; renderExpandMsgList(); panel.classList.remove('hidden'); setTimeout(() => { const firstInput = document.querySelector('.wechat-expand-msg-input'); firstInput?.focus(); }, 50); } // 获取消息类型的线条图标 function getTypeIcon(type) { switch (type) { case 'voice': return ``; case 'sticker': return ``; case 'photo': return ``; default: // text return ``; } } // 获取消息类型标签 function getTypeLabel(type) { switch (type) { case 'voice': return '语音'; case 'sticker': return '表情'; case 'photo': return '照片'; default: return '文字'; } } function renderExpandMsgList() { const body = document.getElementById('wechat-expand-body'); if (!body) return; let html = '
'; expandMsgItems.forEach((item, index) => { const typeIcon = getTypeIcon(item.type); const typeLabel = getTypeLabel(item.type); html += `
${index + 1}
${typeIcon} ${typeLabel}
`; if (item.type === 'sticker') { // 表情类型:显示选择按钮或已选的表情预览 if (item.content) { html += `
\"表情\"
`; } else { html += ` `; } } else if (item.type === 'photo') { // 照片类型:输入图片描述 html += ` `; } else if (item.type === 'voice') { // 语音类型:输入框 + 时长显示 html += ` ${item.content ? calculateVoiceDuration(item.content) + '\"' : '0\"'} `; } else { // 文字类型:普通输入框 html += ` `; } if (expandMsgItems.length > 1) { html += ``; } html += `
`; }); html += '
'; html += ''; body.innerHTML = html; // 绑定输入事件 document.querySelectorAll('.wechat-expand-msg-input').forEach(input => { input.addEventListener('input', (e) => { const index = parseInt(e.target.dataset.index); expandMsgItems[index].content = e.target.value; // 更新语音时长显示 if (expandMsgItems[index].type === 'voice') { const durEl = e.target.parentElement.querySelector('.wechat-expand-voice-dur'); if (durEl) { const duration = e.target.value.trim() ? calculateVoiceDuration(e.target.value) : 0; durEl.textContent = duration + '\"'; } } }); input.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); addExpandMsgItem(); } }); }); // 绑定类型切换事件 document.querySelectorAll('.wechat-expand-msg-type').forEach(typeBtn => { typeBtn.addEventListener('click', (e) => { const index = parseInt(typeBtn.dataset.index); cycleMessageType(index); }); }); // 绑定删除事件 document.querySelectorAll('.wechat-expand-msg-del').forEach(btn => { btn.addEventListener('click', (e) => { const index = parseInt(e.target.dataset.index); expandMsgItems.splice(index, 1); renderExpandMsgList(); }); }); // 绑定表情选择事件 document.querySelectorAll('.wechat-expand-sticker-select, .wechat-expand-sticker-change').forEach(btn => { btn.addEventListener('click', (e) => { const index = parseInt(btn.dataset.index); openStickerPickerForMultiMsg(index); }); }); document.getElementById('wechat-expand-add-msg')?.addEventListener('click', addExpandMsgItem); } // 循环切换消息类型 function cycleMessageType(index) { const currentType = expandMsgItems[index].type; let newType; if (currentType === 'text') { newType = 'voice'; } else if (currentType === 'voice') { newType = 'sticker'; } else if (currentType === 'sticker') { newType = 'photo'; } else { newType = 'text'; } expandMsgItems[index] = { type: newType, content: '' }; renderExpandMsgList(); } function addExpandMsgItem() { expandMsgItems.push({ type: 'text', content: '' }); renderExpandMsgList(); setTimeout(() => { const inputs = document.querySelectorAll('.wechat-expand-msg-input'); const lastInput = inputs[inputs.length - 1]; lastInput?.focus(); }, 50); } // 打开表情选择器用于混合消息 function openStickerPickerForMultiMsg(index) { pendingStickerIndex = index; // 关闭展开面板,打开表情面板 const expandPanel = document.getElementById('wechat-expand-input'); const emojiPanel = document.getElementById('wechat-emoji-panel'); expandPanel?.classList.add('hidden'); emojiPanel?.classList.remove('hidden'); // 切换到贴纸标签 const stickerTab = document.querySelector('.wechat-emoji-tab[data-tab="sticker"]'); stickerTab?.click(); showToast('请选择表情', '😊'); } // 为混合消息设置表情(由emoji-panel调用) export function setStickerForMultiMsg(stickerUrl) { if (pendingStickerIndex < 0 || pendingStickerIndex >= expandMsgItems.length) { return false; } expandMsgItems[pendingStickerIndex].content = stickerUrl; const savedIndex = pendingStickerIndex; pendingStickerIndex = -1; // 关闭表情面板,重新打开展开面板 const emojiPanel = document.getElementById('wechat-emoji-panel'); emojiPanel?.classList.add('hidden'); // 重新显示混合消息面板 expandMode = 'multi'; const panel = document.getElementById('wechat-expand-input'); const title = document.getElementById('wechat-expand-title'); if (panel && title) { title.textContent = '混合消息'; renderExpandMsgList(); panel.classList.remove('hidden'); } return true; } // 检查是否有待选表情 export function hasPendingStickerSelection() { return pendingStickerIndex >= 0; } export function closeExpandPanel() { const panel = document.getElementById('wechat-expand-input'); panel?.classList.add('hidden'); expandMode = null; } export async function sendExpandContent() { const inGroup = isInGroupChat(); if (expandMode === 'voice') { const textarea = document.getElementById('wechat-expand-voice-text'); const content = textarea?.value.trim(); if (!content) { showToast('请输入语音内容', '🧊'); return; } closeExpandPanel(); if (inGroup) { sendGroupMessage(content, false, true); } else { sendMessage(content, false, true); } return; } if (expandMode === 'photo') { const textarea = document.getElementById('wechat-expand-photo-text'); const content = textarea?.value.trim(); if (!content) { showToast('请输入照片描述', '🧊'); return; } closeExpandPanel(); if (inGroup) { await sendGroupPhotoMessage(content); } else { await sendPhotoMessage(content); } return; } if (expandMode === 'multi') { // 过滤有效消息(文字/语音需要有内容,表情需要有URL) const validMessages = expandMsgItems.filter(m => { if (m.type === 'sticker') { return m.content && m.content.trim(); } return m.content && m.content.trim(); }); if (validMessages.length === 0) { showToast('请至少输入一条消息', '🧊'); return; } closeExpandPanel(); // 使用批量发送函数(一次性发完再调用AI) if (inGroup) { await sendGroupBatchMessages(validMessages); } else { await sendBatchMessages(validMessages); } } } export function toggleFuncPanel() { const panel = document.getElementById('wechat-func-panel'); const expandPanel = document.getElementById('wechat-expand-input'); const emojiPanel = document.getElementById('wechat-emoji-panel'); if (!panel || !expandPanel) return; if (!expandPanel.classList.contains('hidden')) { expandPanel.classList.add('hidden'); expandMode = null; } // 关闭表情面板 emojiPanel?.classList.add('hidden'); panel.classList.toggle('hidden'); } export function hideFuncPanel() { document.getElementById('wechat-func-panel')?.classList.add('hidden'); } function setFuncPanelPage(pageIndex) { funcPanelPage = pageIndex; const pages = document.getElementById('wechat-func-pages'); const dots = document.querySelectorAll('.wechat-func-dot'); if (pages) pages.style.transform = `translateX(-${pageIndex * 100}%)`; dots.forEach((dot, idx) => dot.classList.toggle('active', idx === pageIndex)); } function handleFuncItemClick(func) { switch (func) { case 'voice': hideFuncPanel(); showExpandVoice(); return; case 'multi': hideFuncPanel(); showExpandMulti(); return; case 'photo': hideFuncPanel(); showExpandPhoto(); return; case 'voicecall': hideFuncPanel(); startVoiceCall(); return; case 'videocall': hideFuncPanel(); startVideoCall(); return; case 'music': hideFuncPanel(); showMusicPanel(); return; default: showToast('该功能开发中...', '🧊'); } } export function initFuncPanel() { if (funcPanelInited) return; const pages = document.getElementById('wechat-func-pages'); if (!pages) return; funcPanelInited = true; let startX = 0; let currentX = 0; let isDragging = false; const handleStart = (e) => { startX = e.type === 'touchstart' ? e.touches[0].clientX : e.clientX; currentX = startX; isDragging = true; pages.style.transition = 'none'; }; const handleMove = (e) => { if (!isDragging) return; currentX = e.type === 'touchmove' ? e.touches[0].clientX : e.clientX; }; const handleEnd = () => { if (!isDragging) return; isDragging = false; pages.style.transition = 'transform 0.3s ease'; const diff = startX - currentX; if (Math.abs(diff) > 50) { if (diff > 0 && funcPanelPage < 1) setFuncPanelPage(1); else if (diff < 0 && funcPanelPage > 0) setFuncPanelPage(0); } }; pages.addEventListener('touchstart', handleStart, { passive: true }); pages.addEventListener('touchmove', handleMove, { passive: true }); pages.addEventListener('touchend', handleEnd); pages.addEventListener('mousedown', (e) => { handleStart(e); e.preventDefault(); }); pages.addEventListener('mousemove', handleMove); pages.addEventListener('mouseup', handleEnd); pages.addEventListener('mouseleave', handleEnd); document.querySelectorAll('.wechat-func-dot').forEach(dot => { dot.addEventListener('click', () => { const page = parseInt(dot.dataset.page); setFuncPanelPage(page); }); }); document.querySelectorAll('.wechat-func-item').forEach(item => { item.addEventListener('click', () => { handleFuncItemClick(item.dataset.func); }); }); // 初始化音乐面板事件 initMusicEvents(); initMusicShareListener(); }