feat: implementado Smart Router Multimodal via MiniMax e OpenRouter
This commit is contained in:
+143
-6
@@ -1191,6 +1191,7 @@ const initApp = () => {
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
let buffer = '';
|
||||
let isMediaResponse = false;
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
@@ -1211,6 +1212,7 @@ const initApp = () => {
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(dataContent);
|
||||
|
||||
if (parsed.content) {
|
||||
assistantContent += parsed.content;
|
||||
// Renderizar markdown em progresso (com o cursor no final)
|
||||
@@ -1221,6 +1223,90 @@ const initApp = () => {
|
||||
hljs.highlightElement(block);
|
||||
});
|
||||
|
||||
scrollToBottom();
|
||||
} else if (parsed.type === 'media_status') {
|
||||
isMediaResponse = true;
|
||||
botContentDiv.innerHTML = `
|
||||
<div class="media-loading">
|
||||
<div class="media-spinner"></div>
|
||||
<p class="media-loading-text">${escapeHtml(parsed.message)}</p>
|
||||
</div>
|
||||
`;
|
||||
scrollToBottom();
|
||||
} else if (parsed.type === 'image') {
|
||||
isMediaResponse = true;
|
||||
assistantContent = `
|
||||
<div class="media-message-card">
|
||||
<div class="media-img-container">
|
||||
<img src="${parsed.url}" alt="Ilustração gerada" class="generated-image" onload="scrollToBottom()">
|
||||
</div>
|
||||
<p class="media-caption">✨ "${escapeHtml(parsed.originalPrompt)}"</p>
|
||||
<div class="media-actions-row">
|
||||
<a href="${parsed.url}" download="kemily_ilustracao.png" target="_blank" class="media-download-btn">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" />
|
||||
</svg>
|
||||
<span>Salvar na Galeria</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
botContentDiv.innerHTML = assistantContent;
|
||||
scrollToBottom();
|
||||
} else if (parsed.type === 'audio') {
|
||||
isMediaResponse = true;
|
||||
const audioId = 'audio_' + Date.now();
|
||||
assistantContent = `
|
||||
<div class="media-message-card">
|
||||
<div class="custom-audio-player">
|
||||
<audio src="${parsed.url}" id="${audioId}"></audio>
|
||||
<button class="audio-play-btn" onclick="toggleCustomAudio(this)" title="Ouvir música">
|
||||
<svg class="play-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M8 5v14l11-7z"/>
|
||||
</svg>
|
||||
<svg class="pause-icon hidden" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="audio-timeline-container">
|
||||
<div class="audio-timeline">
|
||||
<div class="audio-progress"></div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="audio-time">0:00</span>
|
||||
</div>
|
||||
<p class="media-caption">🎵 "${escapeHtml(parsed.originalPrompt)}"</p>
|
||||
<div class="media-actions-row">
|
||||
<a href="${parsed.url}" download="kemily_musica.mp3" target="_blank" class="media-download-btn">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" />
|
||||
</svg>
|
||||
<span>Baixar Música</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
botContentDiv.innerHTML = assistantContent;
|
||||
scrollToBottom();
|
||||
} else if (parsed.type === 'video') {
|
||||
isMediaResponse = true;
|
||||
assistantContent = `
|
||||
<div class="media-message-card">
|
||||
<div class="media-video-container">
|
||||
<video controls src="${parsed.url}" class="generated-video" preload="metadata"></video>
|
||||
</div>
|
||||
<p class="media-caption">🎥 "${escapeHtml(parsed.originalPrompt)}"</p>
|
||||
<div class="media-actions-row">
|
||||
<a href="${parsed.url}" download="kemily_video.mp4" target="_blank" class="media-download-btn">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" />
|
||||
</svg>
|
||||
<span>Salvar Vídeo</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
botContentDiv.innerHTML = assistantContent;
|
||||
scrollToBottom();
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -1230,15 +1316,17 @@ const initApp = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Remover o cursor de digitação no final
|
||||
// Remover o cursor de digitação no final se não for mensagem de mídia
|
||||
const cursor = botContentDiv.querySelector('.typing-cursor');
|
||||
if (cursor) cursor.remove();
|
||||
|
||||
// Parse final limpo do markdown
|
||||
botContentDiv.innerHTML = marked.parse(assistantContent);
|
||||
botContentDiv.querySelectorAll('pre code').forEach((block) => {
|
||||
hljs.highlightElement(block);
|
||||
});
|
||||
// Parse final limpo do markdown (se for texto normal)
|
||||
if (!isMediaResponse) {
|
||||
botContentDiv.innerHTML = marked.parse(assistantContent);
|
||||
botContentDiv.querySelectorAll('pre code').forEach((block) => {
|
||||
hljs.highlightElement(block);
|
||||
});
|
||||
}
|
||||
|
||||
// Adicionar botões de ação (Copiar, Regenerar, Ouvir) após o streaming
|
||||
const escapedContent = escapeHtml(assistantContent).replace(/"/g, '"');
|
||||
@@ -1371,6 +1459,55 @@ const initApp = () => {
|
||||
initVoiceRecognition();
|
||||
};
|
||||
|
||||
// Player de áudio personalizado global para as mídias geradas
|
||||
window.toggleCustomAudio = (btn) => {
|
||||
const player = btn.closest('.custom-audio-player');
|
||||
const audio = player.querySelector('audio');
|
||||
const playIcon = btn.querySelector('.play-icon');
|
||||
const pauseIcon = btn.querySelector('.pause-icon');
|
||||
const progress = player.querySelector('.audio-progress');
|
||||
const timeSpan = player.querySelector('.audio-time');
|
||||
|
||||
if (audio.paused) {
|
||||
// Pausar outros players que possam estar ativos
|
||||
document.querySelectorAll('.custom-audio-player audio').forEach(a => {
|
||||
if (a !== audio && !a.paused) {
|
||||
a.pause();
|
||||
const otherBtn = a.closest('.custom-audio-player').querySelector('.audio-play-btn');
|
||||
otherBtn.querySelector('.play-icon').classList.remove('hidden');
|
||||
otherBtn.querySelector('.pause-icon').classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
audio.play();
|
||||
playIcon.classList.add('hidden');
|
||||
pauseIcon.classList.remove('hidden');
|
||||
} else {
|
||||
audio.pause();
|
||||
playIcon.classList.remove('hidden');
|
||||
pauseIcon.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Atualizar progresso em tempo real
|
||||
audio.ontimeupdate = () => {
|
||||
if (audio.duration) {
|
||||
const percent = (audio.currentTime / audio.duration) * 100;
|
||||
progress.style.width = percent + '%';
|
||||
|
||||
const mins = Math.floor(audio.currentTime / 60);
|
||||
const secs = Math.floor(audio.currentTime % 60).toString().padStart(2, '0');
|
||||
timeSpan.textContent = `${mins}:${secs}`;
|
||||
}
|
||||
};
|
||||
|
||||
audio.onended = () => {
|
||||
playIcon.classList.remove('hidden');
|
||||
pauseIcon.classList.add('hidden');
|
||||
progress.style.width = '0%';
|
||||
timeSpan.textContent = '0:00';
|
||||
};
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initApp);
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user