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 {
|
||||
|
||||
@@ -1898,3 +1898,208 @@ code {
|
||||
color: var(--text-muted);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
SMART ROUTER MEDIA GENERATION (Imagem, Áudio e Vídeo)
|
||||
========================================================================== */
|
||||
.media-message-card {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: 16px;
|
||||
padding: 16px;
|
||||
margin: 12px 0;
|
||||
max-width: 500px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
.media-img-container, .media-video-container {
|
||||
width: 100%;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
aspect-ratio: 1/1;
|
||||
}
|
||||
|
||||
.media-video-container {
|
||||
aspect-ratio: 16/9;
|
||||
}
|
||||
|
||||
.generated-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 12px;
|
||||
transition: transform var(--transition-normal);
|
||||
}
|
||||
|
||||
.generated-image:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.generated-video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.media-caption {
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
font-style: italic;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.media-actions-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.media-download-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
background: linear-gradient(135deg, var(--brand-green) 0%, #1e7e4a 100%);
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
padding: 10px 16px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: opacity var(--transition-fast), transform var(--transition-fast);
|
||||
flex: 1;
|
||||
box-shadow: 0 2px 8px rgba(30, 190, 110, 0.2);
|
||||
}
|
||||
|
||||
.media-download-btn:hover {
|
||||
opacity: 0.9;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.media-download-btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.media-download-btn svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
/* Player de Áudio Customizado */
|
||||
.custom-audio-player {
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.audio-play-btn {
|
||||
background: var(--brand-green);
|
||||
border: none;
|
||||
color: #ffffff;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
transition: transform var(--transition-fast);
|
||||
}
|
||||
|
||||
.audio-play-btn:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.audio-play-btn svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.audio-play-btn svg.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.audio-timeline-container {
|
||||
flex: 1;
|
||||
height: 6px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 3px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.audio-timeline {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.audio-progress {
|
||||
height: 100%;
|
||||
background: var(--brand-green);
|
||||
border-radius: 3px;
|
||||
width: 0%;
|
||||
transition: width 0.1s linear;
|
||||
}
|
||||
|
||||
.audio-time {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
font-family: monospace;
|
||||
min-width: 32px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Loading de Mídia */
|
||||
.media-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
gap: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.media-spinner {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 3px solid rgba(30, 190, 110, 0.1);
|
||||
border-radius: 50%;
|
||||
border-top-color: var(--brand-green);
|
||||
animation: spin 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.media-loading-text {
|
||||
font-size: 13.5px;
|
||||
color: var(--text-secondary);
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(8px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user