Abmacode12 commited on
Commit
6065311
·
verified ·
1 Parent(s): 564fd57

+ = ouvrir un sélecteur de fichiers

Browse files

prise = bouton “connecter des applications”

micro = dictée vocale (Web Speech API)

flèche = envoyer

Placeholder : “Envoyer un message à Espace Codage”

Au-dessus : panneau “Voir l’ordinateur de Espace Codage” qui défile (logs)

"use client";

import React, { useEffect, useMemo, useRef, useState } from "react";

type Props = {
onSend: (text: string, files?: File[]) => Promise<void> | void;
onConnectApps?: () => void;
};

type SpeechState = "idle" | "listening" | "unsupported" | "denied";

export default function ChatFooterBar({ onSend, onConnectApps }: Props) {
const [text, setText] = useState("");
const [files, setFiles] = useState<File[]>([]);
const [speech, setSpeech] = useState<SpeechState>("idle");
const [logs, setLogs] = useState<string[]>([
"Espace Codage • Démarrage…",
"Chargement des services…",
"Prêt ✅"
]);

const fileInputRef = useRef<HTMLInputElement | null>(null);
const logsRef = useRef<HTMLDivElement | null>(null);

// ---- “Ordinateur” qui défile (simulation de flux)
useEffect(() => {
const t = setInterval(() => {
setLogs((prev) => {
const next = [...prev, `Console • ${new Date().toLocaleTimeString()} • activité…`];
return next.length > 200 ? next.slice(-200) : next;
});
}, 1400);
return () => clearInterval(t);
}, []);

useEffect(() => {
if (!logsRef.current) return;
logsRef.current.scrollTop = logsRef.current.scrollHeight;
}, [logs]);

// ---- Dictée vocale (Web Speech API)
const SpeechRecognitionImpl = useMemo(() => {
if (typeof window === "undefined") return null;
const w = window as any;
return w.SpeechRecognition || w.webkitSpeechRecognition || null;
}, []);

const startDictation = async () => {
if (!SpeechRecognitionImpl) {
setSpeech("unsupported");
return;
}

// Certains navigateurs demandent une permission micro. On démarre simplement la reco.
const recog = new SpeechRecognitionImpl();
recog.lang = "fr-FR";
recog.interimResults = true;
recog.continuous = true;

setSpeech("listening");

let finalText = "";

recog.onresult = (event: any) => {
let interim = "";
for (let i = event.resultIndex; i < event.results.length; i++) {
const transcript = event.results[i][0].transcript;
if (event.results[i].isFinal) finalText += transcript;
else interim += transcript;
}
setText((prev) => {
const base = prev.trim().length ? prev.trim() + " " : "";
const merged = (base + (finalText + interim)).replace(/\s+/g, " ").trim();
return merged;
});
};

recog.onerror = (e: any) => {
// not-allowed / service-not-allowed / audio-capture, etc.
if (String(e?.error).includes("not-allowed") || String(e?.error).includes("service-not-allowed")) {
setSpeech("denied");
} else {
setSpeech("idle");
}
try { recog.stop(); } catch {}
};

recog.onend = () => {
setSpeech((s) => (s === "listening" ? "idle" : s));
};

try {
recog.start();
} catch {
setSpeech("idle");
}
};

const stopDictation = () => {
// On ne garde pas l’instance globale ici. UX simple : re-cliquer micro stoppe via rechargement de state.
// Si tu veux un stop “dur”, on peut stocker l’instance dans un ref.
setSpeech("idle");
};

const handlePickFiles = () => fileInputRef.current?.click();

const handleFilesChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
const list = Array.from(e.target.files ?? []);
setFiles(list);
};

const handleSend = async () => {
const msg = text.trim();
if (!msg && files.length === 0) return;

await onSend(msg, files);

setText("");
setFiles([]);
if (fileInputRef.current) fileInputRef.current.value = "";
};

return (
<div className="w-full">
{/* --- Petit écran au-dessus (ordinateur) --- */}
<div className="mb-2 rounded-xl border border-white/10 bg-white/5 overflow-hidden">
<div className="flex items-center justify-between px-3 py-2 border-b border-white/10">
<div className="text-xs font-semibold text-white/80">Voir l’ordinateur de Espace Codage</div>
<div className="text-[11px] text-white/50">
{speech === "listening" ? "Micro actif…" : "En ligne"}
</div>
</div>

<div
ref={logsRef}
className="h-28 overflow-auto px-3 py-2 text-[11px] leading-5 text-white/70"
>
{logs.map((l, idx) => (
<div key={idx} className="whitespace-pre-wrap">
{l}
</div>
))}
</div>
</div>

{/* --- Barre du bas (comme la capture) --- */}
<div className="flex items-center gap-2 rounded-xl border border-white/10 bg-white/5 px-2 py-2">
{/* + Fichiers */}
<button
type="button"
onClick={handlePickFiles}
className="h-9 w-9 grid place-items-center rounded-lg hover:bg-white/10 border border-white/10"
aria-label="Ajouter des fichiers"
title="Ajouter des fichiers"
>
<PlusIcon />
</button>

{/* Connecter des apps (prise) */}
<button
type="button"
onClick={onConnectApps}
className="h-9 w-9 grid place-items-center rounded-lg hover:bg-white/10 border border-white/10"
aria-label="Connecter des applications"
title="Connecter des applications"
>
<PlugIcon />
</button>

{/* Champ texte */}
<div className="flex-1">
<input
value={text}
onChange={(e) => setText(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
void handleSend();
}
}}
className="w-full bg-transparent outline-none text-sm px-2 py-2 text-white placeholder:text-white/50"
placeholder="Envoyer un message à Espace Codage"
/>
{!!files.length && (
<div className="px-2 pt-1 text-[11px] text-white/60">
{files.length} fichier(s) ajouté(s)
</div>
)}
</div>

{/* Micro dictée */}
<button
type="button"
onClick={() => (speech === "listening" ? stopDictation() : startDictation())}
className={[
"h-9 w-9 grid place-items-center rounded-lg border border-white/10",
speech === "listening" ? "bg-white text-black" : "hover:bg-white/10"
].join(" ")}
aria-label="Saisie vocale"
title={
speech === "unsupported"
? "Dictée vocale non supportée"
: speech === "denied"
? "Permission micro refusée"
: "Saisie vocale"
}
>
<MicIcon active={speech === "listening"} />
</button>

{/* Envoyer */}
<button
type="button"
onClick={() => void handleSend()}
className="h-9 w-9 grid place-items-center rounded-lg bg-white/10 hover:bg-white/15 border border-white/10"
aria-label="Envoyer"
title="Envoyer"
>
<SendIcon />
</button>

<input
ref={fileInputRef}
type="file"
multiple
className="hidden"
onChange={handleFilesChanged}
/>
</div>
</div>
);
}

/* --- Icônes inline (pas de dépendances) --- */

function PlusIcon() {
return (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
<path d="M12 5v14M5 12h14" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
</svg>
);
}

function PlugIcon() {
return (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
<path
d="M9 7v5M15 7v5M7 12h10M10 12v4a4 4 0 0 0 4 4h1"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
<path
d="M6 7h12"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
</svg>
);
}

function MicIcon({ active }: { active: boolean }) {
return (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
<path
d="M12 14a3 3 0 0 0 3-3V7a3 3 0 0 0-6 0v4a3 3 0 0 0 3 3Z"
stroke="currentColor"
strokeWidth="2"
/>
<path
d="M19 11a7 7 0 0 1-14 0"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
<path
d="M12 18v3"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
<path
d="M8 21h8"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
{active ? (
<path
d="M4 4l16 16"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
opacity="0.25"
/>
) : null}
</svg>
);
}

function SendIcon() {
return (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
<path
d="M5 12h12"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
<path
d="M13 6l6 6-6 6"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}

2) Exemple d’intégration dans ton chat : components/ChatPanel.tsx

Ici tu branches ton système existant (onSend).
Le code ci-dessous montre comment récupérer texte + fichiers.

"use client";

import React, { useState } from "react";
import ChatFooterBar from "./ChatFooterBar";

