Active User & Configuring Model & voice from Query Strings #7
5 changed files with 346 additions and 66 deletions
115
app.py
115
app.py
|
|
@ -25,7 +25,7 @@ from flask import (
|
|||
Response,
|
||||
)
|
||||
|
||||
from flask_socketio import SocketIO, emit, join_room
|
||||
from flask_socketio import SocketIO, emit, join_room, leave_room
|
||||
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from sqlalchemy.exc import InvalidRequestError
|
||||
|
|
@ -170,6 +170,29 @@ class Room(db.Model):
|
|||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(128), nullable=False, unique=True)
|
||||
title = db.Column(db.String(128), nullable=True)
|
||||
# Store as a comma-separated string
|
||||
active_users = db.Column(db.Text, default="")
|
||||
|
||||
def add_user(self, username):
|
||||
users = set(self.active_users.split(",")) if self.active_users else set()
|
||||
users.add(username)
|
||||
self.active_users = ",".join(sorted(users))
|
||||
|
||||
def remove_user(self, username):
|
||||
users = set(self.active_users.split(",")) if self.active_users else set()
|
||||
users.discard(username)
|
||||
self.active_users = ",".join(sorted(users))
|
||||
|
||||
def get_active_users(self):
|
||||
return self.active_users.split(",") if self.active_users else []
|
||||
|
||||
|
||||
class UserSession(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
session_id = db.Column(db.String(128), unique=True, nullable=False)
|
||||
username = db.Column(db.String(128))
|
||||
room_name = db.Column(db.String(128))
|
||||
room_id = db.Column(db.Integer)
|
||||
|
||||
|
||||
class Message(db.Model):
|
||||
|
|
@ -427,11 +450,31 @@ def search_messages(keywords):
|
|||
return search_results_list
|
||||
|
||||
|
||||
# Handle user joining a room
|
||||
@socketio.on("join")
|
||||
def on_join(data):
|
||||
room_name = data["room_name"]
|
||||
username = data["username"]
|
||||
room = get_room(room_name)
|
||||
|
||||
room.add_user(username)
|
||||
|
||||
# Store session data in the database
|
||||
user_session = UserSession(
|
||||
session_id=request.sid, username=username, room_name=room_name, room_id=room.id
|
||||
)
|
||||
db.session.add(user_session)
|
||||
|
||||
# Emit the active users list to the new joiner
|
||||
emit("active_users", {"users": room.get_active_users()}, room=request.sid)
|
||||
# Emit the active users list to everyone in the room
|
||||
emit(
|
||||
"active_users",
|
||||
{"users": room.get_active_users()},
|
||||
room=room_name,
|
||||
include_self=False,
|
||||
)
|
||||
|
||||
# this makes the client start listening for new events for this room.
|
||||
join_room(room_name)
|
||||
|
||||
|
|
@ -446,9 +489,6 @@ def on_join(data):
|
|||
total_token_count = 0
|
||||
|
||||
# Send the history of messages only to the newly connected client.
|
||||
# The reason for using `request.sid` here is to target the specific session (or client) that
|
||||
# just connected, so only they receive the backlog of messages, rather than broadcasting
|
||||
# this information to all clients in the room.
|
||||
for message in previous_messages:
|
||||
if not message.is_base64_image():
|
||||
total_token_count += message.token_count
|
||||
|
|
@ -466,17 +506,18 @@ def on_join(data):
|
|||
if room.title is None and message_count >= 6:
|
||||
room.title = gpt_generate_room_title(previous_messages)
|
||||
db.session.add(room)
|
||||
db.session.commit()
|
||||
socketio.emit("update_room_title", {"title": room.title}, room=room.name)
|
||||
# Emit an event to update this rooms title in the sidebar for all users.
|
||||
# Emit an event to update this room's title in the sidebar for all users.
|
||||
updated_room_data = {"id": room.id, "name": room.name, "title": room.title}
|
||||
socketio.emit("update_room_list", updated_room_data, room=None)
|
||||
|
||||
# commit session & active user list and title to database.
|
||||
db.session.commit()
|
||||
|
||||
# Broadcast to all clients in the room that a new user has joined.
|
||||
# Here, `room=room` ensures the message is sent to everyone in that specific room.
|
||||
emit(
|
||||
"chat_message",
|
||||
{"id": None, "content": f"{data['username']} has joined the room."},
|
||||
{"id": None, "content": f"{username} has joined the room."},
|
||||
room=room.name,
|
||||
)
|
||||
emit(
|
||||
|
|
@ -489,6 +530,24 @@ def on_join(data):
|
|||
)
|
||||
|
||||
|
||||
# Handle user leaving a room
|
||||
@socketio.on("disconnect")
|
||||
def on_disconnect():
|
||||
sid = request.sid
|
||||
user_session = UserSession.query.filter_by(session_id=sid).first()
|
||||
|
||||
if user_session:
|
||||
room_name = user_session.room_name
|
||||
username = user_session.username
|
||||
room = Room.query.filter_by(name=room_name).first()
|
||||
room.remove_user(username)
|
||||
leave_room(room_name)
|
||||
emit("active_users", {"users": room.get_active_users()}, room=room_name)
|
||||
# Remove session data from the database
|
||||
db.session.delete(user_session)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
@socketio.on("chat_message")
|
||||
def handle_message(data):
|
||||
room_name = data["room_name"]
|
||||
|
|
@ -1017,6 +1076,12 @@ def chat_claude(
|
|||
db.session.add(new_message)
|
||||
db.session.commit()
|
||||
|
||||
socketio.emit(
|
||||
"message_chunk",
|
||||
{"id": msg_id, "content": "", "is_complete": True},
|
||||
room=room.name,
|
||||
)
|
||||
|
||||
socketio.emit("delete_processing_message", msg_id, room=room.name)
|
||||
|
||||
|
||||
|
|
@ -1037,7 +1102,9 @@ def get_openai_client_and_model(model_name="NousResearch/Hermes-3-Llama-3.1-8B")
|
|||
if is_vllm_model:
|
||||
openai_client = OpenAI(base_url=vllm_endpoint, api_key=vllm_api_key)
|
||||
elif is_ollama_model:
|
||||
openai_client = OpenAI(base_url="http://127.0.0.1:11434/v1", api_key=vllm_api_key)
|
||||
openai_client = OpenAI(
|
||||
base_url="http://127.0.0.1:11434/v1", api_key=vllm_api_key
|
||||
)
|
||||
elif is_xai_model:
|
||||
openai_client = OpenAI(base_url="https://api.x.ai/v1", api_key=xai_api_key)
|
||||
elif is_google_model:
|
||||
|
|
@ -1163,6 +1230,12 @@ def chat_gpt(username, room_name, model_name="gpt-4o-mini"):
|
|||
db.session.add(new_message)
|
||||
db.session.commit()
|
||||
|
||||
socketio.emit(
|
||||
"message_chunk",
|
||||
{"id": msg_id, "content": "", "is_complete": True},
|
||||
room=room.name,
|
||||
)
|
||||
|
||||
socketio.emit("delete_processing_message", msg_id, room=room.name)
|
||||
|
||||
|
||||
|
|
@ -1269,6 +1342,12 @@ def chat_mistral(username, room_name, model_name="mistral-tiny"):
|
|||
db.session.add(new_message)
|
||||
db.session.commit()
|
||||
|
||||
socketio.emit(
|
||||
"message_chunk",
|
||||
{"id": msg_id, "content": "", "is_complete": True},
|
||||
room=room.name,
|
||||
)
|
||||
|
||||
socketio.emit("delete_processing_message", msg_id, room=room.name)
|
||||
|
||||
|
||||
|
|
@ -1393,6 +1472,12 @@ def chat_together(
|
|||
db.session.add(new_message)
|
||||
db.session.commit()
|
||||
|
||||
socketio.emit(
|
||||
"message_chunk",
|
||||
{"id": msg_id, "content": "", "is_complete": True},
|
||||
room=room.name,
|
||||
)
|
||||
|
||||
socketio.emit("delete_processing_message", msg_id, room=room.name)
|
||||
|
||||
|
||||
|
|
@ -1499,6 +1584,12 @@ def chat_groq(username, room_name, model_name="mixtral-8x7b-32768"):
|
|||
db.session.add(new_message)
|
||||
db.session.commit()
|
||||
|
||||
socketio.emit(
|
||||
"message_chunk",
|
||||
{"id": msg_id, "content": "", "is_complete": True},
|
||||
room=room.name,
|
||||
)
|
||||
|
||||
socketio.emit("delete_processing_message", msg_id, room=room.name)
|
||||
|
||||
|
||||
|
|
@ -1607,6 +1698,12 @@ def chat_llama(username, room_name, model_name="mistral-7b-instruct-v0.2.Q3_K_L.
|
|||
db.session.add(new_message)
|
||||
db.session.commit()
|
||||
|
||||
socketio.emit(
|
||||
"message_chunk",
|
||||
{"id": msg_id, "content": "", "is_complete": True},
|
||||
room=room.name,
|
||||
)
|
||||
|
||||
socketio.emit("delete_processing_message", msg_id, room=room.name)
|
||||
|
||||
|
||||
|
|
|
|||
32
migrations/versions/1ac5a8e0f577_user_session_table.py
Normal file
32
migrations/versions/1ac5a8e0f577_user_session_table.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
"""user session table
|
||||
|
||||
Revision ID: 1ac5a8e0f577
|
||||
Revises: 38a330686a17
|
||||
Create Date: 2024-11-23 11:25:01.723169
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import sqlite
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '1ac5a8e0f577'
|
||||
down_revision = '38a330686a17'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.create_table('user_session',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('session_id', sa.String(length=128), nullable=False),
|
||||
sa.Column('username', sa.String(length=128), nullable=True),
|
||||
sa.Column('room_name', sa.String(length=128), nullable=True),
|
||||
sa.Column('room_id', sa.Integer(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('session_id')
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_table('user_session')
|
||||
25
migrations/versions/38a330686a17_room_active_users.py
Normal file
25
migrations/versions/38a330686a17_room_active_users.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
"""room active users
|
||||
|
||||
Revision ID: 38a330686a17
|
||||
Revises: d737de68d6fa
|
||||
Create Date: 2024-11-23 09:52:50.824162
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import sqlite
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '38a330686a17'
|
||||
down_revision = 'd737de68d6fa'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
with op.batch_alter_table('room', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('active_users', sa.Text(), nullable=True))
|
||||
|
||||
def downgrade():
|
||||
with op.batch_alter_table('room', schema=None) as batch_op:
|
||||
batch_op.drop_column('active_users')
|
||||
|
|
@ -62,7 +62,7 @@
|
|||
overflow-y: auto;
|
||||
border: 1px solid #e1e1e1;
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
padding-left: 10px;
|
||||
margin-bottom: 10px;
|
||||
width: 100%; /* Allow chat window to fill available space */
|
||||
}
|
||||
|
|
@ -112,7 +112,7 @@
|
|||
/* Styling for the main container that holds the rooms list and chat */
|
||||
.main-container {
|
||||
display: grid;
|
||||
grid-template-columns: 20% 80%; /* Adjust the 20% as needed */
|
||||
grid-template-columns: 15% 70% 15%;
|
||||
width: 100%;
|
||||
height: 90vh;
|
||||
}
|
||||
|
|
@ -174,16 +174,7 @@
|
|||
padding-right: 0.8em; /* Adjust the padding as needed */
|
||||
}
|
||||
.download-links {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.download-links a {
|
||||
margin-bottom: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Hamburger button styling */
|
||||
|
|
@ -249,6 +240,10 @@
|
|||
background-color: #555; /* Darken on hover */
|
||||
}
|
||||
|
||||
.utility-belt {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* Media query for mobile devices */
|
||||
@media (max-width: 768px) {
|
||||
.main-container {
|
||||
|
|
@ -295,7 +290,7 @@
|
|||
<ul id="rooms-list-ul">
|
||||
<!-- Loop through rooms and create list items for each room -->
|
||||
{% for room in rooms %}
|
||||
<a href="{{ url_for('chat', room_name=room.name) }}?username={{ username }}">
|
||||
<a href="{{ url_for('chat', room_name=room.name) }}?{{ request.query_string.decode('utf-8')|safe }}">
|
||||
<li data-room-id="{{ room.id }}">
|
||||
<!-- Display the room title if available, otherwise the room name -->
|
||||
<b>{{ room.name }}</b> {% if room.title %}<br />{{ room.title }} {% endif %}
|
||||
|
|
|
|||
|
|
@ -3,11 +3,6 @@
|
|||
{% block title %}Chatroom{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="download-links">
|
||||
<div>
|
||||
Chat History: <a href="/download_chat_history?room_name={{ room_name }}" download="{{ room_name }}.json">JSON</a> or <a href="/download_chat_history_md?room_name={{ room_name }}" download="{{ room_name }}.md">Markdown</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="chat-container">
|
||||
<!-- Chat area where messages will be displayed -->
|
||||
|
|
@ -18,6 +13,50 @@
|
|||
</form>
|
||||
</div>
|
||||
|
||||
<div class="utility-belt">
|
||||
<div class="download-links">
|
||||
History
|
||||
<a href="/download_chat_history?room_name={{ room_name }}" download="{{ room_name }}.json">JSON</a> or
|
||||
<a href="/download_chat_history_md?room_name={{ room_name }}" download="{{ room_name }}.md">Markdown</a>
|
||||
</div>
|
||||
<br>
|
||||
<div>
|
||||
<label for="model-select">Model</label>
|
||||
<select id="model-select">
|
||||
<option value="None">None</option>
|
||||
<option value="gemini-flash">gemini-flash</option>
|
||||
<option value="gemini-flash-8b">gemini-flash-8b</option>
|
||||
<option value="gemini-pro">gemini-pro</option>
|
||||
<option value="grok-beta">grok-beta</option>
|
||||
<option value="vllm/hermes-llama-3">vllm/hermes-llama-3</option>
|
||||
<option value="gpt-4">gpt-4</option>
|
||||
<option value="gpt-4o-2024-08-06">gpt-4o-2024-08-06</option>
|
||||
<option value="gpt-mini">gpt-mini</option>
|
||||
<option value="gpt-o1-mini">gpt-o1-mini</option>
|
||||
<option value="claude-haiku">claude-haiku</option>
|
||||
<option value="claude-sonnet">claude-sonnet</option>
|
||||
<option value="claude-opus">claude-opus</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="voice-select">Voice</label>
|
||||
<select id="voice-select">
|
||||
<option value="onyx">Onyx</option>
|
||||
<option value="alloy">Alloy</option>
|
||||
<option value="echo">Echo</option>
|
||||
<option value="fable">Fable</option>
|
||||
<option value="nova">Nova</option>
|
||||
<option value="shimmer">Shimmer</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Active Users</h3>
|
||||
<ul id="active-users">
|
||||
<!-- Active users will be dynamically populated here -->
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Constants
|
||||
const API_KEY = "dummy-api-key";
|
||||
|
|
@ -26,6 +65,14 @@ const urlParams = new URLSearchParams(window.location.search);
|
|||
const username = urlParams.get("username");
|
||||
const room_name = "{{ room_name }}";
|
||||
|
||||
// Global constants for valid voices and models
|
||||
const VALID_VOICES = ['alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer'];
|
||||
const VALID_MODELS = [
|
||||
'None', 'gemini-flash', 'gemini-flash-8b', 'gemini-pro', 'grok-beta',
|
||||
'vllm/hermes-llama-3', 'gpt-4', 'gpt-4o-2024-08-06', 'gpt-mini',
|
||||
'gpt-o1-mini', 'claude-haiku', 'claude-sonnet', 'claude-opus'
|
||||
];
|
||||
|
||||
// Configuration for DOMPurify to specify which tags and attributes are allowed
|
||||
const dompurify_config = {
|
||||
ADD_TAGS: ["iframe", "img"],
|
||||
|
|
@ -48,13 +95,73 @@ document.addEventListener('DOMContentLoaded', (event) => {
|
|||
const distanceFromBottom = chatContainer.scrollHeight - chatContainer.scrollTop - chatContainer.clientHeight;
|
||||
userHasScrolledUp = distanceFromBottom > 5;
|
||||
});
|
||||
|
||||
// Set initial model and voice from query string
|
||||
const modelSelect = document.getElementById("model-select");
|
||||
const voiceSelect = document.getElementById("voice-select");
|
||||
const initialModel = urlParams.get("model") || "None";
|
||||
const initialVoice = urlParams.get("voice") || "onyx";
|
||||
modelSelect.value = initialModel;
|
||||
voiceSelect.value = initialVoice;
|
||||
|
||||
// Update query string when model or voice changes
|
||||
modelSelect.addEventListener("change", updateQueryString);
|
||||
voiceSelect.addEventListener("change", updateQueryString);
|
||||
});
|
||||
|
||||
|
||||
// Function to validate and update the query string
|
||||
function updateQueryString() {
|
||||
const sanitizedUsername = sanitizeUsername(username);
|
||||
const modelSelect = document.getElementById("model-select");
|
||||
const voiceSelect = document.getElementById("voice-select");
|
||||
|
||||
// Validate model and voice
|
||||
const model = VALID_MODELS.includes(modelSelect.value) ? modelSelect.value : 'None';
|
||||
const voice = VALID_VOICES.includes(voiceSelect.value) ? voiceSelect.value : 'onyx';
|
||||
|
||||
const newUrl = new URL(window.location.href);
|
||||
newUrl.searchParams.set("username", sanitizedUsername);
|
||||
newUrl.searchParams.set("model", model);
|
||||
newUrl.searchParams.set("voice", voice);
|
||||
window.history.replaceState({}, '', newUrl);
|
||||
}
|
||||
|
||||
// Object to keep track of active users
|
||||
let activeUsers = {};
|
||||
|
||||
// Function to update the active user list in the DOM
|
||||
function updateActiveUserList(users) {
|
||||
const userListElement = document.getElementById("active-users");
|
||||
userListElement.innerHTML = ''; // Clear the current list
|
||||
|
||||
// Populate the list with active users
|
||||
users.forEach(username => {
|
||||
const userItem = document.createElement("li");
|
||||
userItem.textContent = username;
|
||||
userListElement.appendChild(userItem);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Update active users list whenever the event is received
|
||||
socket.on("active_users", (data) => {
|
||||
updateActiveUserList(data.users);
|
||||
});
|
||||
|
||||
|
||||
// Function to handle sending the message
|
||||
function sendMessage() {
|
||||
const message = document.getElementById("message").value;
|
||||
if (message.trim() !== "") { // Ensure we're not sending empty messages
|
||||
socket.emit("chat_message", {"username": username, "message": message, "room_name": room_name});
|
||||
const model = document.getElementById("model-select").value;
|
||||
let messageToSend = message.trim();
|
||||
|
||||
if (model !== "None") {
|
||||
messageToSend = `${model} ${messageToSend}`;
|
||||
}
|
||||
|
||||
if (messageToSend !== "") { // Ensure we're not sending empty messages
|
||||
socket.emit("chat_message", {"username": username, "message": messageToSend, "room_name": room_name});
|
||||
document.getElementById("message").value = "";
|
||||
}
|
||||
}
|
||||
|
|
@ -94,17 +201,34 @@ socket.on('update_room_list', function(updatedRoom) {
|
|||
}
|
||||
});
|
||||
|
||||
// Function to sanitize the username
|
||||
function sanitizeUsername(username) {
|
||||
// Split the username on commas and take the first part.
|
||||
// The backend denormalizes the user list in the room table via csv.
|
||||
return username.split(',')[0].trim();
|
||||
}
|
||||
|
||||
// Socket event when the user connects
|
||||
socket.on("connect", () => {
|
||||
socket.emit("join", {"username": username, "room_name": room_name});
|
||||
// Sanitize the username before joining
|
||||
const sanitizedUsername = sanitizeUsername(username);
|
||||
socket.emit("join", {"username": sanitizedUsername, "room_name": room_name});
|
||||
// Update the query string with the sanitized username
|
||||
updateQueryString();
|
||||
});
|
||||
|
||||
// Function to read text using TTS
|
||||
async function speakText(text, playButton, messageId, voice = 'onyx', rate = 0.9) {
|
||||
async function speakText(text, playButton, messageId) {
|
||||
const voice = document.getElementById("voice-select").value;
|
||||
const cacheKey = `${messageId}-${voice}`; // Unique cache key for each message and voice
|
||||
|
||||
// Clean the text to include only alphanumeric characters, spaces, and key punctuation
|
||||
const cleanText = text.replace(/[^a-zA-Z0-9\s.,!?]/g, '');
|
||||
|
||||
try {
|
||||
// Check if the audio is already cached
|
||||
if (audioCache[messageId]) {
|
||||
const audio = audioCache[messageId];
|
||||
if (audioCache[cacheKey]) {
|
||||
const audio = audioCache[cacheKey];
|
||||
toggleAudioPlayback(audio, playButton);
|
||||
return;
|
||||
}
|
||||
|
|
@ -122,7 +246,7 @@ async function speakText(text, playButton, messageId, voice = 'onyx', rate = 0.9
|
|||
body: JSON.stringify({
|
||||
model: 'tts-1',
|
||||
voice: voice,
|
||||
input: text
|
||||
input: cleanText // Use the cleaned text
|
||||
})
|
||||
});
|
||||
|
||||
|
|
@ -133,10 +257,10 @@ async function speakText(text, playButton, messageId, voice = 'onyx', rate = 0.9
|
|||
const audioBlob = await response.blob();
|
||||
const audioUrl = URL.createObjectURL(audioBlob);
|
||||
const audio = new Audio(audioUrl);
|
||||
audio.playbackRate = rate;
|
||||
audio.playbackRate = 0.9;
|
||||
|
||||
// Cache the audio
|
||||
audioCache[messageId] = audio;
|
||||
// Cache the audio only after it is successfully created
|
||||
audioCache[cacheKey] = audio;
|
||||
|
||||
// Enable button and change text to "Pause"
|
||||
playButton.disabled = false;
|
||||
|
|
@ -327,44 +451,20 @@ socket.on("message_chunk", (data) => {
|
|||
let messageWrapper = document.getElementById(wrapperId);
|
||||
let targetMessageElement;
|
||||
|
||||
// If the message wrapper doesn't exist, it's an initial chunk
|
||||
// If the message wrapper doesn't exist, create it
|
||||
if (!messageWrapper) {
|
||||
messageWrapper = document.createElement("div");
|
||||
messageWrapper.className = "message-wrapper";
|
||||
messageWrapper.id = wrapperId;
|
||||
document.getElementById("chat").appendChild(messageWrapper);
|
||||
}
|
||||
|
||||
// Create the div element to hold the message content
|
||||
// If the message-content div doesn't exist, create it
|
||||
if (!messageWrapper.querySelector(".message-content")) {
|
||||
targetMessageElement = document.createElement("div");
|
||||
targetMessageElement.className = "message-content";
|
||||
|
||||
// Create a container for the buttons
|
||||
const buttonContainer = document.createElement("div");
|
||||
buttonContainer.className = "button-container";
|
||||
|
||||
// Create the "x" button for deletion
|
||||
const deleteButton = document.createElement("button");
|
||||
deleteButton.innerHTML = "x";
|
||||
deleteButton.onclick = () => deleteMessage(data.id, room_name);
|
||||
buttonContainer.appendChild(deleteButton);
|
||||
|
||||
// Create the edit button
|
||||
const editButton = document.createElement("button");
|
||||
editButton.textContent = "Edit";
|
||||
editButton.className = "edit-button";
|
||||
editButton.onclick = () => editMessage(data.id, targetMessageElement);
|
||||
buttonContainer.appendChild(editButton);
|
||||
|
||||
// Create the play button for TTS
|
||||
const playButton = document.createElement("button");
|
||||
playButton.textContent = "Play";
|
||||
playButton.onclick = () => speakText(data.content, playButton, data.id);
|
||||
buttonContainer.appendChild(playButton);
|
||||
|
||||
messageWrapper.appendChild(buttonContainer);
|
||||
messageWrapper.appendChild(targetMessageElement);
|
||||
} else {
|
||||
// If the wrapper already exists, get the message-content div inside it
|
||||
targetMessageElement = messageWrapper.querySelector(".message-content");
|
||||
}
|
||||
|
||||
|
|
@ -394,6 +494,38 @@ socket.on("message_chunk", (data) => {
|
|||
if (!userHasScrolledUp) {
|
||||
document.getElementById("chat").scrollTop = document.getElementById("chat").scrollHeight;
|
||||
}
|
||||
|
||||
// Check if the message is complete and add buttons if they haven't been added
|
||||
if (data.is_complete && !messageWrapper.querySelector(".button-container")) {
|
||||
// Create a container for the buttons
|
||||
const buttonContainer = document.createElement("div");
|
||||
buttonContainer.className = "button-container";
|
||||
|
||||
// Create the delete button
|
||||
const deleteButton = document.createElement("button");
|
||||
deleteButton.innerHTML = "x";
|
||||
deleteButton.onclick = () => deleteMessage(data.id, room_name);
|
||||
buttonContainer.appendChild(deleteButton);
|
||||
|
||||
// Create the edit button
|
||||
const editButton = document.createElement("button");
|
||||
editButton.textContent = "Edit";
|
||||
editButton.className = "edit-button";
|
||||
editButton.onclick = () => editMessage(data.id, targetMessageElement);
|
||||
buttonContainer.appendChild(editButton);
|
||||
|
||||
// Create the play button for TTS
|
||||
const playButton = document.createElement("button");
|
||||
playButton.textContent = "Play";
|
||||
playButton.onclick = () => {
|
||||
const fullText = targetMessageElement.textContent || targetMessageElement.innerText;
|
||||
speakText(fullText, playButton, data.id);
|
||||
};
|
||||
buttonContainer.appendChild(playButton);
|
||||
|
||||
// Append the button container before the message content
|
||||
messageWrapper.insertBefore(buttonContainer, targetMessageElement);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
|
@ -405,7 +537,6 @@ socket.on("message_deleted", (data) => {
|
|||
}
|
||||
});
|
||||
|
||||
|
||||
// Socket event for when a message is updated
|
||||
socket.on("message_updated", (data) => {
|
||||
// Find the existing message wrapper by ID
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue