gbserver/static/index.html

484 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>