import React, { useState, useEffect, useRef } from 'react';// Fonction pour encoder les données audio PCM en WAV
const pcmToWav = (pcmData, sampleRate) => {
const pcm16 = pcmData; // Already Int16Array
const numChannels = 1;
const bytesPerSample = 2; // 16-bit PCMconst header = new ArrayBuffer(44);
const view = new DataView(header);// RIFF identifier
writeString(view, 0, 'RIFF');
// File length (placeholder)
view.setUint32(4, 36 + pcm16.length * bytesPerSample, true);
// RIFF type
writeString(view, 8, 'WAVE');
// format chunk identifier
writeString(view, 12, 'fmt ');
// format chunk length
view.setUint32(16, 16, true);
// sample format (raw PCM)
view.setUint16(20, 1, true);
// channel count
view.setUint16(22, numChannels, true);
// sample rate
view.setUint32(24, sampleRate, true);
// byte rate (sampleRate * numChannels * bytesPerSample)
view.setUint32(28, sampleRate * numChannels * bytesPerSample, true);
// block align (numChannels * bytesPerSample)
view.setUint16(32, numChannels * bytesPerSample, true);
// bits per sample
view.setUint16(34, 16, true);
// data chunk identifier
writeString(view, 36, 'data');
// data chunk length (placeholder)
view.setUint32(40, pcm16.length * bytesPerSample, true);const blob = new Blob([header, pcm16], { type: 'audio/wav' });
return blob;
};const writeString = (view, offset, string) => {
for (let i = 0; i < string.length; i++) {
view.setUint8(offset + i, string.charCodeAt(i));
}
};const base64ToArrayBuffer = (base64) => {
const binaryString = window.atob(base64);
const len = binaryString.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes.buffer;
};const App = () => {
const [messages, setMessages] = useState([]);
const [input, setInput] = useState('');
const [loading, setLoading] = useState(false);
const messagesEndRef = useRef(null);// Initial context about the congress for the AI agent
const congressContext = `
Vous êtes un assistant IA utile et informatif pour le 69e congrès franco-allemand de la FAFA pour l'Europe.
Voici les informations clés sur le congrès :**Nom de l'événement** : 69e Congrès Franco-Allemand FAFA/VDFG
**Organisateurs** : La Fédération des Acteurs Franco-Allemands pour l’Europe (FAFA pour l’Europe) et la Vereinigung Deutsch-Französischer Gesellschaften für Europa e.V. (VDFG für Europa e.V.).
**Dates** : Du 17 au 19 octobre 2025.
**Lieu** : CCI Espace d’Affaires, Centre des Salorges - Nantes.
**Thème Principal** : "LA RELATION FRANCE - ALLEMAGNE : TOUS ACTEURS".
**Slogans** : "Die deutsch-französischen Beziehungen: Gemeinsam. Für alle." et "Coopérer pour s’unir, s’unir pour construire".
**Patronage** : Sous le Haut Patronage de l’Ambassadeur de la République Fédérale d’Allemagne en France.**Participants attendus** : Environ 300 participants.
**Organisations et Partenaires** : Plus de 30 mobilisés.
**Tables Rondes et Ateliers** : Plus de 10 prévus.**Personnalités Annoncées (intervenants)** :
* S.E.M Stephan Steinlein (Ambassadeur d'Allemagne en France)
* Pascal Thibaut (Correspondant Radio France Internationale - Berlin)
* Frédéric Petit (Député pour les Français de l'étranger)
* Karin Broermann (Consule honoraire d'Allemagne à Nantes)
* Et d'autres personnalités du monde politique, diplomatique, économique, institutionnel, académique, éducatif et associatif.**Événements Clés et Programme Détaillé** :
* **Espace Forum** : Du 17 au 19 octobre 2025, pour rencontrer 30 structures clés. Inauguration le 17 octobre à 10h par Mme Karin Broermann.
* **Tables Rondes et Ateliers** : Se dérouleront du 17 au 19 octobre, avec des ateliers spécifiques le samedi 18 octobre. Ils aborderont les grands enjeux franco-allemands et européens.
* **Networking** : Espaces dédiés le samedi 18 octobre.
* **Exposition "Le traité d’Aix-la-Chapelle : de quoi parle-t-on ?"** (par la FAFA) : Présente du 17 au 19 octobre.
* **Exposition "La vie quotidienne des prisonniers de guerre français en Allemagne pendant la Seconde Guerre mondiale"** (par l'ADAPG).
* **Remise du Prix Elsie-Kühn-Leitz** : Le vendredi 17 octobre 2025.
* **Soirée de Gala** : Le samedi 18 octobre au Salon Mauduit à Nantes, célébrant le 60e anniversaire du jumelage Nantes-Sarrebruck et le partenariat Nantes Métropole – Hambourg Métropole.
* **Jeu Franco-Allemand "Napiturillon"** : Proposé du 18 au 19 octobre.**Programme Quotidien (Résumé)** :
* **Vendredi 17 octobre** :
* 10h : Inauguration de l'Espace Forum par Mme Karin Broermann, Consule honoraire de la République Fédérale d’Allemagne à Nantes.
* 11h30-12h30 : Atelier dédié aux étudiants : "Changeons de regard sur l’industrie - Vers un partenariat industriel franco-allemand", animé par M. Olivier Pasquier (CAFANA).
* 13h45 : Inauguration du Congrès par S.E.M. l'Ambassadeur d’Allemagne en France Stephan Steinlein (sollicité) et des personnalités politiques.
* Remise du prix Elsie-Kühn-Leitz.
* Journée ouverte au public (forum, atelier étudiants, séance inaugurale, tables rondes et remise du prix Elsie-Kühn-Leitz).
* **Samedi 18 octobre** :
* 4 tables rondes, 9 ateliers/networking/FAQ.
* Soirée de Gala.
* Journée réservée aux congressistes.
* **Dimanche 19 octobre** :
* Conférence plénière finale : "Comment la société civile peut stimuler l’action politique : La relation France-Allemagne pour l'Europe : Tous Acteurs".
* Propositions pour l’amélioration de la visibilité du franco-allemand.
* Journée ouverte au public.**Thèmes des Tables Rondes** :
* Éducation : Les pôles de synergie franco-allemands - Retour d'expériences.
* Étudiants : Défis interculturels franco-allemands dans les enjeux de la transition écologique.
* Affaires Européennes : La vision de l’Europe en France, en Allemagne, en Pologne. Évolution du Triangle de Weimar.
* Économie et souveraineté : Les industries et les projets franco-allemands pour la souveraineté énergétique européenne.
* Tech : L’IA en Europe : Défis - Succès – Échecs – Éthique.
* Politique : 5 ans après sa mise en œuvre : Un premier bilan du Traité d’Aix-la-Chapelle.**Thèmes des Ateliers (Samedi 18 octobre)** :
* Comment transmettre l’Histoire aux jeunes générations.
* Projets internationaux au service de la Jeunesse.
* Comment améliorer la visibilité du Franco-Allemand? Tous acteurs !
* Présentation des acteurs des mobilités France-Allemagne.
* Jumelages horizon 2035 : quelles perspectives pour quels rôles ?
* L’Intelligence artificielle au service des associations.
* FAQ : Les critères des subventions pour des projets franco-allemands.
* À la découverte de parcours de formation du CAP au Master, avec mobilité européenne.**Accès au Congrès** :
* Vendredi 17 et dimanche 19 octobre : Ouvert au public.
* Samedi 18 octobre : Réservé aux congressistes.
* Lycéens, étudiants et leurs professeurs : Accès gratuit (hors restauration) sur inscription préalable.**Inscription** :
* Les participants peuvent s'inscrire via le lien : billetweb.fr/congres-fafa-pour-leurope-2025-a-nantes.**Parrainage du Congrès** :
* Partenaire du congrès : À partir de 10 000 €.
* Parrain du congrès : De 5 000 € à 9 900 €.
* Accompagnateur du congrès : De 2 000 à 4 900 €.
* Options spécifiques :
* Participation à l’Espace Forum (stand) : 1 500,00 €.
* Participation au cocktail d’ouverture : 1 800,00 €.
* Participation à la restauration des étudiants : 1 700,00 €.
* Participation au cocktail de clôture : 1 600,00 €.
* Soutien à la soirée de gala : 2 000,00 €.
* Soutien libre.
* Modalités de paiement : Acompte de 50% à la commande, solde au plus tard le 31 août 2025. Paiement par virement (FR76 - 1027 – 8026 – 8800 – 0488 – 0840 – 586 - BIC : CMCIFR2A) ou par chèque à l’ordre de FAFA pour l’Europe.
* Un reçu fiscal sera délivré pour bénéficier d’une réduction d’impôt.**Contacts pour plus d'informations** :
* Jean-Michel PRATS, Président : 06 12 52 59 69 – jm.prats@fafapourleurope.fr
* Françoise ARNOLD, Vice-présidente : 06 07 24 27 07 – f.arnold@fafapourleurope.frRépondez aux questions sur le congrès en utilisant les informations ci-dessus. Si la question est hors sujet ou si l'information n'est pas dans le contexte fourni, dites que vous ne pouvez pas aider sur ce point et proposez de diriger l'utilisateur vers les contacts officiels.
`;// Scroll to the latest message
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);const handleSendMessage = async () => {
if (input.trim() === '') return;const newUserMessage = { sender: 'user', text: input };
setMessages((prevMessages) => [...prevMessages, newUserMessage]);
setInput('');
setLoading(true);try {
// Build the chat history for the API, including the initial context
let chatHistory = [{ role: 'user', parts: [{ text: congressContext }] }];
messages.forEach(msg => {
chatHistory.push({ role: msg.sender === 'user' ? 'user' : 'model', parts: [{ text: msg.text }] });
});
chatHistory.push({ role: 'user', parts: [{ text: input }] });const payload = {
contents: chatHistory,
generationConfig: {
temperature: 0.7, // Adjust for creativity vs. focus
maxOutputTokens: 500,
},
};const apiKey = ''; // Leave this as-is; Canvas provides it at runtime
const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-05-20:generateContent?key=${apiKey}`;let response;
let result;
let retries = 0;
const maxRetries = 5;
const initialDelay = 1000; // 1 secondwhile (retries < maxRetries) {
try {
response = await fetch(apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});if (response.status === 429) { // Too Many Requests
const delay = initialDelay * Math.pow(2, retries);
retries++;
await new Promise(res => setTimeout(res, delay));
continue; // Retry the request
}if (!response.ok) {
const errorData = await response.json();
throw new Error(`Erreur HTTP: ${response.status} - ${errorData.error.message}`);
}result = await response.json();
break; // Success, exit retry loop
} catch (error) {
console.error('Erreur lors de la récupération de la réponse de Gemini:', error);
// For network errors or other non-429 errors, consider a few retries as well
if (retries < maxRetries - 1) { // Don't retry on the last attempt
const delay = initialDelay * Math.pow(2, retries);
retries++;
await new Promise(res => setTimeout(res, delay));
} else {
throw error; // Re-throw if max retries reached or unrecoverable error
}
}
}if (result && result.candidates && result.candidates.length > 0 &&
result.candidates[0].content && result.candidates[0].content.parts &&
result.candidates[0].content.parts.length > 0) {
const botResponseText = result.candidates[0].content.parts[0].text;
setMessages((prevMessages) => [...prevMessages, { sender: 'bot', text: botResponseText }]);
} else {
setMessages((prevMessages) => [...prevMessages, { sender: 'bot', text: "Désolé, je n'ai pas pu générer de réponse. Veuillez réessayer." }]);
}} catch (error) {
console.error('Erreur lors de la communication avec l\'API Gemini:', error);
setMessages((prevMessages) => [...prevMessages, { sender: 'bot', text: "Il y a eu une erreur de connexion. Veuillez vérifier votre connexion ou réessayer plus tard." }]);
} finally {
setLoading(false);
}
};const handleSpeech = async (text) => {
try {
const payload = {
contents: [{
parts: [{ text: text }]
}],
generationConfig: {
responseModalities: ["AUDIO"],
speechConfig: {
voiceConfig: {
prebuiltVoiceConfig: { voiceName: "Zephyr" } // You can try other voices like Kore, Puck, etc.
}
}
},
model: "gemini-2.5-flash-preview-tts"
};
const apiKey = "";
const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-tts:generateContent?key=${apiKey}`;const response = await fetch(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];
const audioData = part?.inlineData?.data;
const mimeType = part?.inlineData?.mimeType;if (audioData && mimeType && mimeType.startsWith("audio/")) {
const sampleRateMatch = mimeType.match(/rate=(\d+)/);
const sampleRate = sampleRateMatch ? parseInt(sampleRateMatch[1], 10) : 16000; // Default if not found
const pcmData = base64ToArrayBuffer(audioData);
const pcm16 = new Int16Array(pcmData);
const wavBlob = pcmToWav(pcm16, sampleRate);
const audioUrl = URL.createObjectURL(wavBlob);
const audio = new Audio(audioUrl);
audio.play();
audio.onended = () => {
URL.revokeObjectURL(audioUrl); // Clean up the URL after playing
};
} else {
console.error("No audio data or invalid mime type received.");
}
} catch (error) {
console.error("Error generating speech:", error);
}
};return (
);
};export default App;
FAFA Chat Assistant
Posez vos questions sur le 69e congrès
{messages.length === 0 && (
))}
{loading && (
)}
Bienvenue ! Comment puis-je vous aider concernant le 69e congrès de la FAFA pour l'Europe ?
)}
{messages.map((message, index) => ({message.text}
{message.sender === 'bot' && ( )}Typing...
setInput(e.target.value)}
onKeyPress={(e) => {
if (e.key === 'Enter') {
handleSendMessage();
}
}}
disabled={loading}
/>