485 lines
17 KiB
HTML
485 lines
17 KiB
HTML
|
|
<!doctype html>
|
||
|
|
<html>
|
||
|
|
<head>
|
||
|
|
<title>General Bots - ChatGPT Clone</title>
|
||
|
|
<style>
|
||
|
|
* {
|
||
|
|
margin: 0;
|
||
|
|
padding: 0;
|
||
|
|
box-sizing: border-box;
|
||
|
|
}
|
||
|
|
body {
|
||
|
|
font-family:
|
||
|
|
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||
|
|
sans-serif;
|
||
|
|
background: #343541;
|
||
|
|
color: white;
|
||
|
|
height: 100vh;
|
||
|
|
display: flex;
|
||
|
|
}
|
||
|
|
.sidebar {
|
||
|
|
width: 260px;
|
||
|
|
background: #202123;
|
||
|
|
padding: 10px;
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
}
|
||
|
|
.new-chat {
|
||
|
|
background: transparent;
|
||
|
|
border: 1px solid #4d4d4f;
|
||
|
|
color: white;
|
||
|
|
padding: 12px;
|
||
|
|
border-radius: 6px;
|
||
|
|
margin-bottom: 10px;
|
||
|
|
cursor: pointer;
|
||
|
|
}
|
||
|
|
.voice-toggle {
|
||
|
|
background: #19c37d;
|
||
|
|
border: 1px solid #19c37d;
|
||
|
|
color: white;
|
||
|
|
padding: 12px;
|
||
|
|
border-radius: 6px;
|
||
|
|
margin-bottom: 10px;
|
||
|
|
cursor: pointer;
|
||
|
|
}
|
||
|
|
.voice-toggle.recording {
|
||
|
|
background: #ef4444;
|
||
|
|
border: 1px solid #ef4444;
|
||
|
|
}
|
||
|
|
.history {
|
||
|
|
flex: 1;
|
||
|
|
overflow-y: auto;
|
||
|
|
}
|
||
|
|
.history-item {
|
||
|
|
padding: 12px;
|
||
|
|
border-radius: 6px;
|
||
|
|
margin-bottom: 5px;
|
||
|
|
cursor: pointer;
|
||
|
|
}
|
||
|
|
.history-item:hover {
|
||
|
|
background: #2a2b32;
|
||
|
|
}
|
||
|
|
.main {
|
||
|
|
flex: 1;
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
}
|
||
|
|
.messages {
|
||
|
|
flex: 1;
|
||
|
|
overflow-y: auto;
|
||
|
|
padding: 20px;
|
||
|
|
}
|
||
|
|
.message {
|
||
|
|
max-width: 800px;
|
||
|
|
margin: 0 auto 20px;
|
||
|
|
line-height: 1.5;
|
||
|
|
}
|
||
|
|
.user-message {
|
||
|
|
color: #d1d5db;
|
||
|
|
}
|
||
|
|
.assistant-message {
|
||
|
|
color: #ececf1;
|
||
|
|
}
|
||
|
|
.voice-message {
|
||
|
|
color: #19c37d;
|
||
|
|
font-style: italic;
|
||
|
|
}
|
||
|
|
.input-area {
|
||
|
|
max-width: 800px;
|
||
|
|
margin: 0 auto 20px;
|
||
|
|
position: relative;
|
||
|
|
}
|
||
|
|
.input-area input {
|
||
|
|
width: 100%;
|
||
|
|
background: #40414f;
|
||
|
|
border: none;
|
||
|
|
padding: 12px 45px 12px 15px;
|
||
|
|
border-radius: 12px;
|
||
|
|
color: white;
|
||
|
|
font-size: 16px;
|
||
|
|
}
|
||
|
|
.input-area button {
|
||
|
|
position: absolute;
|
||
|
|
right: 5px;
|
||
|
|
top: 5px;
|
||
|
|
background: #19c37d;
|
||
|
|
border: none;
|
||
|
|
padding: 8px 12px;
|
||
|
|
border-radius: 6px;
|
||
|
|
color: white;
|
||
|
|
cursor: pointer;
|
||
|
|
}
|
||
|
|
.voice-status {
|
||
|
|
text-align: center;
|
||
|
|
margin: 10px 0;
|
||
|
|
color: #19c37d;
|
||
|
|
}
|
||
|
|
.pulse {
|
||
|
|
animation: pulse 2s infinite;
|
||
|
|
}
|
||
|
|
@keyframes pulse {
|
||
|
|
0% {
|
||
|
|
opacity: 1;
|
||
|
|
}
|
||
|
|
50% {
|
||
|
|
opacity: 0.5;
|
||
|
|
}
|
||
|
|
100% {
|
||
|
|
opacity: 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
</style>
|
||
|
|
</head>
|
||
|
|
<body>
|
||
|
|
<div class="sidebar">
|
||
|
|
<button class="new-chat" onclick="createNewSession()">
|
||
|
|
+ New chat
|
||
|
|
</button>
|
||
|
|
<button
|
||
|
|
class="voice-toggle"
|
||
|
|
id="voiceToggle"
|
||
|
|
onclick="toggleVoiceMode()"
|
||
|
|
>
|
||
|
|
🎤 Voice Mode
|
||
|
|
</button>
|
||
|
|
<div class="history" id="history"></div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="main">
|
||
|
|
<div class="voice-status" id="voiceStatus" style="display: none">
|
||
|
|
<div class="pulse">🎤 Listening... Speak now</div>
|
||
|
|
</div>
|
||
|
|
<div class="messages" id="messages"></div>
|
||
|
|
<div class="input-area">
|
||
|
|
<input
|
||
|
|
type="text"
|
||
|
|
id="messageInput"
|
||
|
|
placeholder="Type your message or use voice..."
|
||
|
|
onkeypress="handleKeyPress(event)"
|
||
|
|
/>
|
||
|
|
<button onclick="sendMessage()">Send</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<script src="https://unpkg.com/livekit-client@latest/dist/livekit-client.js"></script>
|
||
|
|
<script>
|
||
|
|
let ws = null;
|
||
|
|
let currentSessionId = null;
|
||
|
|
let isStreaming = false;
|
||
|
|
let voiceRoom = null;
|
||
|
|
let isVoiceMode = false;
|
||
|
|
let mediaRecorder = null;
|
||
|
|
let audioChunks = [];
|
||
|
|
|
||
|
|
async function loadSessions() {
|
||
|
|
const response = await fetch("/api/sessions");
|
||
|
|
const sessions = await response.json();
|
||
|
|
const history = document.getElementById("history");
|
||
|
|
history.innerHTML = "";
|
||
|
|
|
||
|
|
sessions.forEach((session) => {
|
||
|
|
const div = document.createElement("div");
|
||
|
|
div.className = "history-item";
|
||
|
|
div.textContent = session.title;
|
||
|
|
div.onclick = () => switchSession(session.id);
|
||
|
|
history.appendChild(div);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
async function createNewSession() {
|
||
|
|
const response = await fetch("/api/sessions", {
|
||
|
|
method: "POST",
|
||
|
|
});
|
||
|
|
const session = await response.json();
|
||
|
|
currentSessionId = session.session_id;
|
||
|
|
connectWebSocket();
|
||
|
|
loadSessions();
|
||
|
|
document.getElementById("messages").innerHTML = "";
|
||
|
|
if (isVoiceMode) {
|
||
|
|
await startVoiceSession();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function switchSession(sessionId) {
|
||
|
|
currentSessionId = sessionId;
|
||
|
|
loadSessionHistory(sessionId);
|
||
|
|
connectWebSocket();
|
||
|
|
if (isVoiceMode) {
|
||
|
|
startVoiceSession();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function loadSessionHistory(sessionId) {
|
||
|
|
const response = await fetch("/api/sessions/" + sessionId);
|
||
|
|
const history = await response.json();
|
||
|
|
const messages = document.getElementById("messages");
|
||
|
|
messages.innerHTML = "";
|
||
|
|
|
||
|
|
history.forEach(([role, content]) => {
|
||
|
|
const className =
|
||
|
|
role === "user"
|
||
|
|
? "user-message"
|
||
|
|
: role === "assistant"
|
||
|
|
? "assistant-message"
|
||
|
|
: "voice-message";
|
||
|
|
addMessage(
|
||
|
|
role === "user" ? "You" : "Assistant",
|
||
|
|
content,
|
||
|
|
className,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function connectWebSocket() {
|
||
|
|
if (ws) ws.close();
|
||
|
|
ws = new WebSocket("ws://" + window.location.host + "/ws");
|
||
|
|
|
||
|
|
ws.onmessage = function (event) {
|
||
|
|
const response = JSON.parse(event.data);
|
||
|
|
|
||
|
|
if (!response.is_complete) {
|
||
|
|
if (!isStreaming) {
|
||
|
|
isStreaming = true;
|
||
|
|
addMessage(
|
||
|
|
"Assistant",
|
||
|
|
response.content,
|
||
|
|
"assistant-message",
|
||
|
|
true,
|
||
|
|
);
|
||
|
|
} else {
|
||
|
|
updateLastMessage(response.content);
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
isStreaming = false;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
ws.onopen = function () {
|
||
|
|
console.log("Connected to WebSocket");
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function addMessage(
|
||
|
|
sender,
|
||
|
|
content,
|
||
|
|
className,
|
||
|
|
isStreaming = false,
|
||
|
|
) {
|
||
|
|
const messages = document.getElementById("messages");
|
||
|
|
const messageDiv = document.createElement("div");
|
||
|
|
messageDiv.className = `message ${className}`;
|
||
|
|
messageDiv.id = isStreaming ? "streaming-message" : null;
|
||
|
|
messageDiv.innerHTML = `<strong>${sender}:</strong> ${content}`;
|
||
|
|
messages.appendChild(messageDiv);
|
||
|
|
messages.scrollTop = messages.scrollHeight;
|
||
|
|
}
|
||
|
|
|
||
|
|
function updateLastMessage(content) {
|
||
|
|
const lastMessage =
|
||
|
|
document.getElementById("streaming-message");
|
||
|
|
if (lastMessage) {
|
||
|
|
lastMessage.innerHTML = `<strong>Assistant:</strong> ${lastMessage.textContent.replace("Assistant:", "").trim() + content}`;
|
||
|
|
document.getElementById("messages").scrollTop =
|
||
|
|
document.getElementById("messages").scrollHeight;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function sendMessage() {
|
||
|
|
const input = document.getElementById("messageInput");
|
||
|
|
const message = input.value.trim();
|
||
|
|
|
||
|
|
if (message && ws && ws.readyState === WebSocket.OPEN) {
|
||
|
|
addMessage("You", message, "user-message");
|
||
|
|
ws.send(message);
|
||
|
|
input.value = "";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function handleKeyPress(event) {
|
||
|
|
if (event.key === "Enter") {
|
||
|
|
sendMessage();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function toggleVoiceMode() {
|
||
|
|
isVoiceMode = !isVoiceMode;
|
||
|
|
const voiceToggle = document.getElementById("voiceToggle");
|
||
|
|
const voiceStatus = document.getElementById("voiceStatus");
|
||
|
|
|
||
|
|
if (isVoiceMode) {
|
||
|
|
voiceToggle.textContent = "🔴 Stop Voice";
|
||
|
|
voiceToggle.classList.add("recording");
|
||
|
|
voiceStatus.style.display = "block";
|
||
|
|
await startVoiceSession();
|
||
|
|
} else {
|
||
|
|
voiceToggle.textContent = "🎤 Voice Mode";
|
||
|
|
voiceToggle.classList.remove("recording");
|
||
|
|
voiceStatus.style.display = "none";
|
||
|
|
await stopVoiceSession();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function startVoiceSession() {
|
||
|
|
if (!currentSessionId) return;
|
||
|
|
|
||
|
|
try {
|
||
|
|
const response = await fetch("/api/voice/start", {
|
||
|
|
method: "POST",
|
||
|
|
headers: { "Content-Type": "application/json" },
|
||
|
|
body: JSON.stringify({
|
||
|
|
session_id: currentSessionId,
|
||
|
|
user_id: "user_" + currentSessionId,
|
||
|
|
}),
|
||
|
|
});
|
||
|
|
|
||
|
|
const data = await response.json();
|
||
|
|
if (data.token) {
|
||
|
|
await connectToVoiceRoom(data.token);
|
||
|
|
startVoiceRecording();
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error("Failed to start voice session:", error);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function stopVoiceSession() {
|
||
|
|
if (!currentSessionId) return;
|
||
|
|
|
||
|
|
try {
|
||
|
|
await fetch("/api/voice/stop", {
|
||
|
|
method: "POST",
|
||
|
|
headers: { "Content-Type": "application/json" },
|
||
|
|
body: JSON.stringify({
|
||
|
|
session_id: currentSessionId,
|
||
|
|
}),
|
||
|
|
});
|
||
|
|
|
||
|
|
if (voiceRoom) {
|
||
|
|
voiceRoom.disconnect();
|
||
|
|
voiceRoom = null;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (mediaRecorder && mediaRecorder.state === "recording") {
|
||
|
|
mediaRecorder.stop();
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error("Failed to stop voice session:", error);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function connectToVoiceRoom(token) {
|
||
|
|
try {
|
||
|
|
const room = new LiveKitClient.Room();
|
||
|
|
await room.connect("ws://localhost:7880", token);
|
||
|
|
voiceRoom = room;
|
||
|
|
|
||
|
|
room.on("dataReceived", (data) => {
|
||
|
|
const decoder = new TextDecoder();
|
||
|
|
const message = decoder.decode(data);
|
||
|
|
try {
|
||
|
|
const parsed = JSON.parse(message);
|
||
|
|
if (parsed.type === "voice_response") {
|
||
|
|
addMessage(
|
||
|
|
"Assistant",
|
||
|
|
parsed.text,
|
||
|
|
"assistant-message",
|
||
|
|
);
|
||
|
|
}
|
||
|
|
} catch (e) {
|
||
|
|
console.log("Voice data:", message);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
const localTracks = await LiveKitClient.createLocalTracks({
|
||
|
|
audio: true,
|
||
|
|
video: false,
|
||
|
|
});
|
||
|
|
|
||
|
|
for (const track of localTracks) {
|
||
|
|
await room.localParticipant.publishTrack(track);
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error("Failed to connect to voice room:", error);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function startVoiceRecording() {
|
||
|
|
if (!navigator.mediaDevices) {
|
||
|
|
console.log("Media devices not supported");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
navigator.mediaDevices
|
||
|
|
.getUserMedia({ audio: true })
|
||
|
|
.then((stream) => {
|
||
|
|
mediaRecorder = new MediaRecorder(stream);
|
||
|
|
audioChunks = [];
|
||
|
|
|
||
|
|
mediaRecorder.ondataavailable = (event) => {
|
||
|
|
audioChunks.push(event.data);
|
||
|
|
};
|
||
|
|
|
||
|
|
mediaRecorder.onstop = () => {
|
||
|
|
const audioBlob = new Blob(audioChunks, {
|
||
|
|
type: "audio/wav",
|
||
|
|
});
|
||
|
|
simulateVoiceTranscription();
|
||
|
|
};
|
||
|
|
|
||
|
|
mediaRecorder.start();
|
||
|
|
|
||
|
|
setTimeout(() => {
|
||
|
|
if (
|
||
|
|
mediaRecorder &&
|
||
|
|
mediaRecorder.state === "recording"
|
||
|
|
) {
|
||
|
|
mediaRecorder.stop();
|
||
|
|
setTimeout(() => {
|
||
|
|
if (isVoiceMode) {
|
||
|
|
startVoiceRecording();
|
||
|
|
}
|
||
|
|
}, 1000);
|
||
|
|
}
|
||
|
|
}, 5000);
|
||
|
|
})
|
||
|
|
.catch((error) => {
|
||
|
|
console.error("Error accessing microphone:", error);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function simulateVoiceTranscription() {
|
||
|
|
const phrases = [
|
||
|
|
"Hello, how can I help you today?",
|
||
|
|
"I understand what you're saying",
|
||
|
|
"That's an interesting point",
|
||
|
|
"Let me think about that",
|
||
|
|
"I can assist you with that",
|
||
|
|
"What would you like to know?",
|
||
|
|
"That sounds great",
|
||
|
|
"I'm listening to your voice",
|
||
|
|
];
|
||
|
|
|
||
|
|
const randomPhrase =
|
||
|
|
phrases[Math.floor(Math.random() * phrases.length)];
|
||
|
|
|
||
|
|
if (voiceRoom) {
|
||
|
|
const message = {
|
||
|
|
type: "voice_input",
|
||
|
|
content: randomPhrase,
|
||
|
|
timestamp: new Date().toISOString(),
|
||
|
|
};
|
||
|
|
|
||
|
|
voiceRoom.localParticipant.publishData(
|
||
|
|
new TextEncoder().encode(JSON.stringify(message)),
|
||
|
|
LiveKitClient.DataPacketKind.RELIABLE,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
addMessage("You", `🎤 ${randomPhrase}`, "voice-message");
|
||
|
|
}
|
||
|
|
|
||
|
|
createNewSession();
|
||
|
|
</script>
|
||
|
|
</body>
|
||
|
|
</html>
|