Enter a topic and watch two AIs debate until they reach an agreement. You can also upload a document or image to start the discussion.
Hello! Ask me something, and I will start the debate.
const TTS_MODEL_ID = 'gemini-2.5-flash-preview-tts';
const debateArea = document.getElementById('debate-area');
const questionForm = document.getElementById('question-form');
const questionInput = document.getElementById('question-input');
const submitBtn = document.getElementById('submit-btn');
const suggestBtn = document.getElementById('suggest-btn');
const loadingSpinner = document.getElementById('loading-spinner');
const consensusBox = document.getElementById('consensus-box');
const consensusText = document.getElementById('consensus-text');
const playAudioBtn = document.getElementById('play-audio-btn');
const consensusAudio = document.getElementById('consensus-audio');
const modelASelect = document.getElementById('model-a-select');
const modelBSelect = document.getElementById('model-b-select');
const fileUpload = document.getElementById('file-upload');
const fileNameSpan = document.getElementById('file-name');
let isDebating = false;
const maxTurns = 5;
let fileData = null;
let fileMimeType = null;
let debateHistory = [];
// Utility function to convert PCM data to a WAV blob
function pcmToWav(pcmData, sampleRate) {
const pcm16 = new Int16Array(pcmData);
const dataLength = pcm16.length * 2;
const buffer = new ArrayBuffer(44 + dataLength);
const view = new DataView(buffer);
// WAV header
writeString(view, 0, 'RIFF'); // RIFF identifier
view.setUint32(4, 36 + dataLength, true); // RIFF chunk length
writeString(view, 8, 'WAVE'); // WAVE identifier
writeString(view, 12, 'fmt '); // fmt chunk identifier
view.setUint32(16, 16, true); // fmt chunk length
view.setUint16(20, 1, true); // format (1 = PCM)
view.setUint16(22, 1, true); // channels (mono)
view.setUint32(24, sampleRate, true); // sample rate
view.setUint32(28, sampleRate * 2, true); // byte rate
view.setUint16(32, 2, true); // block align
view.setUint16(34, 16, true); // bits per sample
writeString(view, 36, 'data'); // data chunk identifier
view.setUint32(40, dataLength, true); // data chunk length
// Write PCM data
let offset = 44;
for (let i = 0; i < pcm16.length; i++, offset += 2) { view.setInt16(offset, pcm16[i], true); } return new Blob([view], { type: 'audio/wav' }); } function writeString(view, offset, string) { for (let i = 0; i < string.length; i++) { view.setUint8(offset + i, string.charCodeAt(i)); } } // Function to show a custom message box function showMessage(message, type = 'info') { const box = document.createElement('div'); box.className = `p-4 m-4 rounded-xl text-center shadow-md transition-all duration-300 transform ${ type === 'error' ? 'bg-red-100 text-red-800' : 'bg-gray-100 text-gray-800' } fixed bottom-8 left-1/2 -translate-x-1/2 z-50`; box.textContent = message; document.body.appendChild(box); setTimeout(() => box.remove(), 4000);
}
// Add a chat bubble to the UI
function addMessage(text, type) {
const bubble = document.createElement('div');
bubble.className = `bubble mt-4 ${type}`;
let formattedText = text.replace(/(\*+)(.*?)\1/g, '$2');
formattedText = formattedText.replace(/\n/g, '
');
bubble.innerHTML = formattedText;
debateArea.appendChild(bubble);
debateArea.scrollTop = debateArea.scrollHeight;
}
// Exponential backoff for retries
async function fetchWithRetry(url, options, retries = 3, delay = 1000) {
for (let i = 0; i < retries; i++) { try { const response = await fetch(url, options); if (response.status === 429) { console.log(`Rate limit exceeded. Retrying in ${delay / 1000}s...`); await new Promise(res => setTimeout(res, delay));
delay *= 2;
continue;
}
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response;
} catch (error) {
if (i === retries - 1) throw error;
console.log(`Fetch failed. Retrying in ${delay / 1000}s...`);
await new Promise(res => setTimeout(res, delay));
delay *= 2;
}
}
}
// Function to generate content from the AI model
async function generateContent(modelId, parts, systemInstruction) {
const apiKey = "";
const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${modelId}:generateContent?key=${apiKey}`;
let payload = {
contents: [{ parts: parts }],
systemInstruction: { parts: [{ text: systemInstruction }] },
generationConfig: {
temperature: 0.7,
maxOutputTokens: 500
}
};
// Add Google Search grounding for models that support it
if (modelId === 'gemini-2.5-flash-preview-05-20' || modelId === 'gemini-1.5-pro-latest') {
payload.tools = [{ "google_search": {} }];
}
const response = await fetchWithRetry(apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const result = await response.json();
return result.candidates?.[0]?.content?.parts?.[0]?.text || '';
}
// Function to generate TTS audio
async function generateTts(text) {
const apiKey = "";
const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${TTS_MODEL_ID}:generateContent?key=${apiKey}`;
const payload = {
contents: [{ parts: [{ text: text }] }],
generationConfig: {
responseModalities: ["AUDIO"],
speechConfig: {
voiceConfig: {
prebuiltVoiceConfig: { voiceName: "Rasalgethi" }
}
}
},
model: TTS_MODEL_ID
};
const response = await fetchWithRetry(apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const result = await response.json();
const part = result?.candidates?.[0]?.content?.parts?.[0];
if (part && part.inlineData && part.inlineData.data) {
const base64Data = part.inlineData.data;
const mimeType = part.inlineData.mimeType;
const sampleRate = parseInt(mimeType.match(/rate=(\d+)/)[1], 10);
const pcmData = Uint8Array.from(atob(base64Data), c => c.charCodeAt(0));
const wavBlob = pcmToWav(pcmData, sampleRate);
const audioUrl = URL.createObjectURL(wavBlob);
consensusAudio.src = audioUrl;
consensusAudio.hidden = false;
} else {
showMessage('Error: Could not generate audio.', 'error');
}
}
// Function to check for consensus
function checkForConsensus(text) {
const lowerText = text.toLowerCase();
return lowerText.includes("consensus reached") || lowerText.includes("we agree") || lowerText.includes("final conclusion");
}
// Main debate loop
async function startDebate(initialPrompt) {
isDebating = true;
submitBtn.disabled = true;
questionInput.disabled = true;
loadingSpinner.classList.remove('hidden');
consensusBox.classList.add('hidden');
consensusAudio.pause();
consensusAudio.src = '';
debateHistory = [];
const modelA = modelASelect.value;
const modelB = modelBSelect.value;
const systemInstruction = `You are two distinct AI personalities debating a user's question.
* **Model A** is analytical, logical, and focuses on facts and data. Your model ID is ${modelA}.
* **Model B** is creative, philosophical, and focuses on abstract concepts and possibilities. Your model ID is ${modelB}.
You will respond in a structured format, taking turns debating the topic.
Each response should be a new turn in the conversation, starting with the model's name.
Continue the debate until a final consensus is reached.
The consensus must be a single, coherent paragraph that synthesizes both viewpoints.
If you reach a consensus, the last turn must begin with "Final Consensus:".
Example format:
Model A: [argument]
Model B: [counter-argument]
Model A: [rebuttal]
Final Consensus: [agreed upon conclusion]
If you cannot agree after 5 turns, provide a summary of the remaining points of disagreement.`;
// Create the initial parts array for the API request
let initialParts = [{ text: initialPrompt }];
if (fileData && fileMimeType) {
initialParts.push({ inlineData: { mimeType: fileMimeType, data: fileData } });
}
addMessage(initialPrompt, 'user-prompt');
let consensusReached = false;
let lastResponse = '';
for (let turn = 0; turn < maxTurns && !consensusReached; turn++) { let debatePrompt = `Turn ${turn + 1}:`; let partsToSend = debateHistory.map(item => ({ text: item }));
// Initial prompt with file data
if (turn === 0) {
partsToSend = initialParts;
partsToSend.push({text: `Based on the provided ${fileMimeType ? fileMimeType.split('/')[0] : 'context'} (if any) and the following question: "${initialPrompt}", please start the debate.
${debatePrompt}
Model A:`});
} else {
partsToSend.push({text: `${debatePrompt}\nModel A:`});
}
try {
const response = await generateContent(modelA, partsToSend, systemInstruction);
if (!response) {
showMessage("AI response failed. Please try again.", "error");
break;
}
const parts = response.split(/Model B:|Final Consensus:/).map(part => part.trim()).filter(Boolean);
if (parts.length > 0) {
const modelAResponse = parts[0];
addMessage(`Model A: ${modelAResponse}`, 'model-a');
debateHistory.push(`Model A: ${modelAResponse}\n`);
if (parts[1]) {
const modelBResponse = parts[1];
addMessage(`Model B: ${modelBResponse}`, 'model-b');
debateHistory.push(`Model B: ${modelBResponse}\n`);
}
}
lastResponse = response;
consensusReached = checkForConsensus(lastResponse);
} catch (error) {
console.error('Debate error:', error);
showMessage("An error occurred during the debate. Please check the console for details.", "error");
break;
}
}
loadingSpinner.classList.add('hidden');
isDebating = false;
submitBtn.disabled = false;
questionInput.disabled = false;
if (consensusReached) {
const consensus = lastResponse.split('Final Consensus:')[1].trim();
consensusText.textContent = consensus;
consensusBox.classList.remove('hidden');
await generateTts(consensus);
} else {
showMessage("The models could not reach a consensus.", "info");
}
}
async function suggestTopics() {
if (isDebating) return;
loadingSpinner.classList.remove('hidden');
submitBtn.disabled = true;
suggestBtn.disabled = true;
const apiKey = "";
const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent?key=${apiKey}`;
const payload = {
contents: [{
parts: [{ text: "Suggest three concise and interesting debate topics that can be answered in a few turns." }]
}],
generationConfig: {
temperature: 0.9,
}
};
try {
const response = await fetchWithRetry(apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const result = await response.json();
const topics = result.candidates?.[0]?.content?.parts?.[0]?.text;
if (topics) {
const topicsHtml = topics.split('\n').map(topic => `
${topic}
`).join('');
const messageHtml = `
${topicsHtml}
`;
document.body.insertAdjacentHTML('beforeend', messageHtml);
setTimeout(() => {
document.body.lastChild.remove();
}, 10000); // Hide after 10 seconds
}
} catch (error) {
showMessage("Failed to suggest topics.", "error");
console.error(error);
} finally {
loadingSpinner.classList.add('hidden');
submitBtn.disabled = false;
suggestBtn.disabled = false;
}
}
// Play audio on button click
playAudioBtn.addEventListener('click', () => {
if (consensusAudio.src) {
consensusAudio.play();
} else {
showMessage("Audio not available.", "error");
}
});
fileUpload.addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file) {
fileNameSpan.textContent = "Choose a file...";
fileData = null;
fileMimeType = null;
return;
}
const reader = new FileReader();
fileNameSpan.textContent = file.name;
if (file.type.startsWith('image/')) {
reader.onload = (event) => {
const base64String = event.target.result.split(',')[1];
fileData = base64String;
fileMimeType = file.type;
showMessage("Image uploaded successfully.", "info");
};
reader.readAsDataURL(file);
} else if (file.type === 'text/plain' || file.name.endsWith('.md')) {
reader.onload = (event) => {
fileData = event.target.result;
fileMimeType = file.type;
showMessage("Text file uploaded successfully.", "info");
};
reader.readAsText(file);
} else {
showMessage("Unsupported file type. Please upload a .txt, .md, .jpg, or .png file.", "error");
e.target.value = '';
}
});
questionForm.addEventListener('submit', (e) => {
e.preventDefault();
if (isDebating) return;
const question = questionInput.value.trim();
if (question) {
debateArea.innerHTML = '';
startDebate(question);
questionInput.value = '';
} else {
showMessage("Please enter a question to begin the debate.", "error");
}
});
suggestBtn.addEventListener('click', suggestTopics);