Abmacode12 commited on
Commit
163ae2c
·
verified ·
1 Parent(s): 6065311

+ = 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 (2) hide show
  1. components/chat.js +50 -27
  2. script.js +11 -5
components/chat.js CHANGED
@@ -76,43 +76,54 @@ display: block;
76
  gap: 0.5rem;
77
  align-items: center;
78
  }
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;
86
- border-radius: 0.5rem;
87
- outline: none;
88
- }
89
- .message-input:focus {
90
- border-color: #93c5fd;
91
- }
92
  .action-button {
93
  background: none;
94
- border: none;
95
  cursor: pointer;
96
  color: #64748b;
97
  padding: 0.5rem;
98
  border-radius: 0.5rem;
 
 
 
 
 
99
  }
100
  .action-button:hover {
101
  background: #f1f5f9;
102
  color: #1e40af;
103
  }
 
 
 
 
 
 
 
 
 
 
104
  .send-button {
105
  background: #3b82f6;
106
  color: white;
107
  border: none;
108
  border-radius: 0.5rem;
109
- padding: 0 1rem;
110
  cursor: pointer;
 
 
 
 
 
111
  }
112
  .send-button:hover {
113
  background: #2563eb;
114
  }
115
- i {
 
 
 
 
116
  width: 20px;
117
  height: 20px;
118
  }
@@ -171,25 +182,37 @@ display: block;
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>
 
76
  gap: 0.5rem;
77
  align-items: center;
78
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  .action-button {
80
  background: none;
81
+ border: 1px solid #e2e8f0;
82
  cursor: pointer;
83
  color: #64748b;
84
  padding: 0.5rem;
85
  border-radius: 0.5rem;
86
+ display: flex;
87
+ align-items: center;
88
+ justify-content: center;
89
+ width: 36px;
90
+ height: 36px;
91
  }
92
  .action-button:hover {
93
  background: #f1f5f9;
94
  color: #1e40af;
95
  }
96
+ .message-input {
97
+ flex-grow: 1;
98
+ padding: 0.75rem 1rem;
99
+ border: 1px solid #e2e8f0;
100
+ border-radius: 0.5rem;
101
+ outline: none;
102
+ }
103
+ .message-input:focus {
104
+ border-color: #93c5fd;
105
+ }
106
  .send-button {
107
  background: #3b82f6;
108
  color: white;
109
  border: none;
110
  border-radius: 0.5rem;
111
+ padding: 0;
112
  cursor: pointer;
113
+ width: 36px;
114
+ height: 36px;
115
+ display: flex;
116
+ align-items: center;
117
+ justify-content: center;
118
  }
119
  .send-button:hover {
120
  background: #2563eb;
121
  }
122
+ .mic-active.listening {
123
+ background: #3b82f6;
124
+ color: white;
125
+ }
126
+ i {
127
  width: 20px;
128
  height: 20px;
129
  }
 
182
  <div class="conversation">
183
  <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>
184
  </div>
185
+ <div class="computer-screen">
186
+ <div class="screen-title">Voir l'ordinateur de Espace Codage</div>
187
+ <div class="logs-container">
188
+ <div class="log-line">Espace Codage • Démarrage...</div>
189
+ <div class="log-line">Chargement des services...</div>
190
+ <div class="log-line">Prêt ✅</div>
191
+ </div>
192
+ </div>
193
  <div class="input-area">
194
+ <button class="action-button" title="Ajouter des fichiers" id="fileButton">
195
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
196
+ <path d="M12 5v14M5 12h14"/>
197
+ </svg>
198
  </button>
199
  <button class="action-button" title="Connecter des applications" id="connectButton">
200
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
201
+ <path d="M9 7v5M15 7v5M7 12h10M10 12v4a4 4 0 0 0 4 4h1M6 7h12"/>
202
+ </svg>
203
  </button>
204
  <input type="text" class="message-input" placeholder="Envoyer un message à Espace Codage" id="messageInput">
205
  <button class="action-button mic-active" title="Saisie vocale" id="micButton">
206
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
207
+ <path d="M12 14a3 3 0 0 0 3-3V7a3 3 0 0 0-6 0v4a3 3 0 0 0 3 3Z"/>
208
+ <path d="M19 11a7 7 0 0 1-14 0" stroke-linecap="round"/>
209
+ <path d="M12 18v3M8 21h8" stroke-linecap="round"/>
210
+ </svg>
211
  </button>
212
  <button class="send-button" title="Envoyer" id="sendButton">
213
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
214
+ <path d="M5 12h14M13 6l6 6-6 6"/>
215
+ </svg>
216
  </button>
217
  <input type="file" class="file-input" id="fileInput" multiple>
218
  </div>
script.js CHANGED
@@ -13,7 +13,6 @@ document.addEventListener('DOMContentLoaded', function() {
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');
@@ -22,7 +21,11 @@ document.addEventListener('DOMContentLoaded', function() {
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
@@ -36,11 +39,14 @@ document.addEventListener('DOMContentLoaded', function() {
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);
 
13
  const chat = document.querySelector('custom-chat');
14
  if (chat) {
15
  const shadowRoot = chat.shadowRoot;
 
16
  // File picker
17
  const fileButton = shadowRoot.getElementById('fileButton');
18
  const fileInput = shadowRoot.getElementById('fileInput');
 
21
  // Connect apps
22
  const connectButton = shadowRoot.getElementById('connectButton');
23
  connectButton.addEventListener('click', () => {
24
+ const conversation = shadowRoot.querySelector('.conversation');
25
+ const message = document.createElement('div');
26
+ message.className = 'message';
27
+ message.textContent = 'Ouverture: connecter des applications...';
28
+ conversation.appendChild(message);
29
  });
30
 
31
  // Microphone
 
39
  stopSpeechRecognition();
40
  }
41
  } else {
42
+ const conversation = shadowRoot.querySelector('.conversation');
43
+ const message = document.createElement('div');
44
+ message.className = 'message';
45
+ message.textContent = 'La reconnaissance vocale n\'est pas supportée par votre navigateur';
46
+ conversation.appendChild(message);
47
  }
48
  });
49
+ // Send message
 
50
  const sendButton = shadowRoot.getElementById('sendButton');
51
  const messageInput = shadowRoot.getElementById('messageInput');
52
  sendButton.addEventListener('click', sendMessage);