Files
my-ia/frontend/index.html
2026-03-31 13:10:44 +02:00

1446 lines
52 KiB
HTML
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Code Assistant</title>
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<link
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Space+Grotesk:wght@300;400;500;600;700&display=swap"
rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/prism.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-python.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-javascript.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-typescript.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-jsx.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-tsx.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-bash.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-css.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-markup.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-json.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-sql.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-java.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-c.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-cpp.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-go.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-rust.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-yaml.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-markdown.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-tomorrow.css" rel="stylesheet" />
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--bg-primary: #0a0a0f;
--bg-secondary: #13131a;
--bg-tertiary: #1a1a24;
--bg-input: #1f1f2e;
--accent-primary: #7c3aed;
--accent-secondary: #a78bfa;
--accent-glow: #8b5cf6;
--text-primary: #e5e7eb;
--text-secondary: #9ca3af;
--text-muted: #6b7280;
--border: #2d2d3d;
--success: #10b981;
--error: #ef4444;
--warning: #f59e0b;
--code-bg: #1e1e2e;
}
body {
font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
overflow: hidden;
height: 100vh;
}
/* Grain texture overlay */
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 400 400' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)' opacity='0.05'/%3E%3C/svg%3E");
pointer-events: none;
z-index: 1000;
mix-blend-mode: overlay;
}
/* Animated gradient background */
body::after {
content: '';
position: fixed;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle at 20% 50%,
rgba(124, 58, 237, 0.08) 0%,
transparent 50%),
radial-gradient(circle at 80% 80%,
rgba(139, 92, 246, 0.06) 0%,
transparent 50%);
animation: gradientShift 15s ease-in-out infinite;
z-index: 0;
}
@keyframes gradientShift {
0%,
100% {
transform: translate(0, 0) rotate(0deg);
}
33% {
transform: translate(5%, -5%) rotate(120deg);
}
66% {
transform: translate(-5%, 5%) rotate(240deg);
}
}
.app-container {
position: relative;
z-index: 1;
display: flex;
height: 100vh;
}
/* Sidebar */
.sidebar {
width: 280px;
background: var(--bg-secondary);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
backdrop-filter: blur(20px);
}
.sidebar-header {
padding: 24px 20px;
border-bottom: 1px solid var(--border);
}
.logo {
display: flex;
align-items: center;
gap: 12px;
font-weight: 700;
font-size: 20px;
letter-spacing: -0.5px;
}
.logo-icon {
width: 32px;
height: 32px;
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
box-shadow: 0 4px 12px rgba(124, 58, 237, 0.3);
}
.new-chat-btn {
margin-top: 16px;
width: 100%;
padding: 12px 16px;
background: var(--bg-tertiary);
border: 1px solid var(--border);
color: var(--text-primary);
border-radius: 10px;
cursor: pointer;
font-family: inherit;
font-size: 14px;
font-weight: 500;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 8px;
}
.new-chat-btn:hover {
background: var(--bg-input);
border-color: var(--accent-primary);
transform: translateY(-1px);
}
.conversations {
flex: 1;
overflow-y: auto;
padding: 12px;
}
.conversation-item {
padding: 12px;
margin-bottom: 6px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
font-size: 14px;
color: var(--text-secondary);
}
.conversation-item:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.conversation-item.active {
background: var(--bg-input);
color: var(--accent-secondary);
}
.model-selector {
padding: 16px 20px;
border-top: 1px solid var(--border);
}
.model-selector select {
width: 100%;
padding: 10px 12px;
background: var(--bg-tertiary);
border: 1px solid var(--border);
color: var(--text-primary);
border-radius: 8px;
font-family: inherit;
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
}
.model-selector select:hover {
border-color: var(--accent-primary);
}
/* Main content */
.main-content {
flex: 1;
display: flex;
flex-direction: column;
position: relative;
}
.chat-header {
padding: 20px 24px;
border-bottom: 1px solid var(--border);
background: var(--bg-secondary);
backdrop-filter: blur(20px);
display: flex;
justify-content: space-between;
align-items: center;
}
.chat-title {
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
}
.header-actions {
display: flex;
gap: 8px;
}
.header-btn {
padding: 8px 16px;
background: var(--bg-tertiary);
border: 1px solid var(--border);
color: var(--text-secondary);
border-radius: 8px;
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
font-family: inherit;
}
.header-btn:hover {
background: var(--bg-input);
color: var(--text-primary);
border-color: var(--accent-primary);
}
.header-btn.active {
background: var(--accent-primary);
color: white;
border-color: var(--accent-primary);
}
/* Messages area */
.messages-container {
flex: 1;
overflow-y: auto;
padding: 24px;
scroll-behavior: smooth;
}
.message {
max-width: 800px;
margin: 0 auto 24px;
animation: messageSlideIn 0.3s ease-out;
}
@keyframes messageSlideIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.message-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.message-avatar {
width: 32px;
height: 32px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 14px;
}
.message.user .message-avatar {
background: linear-gradient(135deg, #3b82f6, #2563eb);
}
.message.assistant .message-avatar {
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
}
.message-role {
font-weight: 600;
font-size: 14px;
}
.message-content {
padding-left: 44px;
line-height: 1.7;
font-size: 15px;
}
.message-content pre {
background: var(--code-bg);
border: 1px solid var(--border);
border-radius: 10px;
padding: 0;
overflow: hidden;
margin: 16px 0;
font-family: 'JetBrains Mono', monospace;
font-size: 13px;
position: relative;
}
.message-content code {
font-family: 'JetBrains Mono', monospace;
font-size: 13px;
}
.message-content pre code {
display: block;
padding: 16px;
overflow-x: auto;
line-height: 1.6;
}
.message-content p {
margin-bottom: 12px;
}
.message-content ul,
.message-content ol {
margin-left: 24px;
margin-bottom: 12px;
}
/* Inline code */
.message-content code:not(pre code) {
background: rgba(124, 58, 237, 0.15);
color: var(--accent-secondary);
padding: 2px 6px;
border-radius: 4px;
font-size: 0.9em;
}
.code-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 16px;
background: rgba(255, 255, 255, 0.03);
border-bottom: 1px solid var(--border);
}
.code-language {
font-size: 12px;
color: var(--text-muted);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.code-actions {
display: flex;
gap: 8px;
}
.code-btn {
padding: 5px 12px;
background: var(--bg-tertiary);
border: 1px solid var(--border);
color: var(--text-secondary);
border-radius: 6px;
cursor: pointer;
font-size: 11px;
font-family: inherit;
transition: all 0.2s;
font-weight: 500;
}
.code-btn:hover {
background: var(--bg-input);
color: var(--text-primary);
border-color: var(--accent-primary);
transform: translateY(-1px);
}
.code-btn:active {
transform: translateY(0);
}
/* Amélioration Prism.js colors */
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: #6b7280;
}
.token.punctuation {
color: #d1d5db;
}
.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.constant,
.token.symbol,
.token.deleted {
color: #f87171;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: #34d399;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string {
color: #fbbf24;
}
.token.atrule,
.token.attr-value,
.token.keyword {
color: #a78bfa;
}
.token.function,
.token.class-name {
color: #60a5fa;
}
.token.regex,
.token.important,
.token.variable {
color: #fb923c;
}
/* Input area */
.input-container {
padding: 24px;
border-top: 1px solid var(--border);
background: var(--bg-secondary);
backdrop-filter: blur(20px);
}
.input-wrapper {
max-width: 800px;
margin: 0 auto;
position: relative;
}
.input-box {
width: 100%;
min-height: 56px;
max-height: 200px;
padding: 16px 60px 16px 16px;
background: var(--bg-input);
border: 2px solid var(--border);
color: var(--text-primary);
border-radius: 14px;
font-family: inherit;
font-size: 15px;
resize: none;
outline: none;
transition: all 0.2s;
line-height: 1.5;
}
.input-box:focus {
border-color: var(--accent-primary);
box-shadow: 0 0 0 4px rgba(124, 58, 237, 0.1);
}
.send-button {
position: absolute;
right: 12px;
bottom: 12px;
width: 36px;
height: 36px;
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
border: none;
border-radius: 8px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
box-shadow: 0 4px 12px rgba(124, 58, 237, 0.3);
}
.send-button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(124, 58, 237, 0.4);
}
.send-button:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
/* Code panel */
.code-panel {
width: 0;
background: var(--bg-secondary);
border-left: 1px solid var(--border);
display: flex;
flex-direction: column;
transition: width 0.3s ease;
overflow: hidden;
}
.code-panel.open {
width: 500px;
}
.code-panel-header {
padding: 20px 24px;
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
}
.code-panel-title {
font-weight: 600;
font-size: 16px;
}
.close-panel-btn {
background: none;
border: none;
color: var(--text-secondary);
cursor: pointer;
font-size: 20px;
padding: 4px;
transition: color 0.2s;
}
.close-panel-btn:hover {
color: var(--text-primary);
}
.code-editor-container {
flex: 1;
display: flex;
flex-direction: column;
padding: 16px;
}
.code-editor {
flex: 1;
width: 100%;
background: var(--code-bg);
border: 1px solid var(--border);
color: var(--text-primary);
padding: 16px;
font-family: 'JetBrains Mono', monospace;
font-size: 13px;
border-radius: 10px;
resize: none;
outline: none;
margin-bottom: 12px;
line-height: 1.6;
tab-size: 4;
}
.code-editor:focus {
border-color: var(--accent-primary);
box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.1);
}
.code-output {
max-height: 300px;
overflow-y: auto;
background: var(--code-bg);
border: 1px solid var(--border);
border-radius: 10px;
padding: 16px;
font-family: 'JetBrains Mono', monospace;
font-size: 13px;
margin-top: 12px;
line-height: 1.6;
}
.output-success {
color: var(--success);
border-color: var(--success);
}
.output-error {
color: var(--error);
border-color: var(--error);
}
.execute-btn {
padding: 12px 20px;
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
border: none;
color: white;
border-radius: 8px;
cursor: pointer;
font-family: inherit;
font-weight: 600;
font-size: 14px;
transition: all 0.2s;
box-shadow: 0 4px 12px rgba(124, 58, 237, 0.3);
}
.execute-btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(124, 58, 237, 0.4);
}
.execute-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.execute-btn:active:not(:disabled) {
transform: translateY(0);
}
/* Loading animation */
.typing-indicator {
display: flex;
gap: 4px;
padding-left: 44px;
margin-top: 8px;
}
.typing-dot {
width: 8px;
height: 8px;
background: var(--accent-primary);
border-radius: 50%;
animation: typingBounce 1.4s infinite ease-in-out;
}
.typing-dot:nth-child(1) {
animation-delay: -0.32s;
}
.typing-dot:nth-child(2) {
animation-delay: -0.16s;
}
@keyframes typingBounce {
0%,
80%,
100% {
transform: scale(0);
opacity: 0.5;
}
40% {
transform: scale(1);
opacity: 1;
}
}
/* Scrollbar */
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-track {
background: var(--bg-secondary);
}
::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 5px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--accent-primary);
}
/* Status indicator */
.status-bar {
position: fixed;
bottom: 16px;
right: 16px;
padding: 8px 16px;
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: 8px;
font-size: 12px;
display: flex;
align-items: center;
gap: 8px;
z-index: 100;
}
.status-dot {
width: 8px;
height: 8px;
background: var(--success);
border-radius: 50%;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect, useRef } = React;
// Configuration
const API_URL = 'http://localhost:9001';
const WS_URL = 'ws://localhost:9001/ws/chat';
function App() {
const [messages, setMessages] = useState([]);
const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [models, setModels] = useState([]);
const [selectedModel, setSelectedModel] = useState('qwen2.5-coder:7b');
const [codePanelOpen, setCodePanelOpen] = useState(false);
const [code, setCode] = useState('');
const [codeOutput, setCodeOutput] = useState('');
const [isConnected, setIsConnected] = useState(false);
const [backendError, setBackendError] = useState(false);
const [errorMessage, setErrorMessage] = useState('');
const messagesEndRef = useRef(null);
const wsRef = useRef(null);
// Gestion d'erreur globale React
useEffect(() => {
const handleError = (error) => {
console.error('🔴 Erreur React:', error);
setErrorMessage(error.message || 'Erreur inconnue');
};
window.addEventListener('error', handleError);
return () => window.removeEventListener('error', handleError);
}, []);
useEffect(() => {
console.log('🚀 Initialisation de l\'application...');
// Charger les modèles disponibles
fetch(`${API_URL}/models`)
.then(res => {
console.log('📡 Réponse /models:', res.status);
if (!res.ok) throw new Error('Backend non accessible');
return res.json();
})
.then(data => {
console.log('✅ Modèles chargés:', data.models?.length || 0);
if (data.models && data.models.length > 0) {
setModels(data.models);
setBackendError(false);
} else {
console.warn('⚠️ Aucun modèle trouvé, utilisation des défauts');
setModels([
{ name: 'qwen2.5-coder:7b', size: 0 },
{ name: 'code-expert', size: 0 }
]);
}
})
.catch(err => {
console.error('❌ Erreur chargement modèles:', err);
setBackendError(true);
setModels([
{ name: 'qwen2.5-coder:7b', size: 0 },
{ name: 'code-expert', size: 0 }
]);
});
// Connexion WebSocket
connectWebSocket();
return () => {
console.log('🛑 Nettoyage de l\'application');
if (wsRef.current) {
wsRef.current.close();
}
};
}, []);
useEffect(() => {
scrollToBottom();
}, [messages]);
const connectWebSocket = () => {
console.log('🔌 Tentative de connexion WebSocket à:', WS_URL);
try {
const ws = new WebSocket(WS_URL);
ws.onopen = () => {
console.log('✅ WebSocket connecté');
setIsConnected(true);
};
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
console.log('📨 Message WebSocket:', data.type);
if (data.type === 'stream') {
// Mise à jour progressive du dernier message
setMessages(prev => {
const newMessages = [...prev];
const lastMessage = newMessages[newMessages.length - 1];
if (lastMessage && lastMessage.role === 'assistant') {
lastMessage.content += data.content;
} else {
newMessages.push({
role: 'assistant',
content: data.content,
timestamp: new Date().toISOString()
});
}
return newMessages;
});
} else if (data.type === 'done') {
console.log('✅ Réponse complète reçue');
setIsLoading(false);
} else if (data.type === 'error') {
console.error('❌ Erreur WebSocket:', data.content);
setIsLoading(false);
setErrorMessage(data.content);
}
} catch (error) {
console.error('❌ Erreur parsing message WebSocket:', error);
}
};
ws.onerror = (error) => {
console.error('❌ Erreur WebSocket:', error);
setIsConnected(false);
setBackendError(true);
};
ws.onclose = () => {
console.log('🔌 WebSocket déconnecté, reconnexion dans 3s...');
setIsConnected(false);
// Reconnexion après 3 secondes
setTimeout(() => {
console.log('🔄 Tentative de reconnexion...');
connectWebSocket();
}, 3000);
};
wsRef.current = ws;
} catch (error) {
console.error('❌ Erreur création WebSocket:', error);
setIsConnected(false);
setBackendError(true);
}
};
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
const sendMessage = async () => {
if (!input.trim() || !wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) return;
const userMessage = {
role: 'user',
content: input,
timestamp: new Date().toISOString()
};
setMessages(prev => [...prev, userMessage]);
setInput('');
setIsLoading(true);
// Envoyer via WebSocket
wsRef.current.send(JSON.stringify({
message: userMessage.content,
model: selectedModel
}));
};
const handleKeyPress = (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
};
const executeCode = async () => {
try {
const response = await fetch(`${API_URL}/execute`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
code: code,
language: 'python'
})
});
const result = await response.json();
if (result.success) {
setCodeOutput(result.stdout || 'Exécution réussie (pas de sortie)');
} else {
setCodeOutput(`Erreur:\n${result.stderr}`);
}
} catch (error) {
setCodeOutput(`Erreur: ${error.message}`);
}
};
const copyCode = (codeText) => {
navigator.clipboard.writeText(codeText);
};
const renderMessage = (msg, index) => {
const isUser = msg.role === 'user';
try {
// Rendu markdown pour l'assistant
let content = msg.content;
if (!isUser) {
// Échapper le HTML d'abord pour éviter les injections
const escapeHtml = (text) => {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
};
// Extraire et stocker les blocs de code avant échappement
const codeBlocks = [];
let codeIndex = 0;
// Remplacer temporairement les blocs de code par des placeholders
content = content.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => {
const placeholder = `__CODE_BLOCK_${codeIndex}__`;
codeBlocks.push({ lang: lang || 'text', code });
codeIndex++;
return placeholder;
});
// Échapper le reste du contenu
content = escapeHtml(content);
// Conversion markdown basique sur le contenu échappé
content = content
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.+?)\*/g, '<em>$1</em>')
.replace(/`(.+?)`/g, '<code>$1</code>')
.replace(/\n/g, '<br>');
// Réinsérer les blocs de code avec coloration
codeBlocks.forEach((block, idx) => {
const { lang, code } = block;
const codeId = `code-${index}-${idx}`;
// Échapper le code pour l'affichage
const escapedCode = code
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
const codeHtml = `<pre><div class="code-header">
<span class="code-language">${lang}</span>
<div class="code-actions">
<button class="code-btn" onclick="window.copyCode('${codeId}')">📋 Copier</button>
<button class="code-btn" onclick="window.insertToEditor('${codeId}')">▶ Tester</button>
</div>
</div><code id="${codeId}" class="language-${lang}">${escapedCode}</code></pre>`;
content = content.replace(`__CODE_BLOCK_${idx}__`, codeHtml);
});
}
return (
<div key={index} className={`message ${msg.role}`}>
<div className="message-header">
<div className="message-avatar">
{isUser ? 'U' : 'AI'}
</div>
<div className="message-role">
{isUser ? 'Vous' : 'Assistant'}
</div>
</div>
<div
className="message-content"
dangerouslySetInnerHTML={{ __html: content }}
/>
</div>
);
} catch (error) {
console.error('Erreur rendu message:', error);
return (
<div key={index} className={`message ${msg.role}`}>
<div className="message-header">
<div className="message-avatar"></div>
<div className="message-role">Erreur</div>
</div>
<div className="message-content" style={{ color: 'var(--error)' }}>
Erreur d'affichage du message. Voir la console (F12).
<br />
Message brut : {msg.content.substring(0, 100)}...
</div>
</div>
);
}
};
// Fonctions globales pour les boutons
window.copyCode = (codeId) => {
try {
const codeElement = document.getElementById(codeId);
if (!codeElement) {
console.error('Élément code introuvable:', codeId);
return;
}
const code = codeElement.textContent;
navigator.clipboard.writeText(code).then(() => {
// Feedback visuel
const btn = event.target;
const originalText = btn.textContent;
btn.textContent = ' Copié !';
btn.style.background = 'var(--success)';
btn.style.color = 'white';
setTimeout(() => {
btn.textContent = originalText;
btn.style.background = '';
btn.style.color = '';
}, 2000);
}).catch(err => {
console.error('Erreur copie:', err);
alert('Erreur lors de la copie');
});
} catch (error) {
console.error('Erreur copyCode:', error);
}
};
window.insertToEditor = (codeId) => {
try {
const codeElement = document.getElementById(codeId);
if (!codeElement) {
console.error('Élément code introuvable:', codeId);
return;
}
setCode(codeElement.textContent);
setCodePanelOpen(true);
} catch (error) {
console.error('Erreur insertToEditor:', error);
}
};
// Appliquer Prism.js après le rendu avec protection complète
useEffect(() => {
// Ne pas lancer Prism pendant le streaming
if (isLoading) return;
const highlightCode = () => {
if (!window.Prism || typeof window.Prism.highlightElement !== 'function') {
console.warn(' Prism.js non disponible');
return;
}
// Laisser React finir le rendu
requestAnimationFrame(() => {
try {
const codeBlocks = document.querySelectorAll(
'pre code[class^="language-"]'
);
if (codeBlocks.length === 0) {
return;
}
console.log(`🎨 Prism: ${codeBlocks.length} bloc(s) à colorier`);
codeBlocks.forEach((block) => {
try {
// Empêche Prism de relancer un parsing sur un bloc déjà traité
if (block.dataset.prismDone) return;
window.Prism.highlightElement(block);
block.dataset.prismDone = 'true';
} catch (err) {
console.warn(' Prism ignoré pour un bloc:', err);
}
});
} catch (err) {
console.error(' Erreur Prism globale:', err);
}
});
};
highlightCode();
}, [isLoading]); // 🔥 UNIQUEMENT quand le stream est fini
return (
<div className="app-container">
{/* Sidebar */}
<div className="sidebar">
<div className="sidebar-header">
<div className="logo">
<div className="logo-icon">AI</div>
Code Assistant
</div>
<button className="new-chat-btn" onClick={() => setMessages([])}>
✨ Nouvelle conversation
</button>
</div>
<div className="conversations">
{/* Historique des conversations */}
</div>
<div className="model-selector">
<select
value={selectedModel}
onChange={(e) => setSelectedModel(e.target.value)}
>
{models && models.length > 0 ? (
models.map(model => (
<option key={model.name} value={model.name}>
{model.name}
</option>
))
) : (
<option value="qwen2.5-coder:7b">qwen2.5-coder:7b</option>
)}
</select>
</div>
</div>
{/* Main content */}
<div className="main-content">
{/* Bandeau d'erreur si problème */}
{errorMessage && (
<div style={{
background: 'var(--error)',
color: 'white',
padding: '12px 24px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}>
<span> Erreur: {errorMessage}</span>
<button
onClick={() => setErrorMessage('')}
style={{
background: 'rgba(255,255,255,0.2)',
border: 'none',
color: 'white',
padding: '4px 12px',
borderRadius: '4px',
cursor: 'pointer'
}}
>
</button>
</div>
)}
<div className="chat-header">
<div className="chat-title">Conversation</div>
<div className="header-actions">
<button
className={`header-btn ${codePanelOpen ? 'active' : ''}`}
onClick={() => setCodePanelOpen(!codePanelOpen)}
>
💻 Code
</button>
</div>
</div>
<div className="messages-container">
{backendError && (
<div style={{
textAlign: 'center',
marginTop: '100px',
padding: '40px',
background: 'var(--bg-tertiary)',
borderRadius: '12px',
maxWidth: '600px',
margin: '100px auto'
}}>
<h2 style={{ fontSize: '24px', marginBottom: '16px', color: 'var(--error)' }}>
Backend non accessible
</h2>
<p style={{ marginBottom: '12px', color: 'var(--text-secondary)' }}>
Le serveur FastAPI ne répond pas sur http://localhost:9001
</p>
<p style={{ marginBottom: '20px', fontSize: '14px', color: 'var(--text-muted)' }}>
Vérifiez que le backend est démarré avec :
</p>
<pre style={{
background: 'var(--code-bg)',
padding: '12px',
borderRadius: '8px',
fontSize: '13px',
textAlign: 'left',
marginBottom: '16px'
}}>
cd backend{'\n'}
. venv/bin/activate{'\n'}
python main.py
</pre>
<p style={{ fontSize: '13px', color: 'var(--text-muted)' }}>
💡 Ouvrez la console (F12) pour voir les logs détaillés
</p>
</div>
)}
{!backendError && messages.length === 0 && (
<div style={{
textAlign: 'center',
marginTop: '100px',
color: 'var(--text-muted)'
}}>
<h2 style={{ fontSize: '32px', marginBottom: '16px' }}>
Bonjour ! 👋
</h2>
<p>Comment puis-je vous aider avec votre code aujourd'hui ?</p>
</div>
)}
{messages.map((msg, idx) => renderMessage(msg, idx))}
{isLoading && (
<div className="message assistant">
<div className="message-header">
<div className="message-avatar">AI</div>
<div className="message-role">Assistant</div>
</div>
<div className="typing-indicator">
<div className="typing-dot"></div>
<div className="typing-dot"></div>
<div className="typing-dot"></div>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
<div className="input-container">
<div className="input-wrapper">
<textarea
className="input-box"
placeholder="Posez une question ou demandez de l'aide avec du code..."
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={handleKeyPress}
disabled={!isConnected}
/>
<button
className="send-button"
onClick={sendMessage}
disabled={!input.trim() || !isConnected}
>
</button>
</div>
</div>
</div>
{/* Code panel */}
<div className={`code-panel ${codePanelOpen ? 'open' : ''}`}>
<div className="code-panel-header">
<div className="code-panel-title">💻 Éditeur de code</div>
<button
className="close-panel-btn"
onClick={() => setCodePanelOpen(false)}
>
×
</button>
</div>
<div className="code-editor-container">
<div style={{
display: 'flex',
gap: '8px',
marginBottom: '12px',
alignItems: 'center'
}}>
<select
style={{
flex: 1,
padding: '8px 12px',
background: 'var(--bg-tertiary)',
border: '1px solid var(--border)',
color: 'var(--text-primary)',
borderRadius: '8px',
fontSize: '13px',
fontFamily: 'inherit'
}}
onChange={(e) => {
// Changer le langage si besoin
console.log('Language:', e.target.value);
}}
>
<option value="python">🐍 Python</option>
<option value="javascript">📜 JavaScript</option>
<option value="bash">⚡ Bash</option>
</select>
<button
className="code-btn"
onClick={() => setCode('')}
style={{ padding: '8px 12px' }}
>
🗑️ Effacer
</button>
</div>
<textarea
className="code-editor"
placeholder="# Écrivez votre code Python ici...
# Ou collez du code depuis le chat avec le bouton 'Tester'
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n-1) + fibonacci(n-2)
print(fibonacci(10))"
value={code}
onChange={(e) => setCode(e.target.value)}
spellCheck="false"
/>
<button
className="execute-btn"
onClick={executeCode}
disabled={!code.trim()}
>
▶ Exécuter le code
</button>
{codeOutput && (
<div className={`code-output ${codeOutput.includes('Erreur') ? 'output-error' : 'output-success'}`}>
<div style={{
fontWeight: 600,
marginBottom: '8px',
fontSize: '12px',
textTransform: 'uppercase',
letterSpacing: '0.5px'
}}>
{codeOutput.includes('Erreur') ? '❌ Sortie (Erreur)' : '✅ Sortie'}
</div>
<pre style={{ margin: 0 }}>{codeOutput}</pre>
</div>
)}
</div>
</div>
{/* Status bar */}
<div className="status-bar">
<div className="status-dot" style={{
background: isConnected ? 'var(--success)' : 'var(--error)'
}} />
{isConnected ? 'Connecté' : 'Déconnecté'}
</div>
</div>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
</script>
</body>
</html>