export default function ChatPanel() {
const [messages, setMessages] = useState<{ role: "user" | "assistant"; content: string }[]>([
{ role: "assistant", content: "Envoyez-moi un message." }
]);

return (
<div className="h-full flex flex-col">
<div className="flex-1 overflow-auto p-3 space-y-2">
{messages.map((m, i) => (
<div

Files changed (3) hide show
  1. components/chat.js +44 -15
  2. script.js +88 -6
  3. style.css +21 -1
components/chat.js CHANGED
@@ -79,7 +79,7 @@ display: block;
79
  .plus-button {
80
  margin-right: 0;
81
  }
82
- .message-input {
83
  flex-grow: 1;
84
  padding: 0.75rem 1rem;
85
  border: 1px solid #e2e8f0;
@@ -116,7 +116,29 @@ display: block;
116
  width: 20px;
117
  height: 20px;
118
  }
119
- </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  <div class="chat-header">
121
  <span>45.pace/index.html</span>
122
  <span class="connection-status">*Connexion active : DeepSite API*</span>
@@ -149,20 +171,27 @@ display: block;
149
  <div class="conversation">
150
  <div class="message">"Je suis ROSALINDA, votre IA dédiée, intégrée à DeepSite. Je travaille avec vous pour générer des vidéos, images et thèmes professionnels. Parlez-moi, joignez vos fichiers, je crée."</div>
151
  </div>
 
 
 
 
 
 
152
  <div class="input-area">
153
- <button class="action-button plus-button" title="Ajouter des fichiers">
154
- <i data-feather="plus"></i>
155
- </button>
156
- <button class="action-button" title="Connecter des applications">
157
- <i data-feather="plug"></i>
158
- </button>
159
- <input type="text" class="message-input" placeholder="Envoyer un message à Espace Codage">
160
- <button class="action-button mic-active active" title="Saisie vocale">
161
- <i data-feather="mic"></i>
162
- </button>
163
- <button class="send-button" title="Envoyer">
164
- <i data-feather="arrow-right"></i>
165
- </button>
 
166
  </div>
167
  `;
168
  }
 
79
  .plus-button {
80
  margin-right: 0;
81
  }
82
+ .message-input {
83
  flex-grow: 1;
84
  padding: 0.75rem 1rem;
85
  border: 1px solid #e2e8f0;
 
116
  width: 20px;
117
  height: 20px;
118
  }
119
+ .file-input {
120
+ display: none;
121
+ }
122
+ .logs-container {
123
+ max-height: 120px;
124
+ overflow-y: auto;
125
+ background: #f8fafc;
126
+ border: 1px solid #e2e8f0;
127
+ border-radius: 0.5rem;
128
+ padding: 0.5rem;
129
+ margin-bottom: 0.5rem;
130
+ font-family: monospace;
131
+ font-size: 0.8rem;
132
+ }
133
+ .log-line {
134
+ margin-bottom: 0.25rem;
135
+ color: #64748b;
136
+ }
137
+ .mic-active.listening {
138
+ background: #3b82f6;
139
+ color: white;
140
+ }
141
+ </style>
142
  <div class="chat-header">
143
  <span>45.pace/index.html</span>
144
  <span class="connection-status">*Connexion active : DeepSite API*</span>
 
171
  <div class="conversation">
172
  <div class="message">"Je suis ROSALINDA, votre IA dédiée, intégrée à DeepSite. Je travaille avec vous pour générer des vidéos, images et thèmes professionnels. Parlez-moi, joignez vos fichiers, je crée."</div>
173
  </div>
174
+ <div class="logs-container">
175
+ <div class="log-line">Espace Codage • Démarrage...</div>
176
+ <div class="log-line">Chargement des services...</div>
177
+ <div class="log-line">Prêt ✅</div>
178
+ </div>
179
+
180
  <div class="input-area">
181
+ <button class="action-button plus-button" title="Ajouter des fichiers" id="fileButton">
182
+ <i data-feather="plus"></i>
183
+ </button>
184
+ <button class="action-button" title="Connecter des applications" id="connectButton">
185
+ <i data-feather="plug"></i>
186
+ </button>
187
+ <input type="text" class="message-input" placeholder="Envoyer un message à Espace Codage" id="messageInput">
188
+ <button class="action-button mic-active" title="Saisie vocale" id="micButton">
189
+ <i data-feather="mic"></i>
190
+ </button>
191
+ <button class="send-button" title="Envoyer" id="sendButton">
192
+ <i data-feather="arrow-right"></i>
193
+ </button>
194
+ <input type="file" class="file-input" id="fileInput" multiple>
195
  </div>
196
  `;
197
  }
script.js CHANGED
@@ -9,11 +9,93 @@ document.addEventListener('DOMContentLoaded', function() {
9
  });
10
  });
11
 
12
- // Microphone toggle
13
- const micButtons = document.querySelectorAll('.mic-active');
14
- micButtons.forEach(button => {
15
- button.addEventListener('click', function() {
16
- this.classList.toggle('active');
 
 
 
 
 
 
 
 
 
17
  });
18
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  });
 
9
  });
10
  });
11
 
