import { store } from '../store.js';
import { formatCompactTokenCount } from '../api.js';
import { supportsAudio, supportsImage, supportsVideo } from '../capabilities.js';
import { icon } from '../icons.js';
const VIDEO_SIZE_LIMIT_MB = 50;
export class InputBar {
constructor() {
this.el = null;
this._pendingImage = null; // { dataUrl, file }
this._pendingVideo = null; // { dataUrl, file }
this._pendingAudio = null; // { file }
this._currentModel = '';
this._sending = false;
}
render() {
const el = document.createElement('div');
el.className = 'border-t border-[var(--c-bd)] bg-[var(--c-bg)]';
el.innerHTML = this._template();
this.el = el;
this._bindEvents();
return this.el;
}
_template() {
const imageSupported = supportsImage(this._currentModel, store.getModelCapabilities());
const videoSupported = supportsVideo(this._currentModel, store.getModelCapabilities());
const audioSupported = supportsAudio(this._currentModel, store.getModelCapabilities());
const mediaSupported = imageSupported || videoSupported;
const mediaAccept = [imageSupported && 'image/*', videoSupported && 'video/*'].filter(Boolean).join(',') || 'image/*,video/*';
const mediaTitle = mediaSupported
? `Attach ${[imageSupported && 'image', videoSupported && 'video'].filter(Boolean).join(' / ')} (or paste)`
: 'Vision not enabled for this model';
return `
`;
}
_bindEvents() {
const textarea = this.el.querySelector('#message-input');
const sendBtn = this.el.querySelector('#send-btn');
const mediaBtn = this.el.querySelector('#media-upload-btn');
const audioBtn = this.el.querySelector('#audio-upload-btn');
const fileInput = this.el.querySelector('#media-file-input');
const audioInput = this.el.querySelector('#audio-file-input');
const removeImageBtn = this.el.querySelector('#remove-image-btn');
const removeVideoBtn = this.el.querySelector('#remove-video-btn');
const removeAudioBtn = this.el.querySelector('#remove-audio-btn');
// Auto-resize textarea
textarea.addEventListener('input', () => this._autoResize(textarea));
// Enter sends; Shift+Enter inserts newline (default behaviour)
textarea.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
this._submit();
}
});
sendBtn.addEventListener('click', () => this._submit());
// Media (image/video) upload
mediaBtn?.addEventListener('click', () => {
if (!mediaBtn.disabled) fileInput.click();
});
audioBtn?.addEventListener('click', () => {
if (!audioBtn.disabled) audioInput.click();
});
fileInput.addEventListener('change', (e) => {
const file = e.target.files?.[0];
if (file) this._handleMediaFile(file);
fileInput.value = '';
});
audioInput.addEventListener('change', (e) => {
const file = e.target.files?.[0];
if (file) this._handleAudioFile(file);
audioInput.value = '';
});
removeImageBtn.addEventListener('click', () => this._clearImage());
removeVideoBtn?.addEventListener('click', () => this._clearVideo());
removeAudioBtn?.addEventListener('click', () => this._clearAudio());
// Paste media support
textarea.addEventListener('paste', (e) => this._handlePaste(e));
// Model change
document.addEventListener('model:changed', (e) => {
this.setModel(e.detail.model);
});
document.addEventListener('caps:changed', () => {
this._updateAttachmentButtons();
});
}
_autoResize(textarea) {
textarea.style.height = 'auto';
const scrollH = textarea.scrollHeight;
const maxH = 144; // max-height ~6 rows
textarea.style.height = Math.min(scrollH, maxH) + 'px';
}
async _handleImageFile(file) {
if (!file.type.startsWith('image/')) return;
if (this._pendingAudio) this._clearAudio();
if (this._pendingVideo) this._clearVideo();
const dataUrl = await this._fileToDataUrl(file);
this._pendingImage = { dataUrl, file };
this._showImagePreview(dataUrl);
}
async _handleVideoFile(file) {
if (!file.type.startsWith('video/')) return;
const sizeMB = file.size / (1024 * 1024);
if (sizeMB > VIDEO_SIZE_LIMIT_MB) {
alert(`Video file is too large (${sizeMB.toFixed(1)} MB). Please keep it under ${VIDEO_SIZE_LIMIT_MB} MB.`);
return;
}
if (this._pendingAudio) this._clearAudio();
if (this._pendingImage) this._clearImage();
const dataUrl = await this._fileToDataUrl(file);
this._pendingVideo = { dataUrl, file };
this._showVideoPreview(dataUrl);
}
_handleMediaFile(file) {
if (file.type.startsWith('image/')) {
this._handleImageFile(file);
} else if (file.type.startsWith('video/')) {
this._handleVideoFile(file);
}
}
_handleAudioFile(file) {
if (!String(file?.type || '').startsWith('audio/') && !/\.(mp3|wav|m4a|aac|ogg|flac|webm)$/i.test(file?.name || '')) return;
if (this._pendingImage) this._clearImage();
this._pendingAudio = { file };
this._showAudioPreview();
}
_fileToDataUrl(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => resolve(e.target.result);
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
_showImagePreview(dataUrl) {
const previewArea = this.el.querySelector('#image-preview-area');
const thumb = this.el.querySelector('#image-preview-thumb');
thumb.src = dataUrl;
previewArea.classList.remove('hidden');
}
_clearImage() {
this._pendingImage = null;
const previewArea = this.el.querySelector('#image-preview-area');
const thumb = this.el.querySelector('#image-preview-thumb');
thumb.src = '';
previewArea.classList.add('hidden');
}
_showVideoPreview(dataUrl) {
const previewArea = this.el.querySelector('#video-preview-area');
const player = this.el.querySelector('#video-preview-player');
player.src = dataUrl;
previewArea.classList.remove('hidden');
}
_clearVideo() {
this._pendingVideo = null;
const previewArea = this.el.querySelector('#video-preview-area');
const player = this.el.querySelector('#video-preview-player');
if (player) { player.pause(); player.src = ''; }
previewArea?.classList.add('hidden');
}
_showAudioPreview() {
const previewArea = this.el.querySelector('#audio-preview-area');
const nameEl = this.el.querySelector('#audio-file-name');
if (!previewArea || !this._pendingAudio) return;
nameEl.textContent = this._pendingAudio.file?.name || 'audio';
previewArea.classList.remove('hidden');
}
_clearAudio() {
this._pendingAudio = null;
const previewArea = this.el.querySelector('#audio-preview-area');
const nameEl = this.el.querySelector('#audio-file-name');
if (nameEl) nameEl.textContent = '';
previewArea?.classList.add('hidden');
}
async _handlePaste(e) {
const items = e.clipboardData?.items;
if (!items) return;
for (const item of items) {
if (item.type.startsWith('image/')) {
const imageSupported = supportsImage(this._currentModel, store.getModelCapabilities());
if (!imageSupported) return;
e.preventDefault();
const file = item.getAsFile();
if (file) await this._handleImageFile(file);
break;
}
if (item.type.startsWith('video/')) {
const videoSupported = supportsVideo(this._currentModel, store.getModelCapabilities());
if (!videoSupported) return;
e.preventDefault();
const file = item.getAsFile();
if (file) await this._handleVideoFile(file);
break;
}
if (item.type.startsWith('audio/')) {
const audioSupported = supportsAudio(this._currentModel, store.getModelCapabilities());
if (!audioSupported) return;
e.preventDefault();
const file = item.getAsFile();
if (file) this._handleAudioFile(file);
break;
}
}
}
_submit() {
if (this._sending) return;
const textarea = this.el.querySelector('#message-input');
const text = textarea.value.trim();
if (!text && !this._pendingImage && !this._pendingVideo && !this._pendingAudio) return;
const image = this._pendingImage;
const video = this._pendingVideo;
const audio = this._pendingAudio;
this._clearImage();
this._clearVideo();
this._clearAudio();
textarea.value = '';
this._autoResize(textarea);
document.dispatchEvent(new CustomEvent('inputbar:send', {
detail: { text, image, video, audio },
}));
}
setSending(sending) {
this._sending = sending;
const sendBtn = this.el.querySelector('#send-btn');
const textarea = this.el.querySelector('#message-input');
if (sendBtn) {
sendBtn.disabled = sending;
sendBtn.className = sending
? 'flex items-center justify-center w-8 h-8 rounded-xl text-[var(--c-tx3)] cursor-not-allowed transition-all'
: 'flex items-center justify-center w-8 h-8 rounded-xl transition-all hover:bg-[var(--c-hi)]';
sendBtn.innerHTML = icon(sending ? 'sendMuted' : 'send');
}
if (textarea) textarea.disabled = sending;
}
setContextInfo(currentTokens, maxTokens, warnTokens = maxTokens) {
const el = this.el.querySelector('#context-info');
if (!el) return;
el.textContent = `ctx ${formatCompactTokenCount(currentTokens)}/${formatCompactTokenCount(maxTokens)}`;
el.title = `Estimated context ${Math.round(currentTokens)} / ${Math.round(maxTokens)} tokens`;
const warning = currentTokens >= warnTokens;
el.className = warning
? 'ml-auto text-right text-[11px] font-mono tabular-nums text-amber-400 transition-colors select-none'
: 'ml-auto text-right text-[11px] font-mono tabular-nums text-[var(--c-tx3)] transition-colors select-none';
}
setKvCount(current, max) {
this.setContextInfo(current, max, Math.floor(max * 0.8));
}
setModel(modelId) {
this._currentModel = modelId;
this._updateAttachmentButtons();
}
_updateAttachmentButtons() {
const mediaBtn = this.el.querySelector('#media-upload-btn');
const audioBtn = this.el.querySelector('#audio-upload-btn');
const fileInput = this.el.querySelector('#media-file-input');
if (!mediaBtn || !audioBtn) return;
const imageSupported = supportsImage(this._currentModel, store.getModelCapabilities());
const videoSupported = supportsVideo(this._currentModel, store.getModelCapabilities());
const audioSupported = supportsAudio(this._currentModel, store.getModelCapabilities());
const mediaSupported = imageSupported || videoSupported;
if (mediaSupported) {
mediaBtn.disabled = false;
mediaBtn.className = 'flex items-center justify-center w-8 h-8 rounded-lg transition-all text-[var(--c-tx3)] hover:text-[var(--c-tx2)] hover:bg-[var(--c-hi)]';
const labels = [imageSupported && 'image', videoSupported && 'video'].filter(Boolean).join(' / ');
mediaBtn.title = `Attach ${labels} (or paste)`;
if (fileInput) {
fileInput.accept = [imageSupported && 'image/*', videoSupported && 'video/*'].filter(Boolean).join(',');
}
} else {
mediaBtn.disabled = true;
mediaBtn.className = 'flex items-center justify-center w-8 h-8 rounded-lg transition-all text-[var(--c-tx3)] opacity-40 cursor-not-allowed';
mediaBtn.title = 'Vision not enabled for this model';
if (this._pendingImage) this._clearImage();
if (this._pendingVideo) this._clearVideo();
}
if (audioSupported) {
audioBtn.disabled = false;
audioBtn.className = 'flex items-center justify-center w-8 h-8 rounded-lg transition-all text-[var(--c-tx3)] hover:text-[var(--c-tx2)] hover:bg-[var(--c-hi)]';
audioBtn.title = 'Attach audio for transcription or translation';
} else {
audioBtn.disabled = true;
audioBtn.className = 'flex items-center justify-center w-8 h-8 rounded-lg transition-all text-[var(--c-tx3)] opacity-40 cursor-not-allowed';
audioBtn.title = 'Audio not enabled for this model';
if (this._pendingAudio) this._clearAudio();
}
}
focus() {
this.el?.querySelector('#message-input')?.focus();
}
}