12
+ // Chat functionality
13
+ const chat = document.querySelector('custom-chat');
14
+ if (chat) {
15
+ const shadowRoot = chat.shadowRoot;
16
+
17
+ // File picker
18
+ const fileButton = shadowRoot.getElementById('fileButton');
19
+ const fileInput = shadowRoot.getElementById('fileInput');
20
+ fileButton.addEventListener('click', () => fileInput.click());
21
+
22
+ // Connect apps
23
+ const connectButton = shadowRoot.getElementById('connectButton');
24
+ connectButton.addEventListener('click', () => {
25
+ alert('Connecter des applications - Cette fonctionnalité sera implémentée bientôt');
26
  });
27
+
28
+ // Microphone
29
+ const micButton = shadowRoot.getElementById('micButton');
30
+ micButton.addEventListener('click', () => {
31
+ if ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window) {
32
+ micButton.classList.toggle('listening');
33
+ if (micButton.classList.contains('listening')) {
34
+ startSpeechRecognition();
35
+ } else {
36
+ stopSpeechRecognition();
37
+ }
38
+ } else {
39
+ alert('La reconnaissance vocale n\'est pas supportée par votre navigateur');
40
+ }
41
+ });
42
+
43
+ // Send message
44
+ const sendButton = shadowRoot.getElementById('sendButton');
45
+ const messageInput = shadowRoot.getElementById('messageInput');
46
+ sendButton.addEventListener('click', sendMessage);
47
+ messageInput.addEventListener('keypress', (e) => {
48
+ if (e.key === 'Enter') sendMessage();
49
+ });
50
+
51
+ // Logs simulation
52
+ const logsContainer = shadowRoot.querySelector('.logs-container');
53
+ setInterval(() => {
54
+ const timestamp = new Date().toLocaleTimeString();
55
+ const logLine = document.createElement('div');
56
+ logLine.className = 'log-line';
57
+ logLine.textContent = `Console • ${timestamp} • activité...`;
58
+ logsContainer.appendChild(logLine);
59
+ logsContainer.scrollTop = logsContainer.scrollHeight;
60
+ }, 1400);
61
+ }
62
+
63
+ let recognition;
64
+ function startSpeechRecognition() {
65
+ const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
66
+ recognition = new SpeechRecognition();
67
+ recognition.lang = 'fr-FR';
68
+ recognition.interimResults = true;
69
+ recognition.continuous = true;
70
+
71
+ recognition.onresult = (event) => {
72
+ const transcript = Array.from(event.results)
73
+ .map(result => result[0])
74
+ .map(result => result.transcript)
75
+ .join(' ');
76
+ messageInput.value = transcript;
77
+ };
78
+
79
+ recognition.onerror = (event) => {
80
+ console.error('Speech recognition error', event.error);
81
+ micButton.classList.remove('listening');
82
+ };
83
+
84
+ recognition.start();
85
+ }
86
+
87
+ function stopSpeechRecognition() {
88
+ if (recognition) {
89
+ recognition.stop();
90
+ }
91
+ }
92
+
93
+ function sendMessage() {
94
+ const message = messageInput.value.trim();
95
+ if (message) {
96
+ // TODO: Implement actual message sending
97
+ console.log('Message sent:', message);
98
+ messageInput.value = '';
99
+ }
100
+ }
101
  });
style.css CHANGED
@@ -40,13 +40,33 @@ custom-code-preview {
40
  .favorites div:hover {
41
  background: #f1f5f9;
42
  }
43
-
44
  .divider {
45
  text-align: center;
46
  color: #94a3b8;
47
  margin: 1rem 0;
48
  }
49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  .conversation-title {
51
  font-weight: 600;
52
  margin-bottom: 1rem;
 
40
  .favorites div:hover {
41
  background: #f1f5f9;
42
  }
 
43
  .divider {
44
  text-align: center;
45
  color: #94a3b8;
46
  margin: 1rem 0;
47
  }
48
 
49
+ .mic-active.listening {
50
+ background-color: #3b82f6;
51
+ color: white;
52
+ }
53
+
54
+ .logs-container {
55
+ max-height: 120px;
56
+ overflow-y: auto;
57
+ background: #f8fafc;
58
+ border: 1px solid #e2e8f0;
59
+ border-radius: 0.5rem;
60
+ padding: 0.5rem;
61
+ margin-bottom: 0.5rem;
62
+ font-family: monospace;
63
+ font-size: 0.8rem;
64
+ }
65
+
66
+ .log-line {
67
+ margin-bottom: 0.25rem;
68
+ color: #64748b;
69
+ }
70
  .conversation-title {
71
  font-weight: 600;
72
  margin-bottom: 1rem;