Improve user experience with battleship feedback and auto-play TTS
- Fix battleship feedback perspective confusion with better Hermes prompting - Add auto-play TTS button with localStorage persistence and queueing system - Move activity controls below model/voice selectors in sidebar - Add activity controls to mobile hamburger menu - Fix model/activity dropdowns to stay within container bounds - Filter activities API to only show .yaml/.yml files - Clean up system message labels by moving to usernames (System (Feedback), System (Question)) - Apply black formatting to app.py
This commit is contained in:
parent
74bb98b517
commit
e28dc11f04
5 changed files with 484 additions and 23 deletions
13
Makefile
13
Makefile
|
|
@ -201,6 +201,19 @@ clean:
|
|||
find . -name "*.pyc" -delete 2>/dev/null || true
|
||||
find . -name "*.pyo" -delete 2>/dev/null || true
|
||||
find . -name "*~" -delete 2>/dev/null || true
|
||||
|
||||
.PHONY: init-db
|
||||
init-db:
|
||||
@echo "🗄️ Initializing database tables..."
|
||||
@if [ -f vars.sh ]; then \
|
||||
. ./vars.sh && python init_db.py; \
|
||||
echo "✅ Database tables created successfully"; \
|
||||
else \
|
||||
echo "❌ Error: vars.sh not found. Please create it from vars.sh.sample"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
clean-cache:
|
||||
rm -rf .pytest_cache/ 2>/dev/null || true
|
||||
rm -rf htmlcov/ 2>/dev/null || true
|
||||
rm -rf .coverage 2>/dev/null || true
|
||||
|
|
|
|||
83
app.py
83
app.py
|
|
@ -31,10 +31,12 @@ from sqlalchemy.exc import InvalidRequestError
|
|||
|
||||
from models import db, Room, UserSession, Message, ActivityState
|
||||
|
||||
app = Flask(__name__)
|
||||
app = Flask(__name__, instance_relative_config=True)
|
||||
|
||||
app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY", "dev-key-change-in-production")
|
||||
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///chat.db"
|
||||
app.config["SQLALCHEMY_DATABASE_URI"] = (
|
||||
f"sqlite:///{os.path.join(app.instance_path, 'chat.db')}"
|
||||
)
|
||||
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
|
||||
|
||||
db.init_app(app)
|
||||
|
|
@ -233,6 +235,28 @@ def get_models():
|
|||
return jsonify({"models": list(MODEL_CLIENT_MAP.keys())})
|
||||
|
||||
|
||||
@app.route("/api/activities", methods=["GET"])
|
||||
def get_activities():
|
||||
"""Return the list of available activities."""
|
||||
activities = []
|
||||
|
||||
if app.config.get("LOCAL_ACTIVITIES"):
|
||||
# List local activity files from research directory
|
||||
import os
|
||||
|
||||
research_dir = "research"
|
||||
if os.path.exists(research_dir):
|
||||
for filename in sorted(os.listdir(research_dir)):
|
||||
if filename.endswith((".yaml", ".yml")):
|
||||
activities.append(f"research/{filename}")
|
||||
else:
|
||||
# For S3 activities, you would list from S3
|
||||
# This is a placeholder - you'd need to implement S3 listing
|
||||
pass
|
||||
|
||||
return jsonify({"activities": activities})
|
||||
|
||||
|
||||
@app.route("/chat/<room_name>")
|
||||
def chat(room_name):
|
||||
# Query all rooms so that newest is first.
|
||||
|
|
@ -651,6 +675,30 @@ def handle_update_message(data):
|
|||
)
|
||||
|
||||
|
||||
@socketio.on("get_activity_status")
|
||||
def handle_get_activity_status(data):
|
||||
"""Get the current activity status for a room."""
|
||||
room_name = data["room_name"]
|
||||
room = get_room(room_name)
|
||||
|
||||
if room:
|
||||
activity_state = ActivityState.query.filter_by(room_id=room.id).first()
|
||||
|
||||
if activity_state:
|
||||
emit(
|
||||
"activity_status",
|
||||
{
|
||||
"active": True,
|
||||
"activity_name": activity_state.s3_file_path,
|
||||
"section_id": activity_state.section_id,
|
||||
"step_id": activity_state.step_id,
|
||||
},
|
||||
room=request.sid,
|
||||
)
|
||||
else:
|
||||
emit("activity_status", {"active": False}, room=request.sid)
|
||||
|
||||
|
||||
def group_consecutive_roles(messages):
|
||||
if not messages:
|
||||
return []
|
||||
|
|
@ -1542,12 +1590,14 @@ def loop_through_steps_until_question(
|
|||
|
||||
# Check if the current step has a question
|
||||
if "question" in step:
|
||||
question_content = f"Question: {step['question']}"
|
||||
question_content = step["question"]
|
||||
translated_question_content = translate_text(
|
||||
question_content, user_language
|
||||
)
|
||||
new_message = Message(
|
||||
username="System", content=translated_question_content, room_id=room.id
|
||||
username="System (Question)",
|
||||
content=translated_question_content,
|
||||
room_id=room.id,
|
||||
)
|
||||
db.session.add(new_message)
|
||||
db.session.commit()
|
||||
|
|
@ -1623,6 +1673,18 @@ def start_activity(room_name, s3_file_path, username):
|
|||
activity_content, activity_state, room_name, username
|
||||
)
|
||||
|
||||
# Emit activity status update
|
||||
socketio.emit(
|
||||
"activity_status",
|
||||
{
|
||||
"active": True,
|
||||
"activity_name": s3_file_path,
|
||||
"section_id": initial_section["section_id"],
|
||||
"step_id": initial_step["step_id"],
|
||||
},
|
||||
room=room_name,
|
||||
)
|
||||
|
||||
|
||||
def cancel_activity(room_name, username):
|
||||
with app.app_context():
|
||||
|
|
@ -1656,6 +1718,9 @@ def cancel_activity(room_name, username):
|
|||
room=room_name,
|
||||
)
|
||||
|
||||
# Emit activity status update
|
||||
socketio.emit("activity_status", {"active": False}, room=room_name)
|
||||
|
||||
|
||||
def display_activity_metadata(room_name, username):
|
||||
with app.app_context():
|
||||
|
|
@ -2114,7 +2179,7 @@ def handle_activity_response(room_name, user_response, username):
|
|||
if feedback:
|
||||
# feedback is metadata language aware, doesn't need to be translated.
|
||||
new_message = Message(
|
||||
username="System", content=feedback, room_id=room.id
|
||||
username="System (Feedback)", content=feedback, room_id=room.id
|
||||
)
|
||||
db.session.add(new_message)
|
||||
db.session.commit()
|
||||
|
|
@ -2123,7 +2188,7 @@ def handle_activity_response(room_name, user_response, username):
|
|||
"chat_message",
|
||||
{
|
||||
"id": new_message.id,
|
||||
"username": "System",
|
||||
"username": "System (Feedback)",
|
||||
"content": feedback,
|
||||
},
|
||||
room=room_name,
|
||||
|
|
@ -2199,12 +2264,12 @@ def handle_activity_response(room_name, user_response, username):
|
|||
db.session.commit()
|
||||
|
||||
# Emit the question again
|
||||
question_content = f"Question: {step['question']}"
|
||||
question_content = step["question"]
|
||||
translated_question_content = translate_text(
|
||||
question_content, user_language
|
||||
)
|
||||
new_message = Message(
|
||||
username="System",
|
||||
username="System (Question)",
|
||||
content=translated_question_content,
|
||||
room_id=room.id,
|
||||
)
|
||||
|
|
@ -2517,7 +2582,7 @@ def provide_feedback(
|
|||
json_metadata,
|
||||
json_new_metadata,
|
||||
)
|
||||
feedback += f"\n\nAI Feedback: {ai_feedback}"
|
||||
feedback += f"\n\n{ai_feedback}"
|
||||
|
||||
return feedback
|
||||
|
||||
|
|
|
|||
|
|
@ -187,21 +187,22 @@ sections:
|
|||
If the user wants to exit, categorize as 'exit'.
|
||||
Otherwise, categorize as 'invalid_move'.
|
||||
feedback_tokens_for_ai: |
|
||||
Write battleship feedback from the game's perspective that covers:
|
||||
You are the naval battle narrator. Look at the metadata provided and report what happened.
|
||||
|
||||
1. User's shot result - check user_hit_result in metadata:
|
||||
- If "hit": Describe the impact and explosion
|
||||
- If "miss": Describe the splash and fog of war
|
||||
2. AI's shot result - report where the AI fired:
|
||||
- If hit: Describe the damage to the player's ship
|
||||
- If miss: Describe the near miss and ocean spray
|
||||
3. CRITICAL: If ai_sunk_ship_this_round contains a ship name, express dismay that the AI destroyed the player's ship in 2 sentences describing the carnage at sea
|
||||
4. CRITICAL: If user_sunk_ship_this_round contains a ship name, celebrate the player destroying the AI ship in 2 sentences describing the carnage at sea
|
||||
5. CRITICAL: If game_over is true, announce the victory:
|
||||
- If user_wins is true: Celebrate the player's total victory with excitement!
|
||||
- If ai_wins is true: Express dismay at the player's defeat!
|
||||
STEP 1 - CHECK SHIP DESTRUCTION (MANDATORY):
|
||||
Look in the metadata for these exact fields:
|
||||
- user_sunk_ship_this_round: If this contains a ship name like "Carrier" or "Battleship", say: "💥 SHIP DESTROYED! You have sunk the enemy's [ship name]! The enemy vessel explodes and sinks! Victory!"
|
||||
- ai_sunk_ship_this_round: If this contains a ship name, say: "🔥 YOUR SHIP SUNK! The enemy destroyed your [ship name]! Your vessel burns and sinks!"
|
||||
|
||||
Describe the sights and sounds of naval warfare! You are the game system rooting for the player!
|
||||
STEP 2 - REPORT SHOTS:
|
||||
- Your shot result (user_hit_result): "hit" or "miss"
|
||||
- Enemy shot result (ai_hit_result): "hit" or "miss"
|
||||
|
||||
EXAMPLE RESPONSE FORMAT:
|
||||
If user_sunk_ship_this_round = "Carrier": "💥 SHIP DESTROYED! You have sunk the enemy's Carrier! [shot details]"
|
||||
If ai_sunk_ship_this_round = "Destroyer": "🔥 YOUR SHIP SUNK! The enemy destroyed your Destroyer! [shot details]"
|
||||
|
||||
Always check the metadata for user_sunk_ship_this_round and ai_sunk_ship_this_round first. These are the most important events to report.
|
||||
|
||||
processing_script: |
|
||||
import random
|
||||
|
|
|
|||
|
|
@ -243,6 +243,78 @@
|
|||
.utility-belt {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* Activity controls styling */
|
||||
#activity-controls {
|
||||
margin-top: 20px;
|
||||
padding: 10px;
|
||||
border-top: 1px solid #e1e1e1;
|
||||
}
|
||||
|
||||
#activity-controls h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
#current-activity-info {
|
||||
background-color: #f0f0f0;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
#current-activity-info p {
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
|
||||
#activity-controls button {
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
margin: 4px 2px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
#cancel-activity-btn {
|
||||
background-color: #f44336;
|
||||
}
|
||||
|
||||
#activity-controls button:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
#activity-select {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 5px;
|
||||
border: 1px solid #e1e1e1;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Model and voice select dropdowns styling */
|
||||
#model-select, #voice-select, #model-select-mobile, #voice-select-mobile {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 5px;
|
||||
border: 1px solid #e1e1e1;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Media query for mobile devices */
|
||||
@media (max-width: 768px) {
|
||||
|
|
@ -296,6 +368,28 @@
|
|||
<option value="shimmer">Shimmer</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="auto-play-tts-mobile">
|
||||
<button id="auto-play-tts-btn-mobile" onclick="toggleAutoPlayTTS()" style="width: 100%; margin-top: 10px; background-color: #f44336; color: white; border: none; padding: 8px; border-radius: 4px; cursor: pointer;">
|
||||
Auto-Play TTS: OFF
|
||||
</button>
|
||||
</div>
|
||||
<div id="activity-controls-mobile">
|
||||
<h3>Activities</h3>
|
||||
<div id="current-activity-info-mobile" style="display: none;">
|
||||
<p>Current Activity: <span id="current-activity-name-mobile"></span></p>
|
||||
<button id="cancel-activity-btn-mobile" onclick="cancelActivity()">Cancel Activity</button>
|
||||
</div>
|
||||
<div id="activity-list-section-mobile">
|
||||
<div style="display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 5px; margin-bottom: 5px;">
|
||||
<select id="activity-select-mobile" style="width: 100%; max-width: 100%; box-sizing: border-box;">
|
||||
<option value="">-- Select an Activity --</option>
|
||||
</select>
|
||||
<button id="refresh-activities-btn-mobile" onclick="refreshActivityList()">🔄</button>
|
||||
</div>
|
||||
<button id="load-activity-btn-mobile" onclick="loadSelectedActivityMobile()" style="margin-top: 5px;">Load Activity</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="user-lists-mobile">
|
||||
<div id="active-users-list">
|
||||
<h3>Active Users</h3>
|
||||
|
|
|
|||
|
|
@ -37,6 +37,29 @@
|
|||
<option value="shimmer">Shimmer</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<button id="auto-play-tts-btn" onclick="toggleAutoPlayTTS()" style="width: 100%; margin-top: 10px; background-color: #f44336; color: white; border: none; padding: 8px; border-radius: 4px; cursor: pointer;">
|
||||
Auto-Play TTS: OFF
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="activity-controls">
|
||||
<h3>Activities</h3>
|
||||
<div id="current-activity-info" style="display: none;">
|
||||
<p>Current Activity: <span id="current-activity-name"></span></p>
|
||||
<button id="cancel-activity-btn" onclick="cancelActivity()">Cancel Activity</button>
|
||||
</div>
|
||||
<div id="activity-list-section">
|
||||
<div style="display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 5px; margin-bottom: 5px;">
|
||||
<select id="activity-select">
|
||||
<option value="">-- Select an Activity --</option>
|
||||
</select>
|
||||
<button id="refresh-activities-btn" onclick="refreshActivityList()">🔄</button>
|
||||
</div>
|
||||
<button id="load-activity-btn" onclick="loadSelectedActivity()" style="margin-top: 5px;">Load Activity</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="user-lists">
|
||||
<div id="active-users-list">
|
||||
<h3>Active Users</h3>
|
||||
|
|
@ -83,6 +106,11 @@ let audioCache = {}; // Cache to store audio blobs
|
|||
// Flag to prevent mutual updates on desktop/mobile
|
||||
let isSyncingDropdowns = false;
|
||||
|
||||
// Auto-play TTS state
|
||||
let autoPlayTTS = localStorage.getItem('autoPlayTTS') === 'true' || false;
|
||||
let ttsQueue = [];
|
||||
let isPlayingTTS = false;
|
||||
|
||||
// Function to sanitize the username
|
||||
function sanitizeUsername(username) {
|
||||
// Split the username on commas and take the first part.
|
||||
|
|
@ -121,6 +149,9 @@ document.addEventListener('DOMContentLoaded', (event) => {
|
|||
const voiceSelectDesktop = document.getElementById("voice-select");
|
||||
const modelSelectMobile = document.getElementById("model-select-mobile");
|
||||
const voiceSelectMobile = document.getElementById("voice-select-mobile");
|
||||
|
||||
// Initialize auto-play TTS button state from localStorage
|
||||
updateAutoPlayTTSDisplay();
|
||||
|
||||
// Function to populate the dropdown
|
||||
function populateModelDropdown(models) {
|
||||
|
|
@ -322,8 +353,9 @@ socket.on('update_room_list', function(updatedRoom) {
|
|||
}
|
||||
});
|
||||
|
||||
// Function to read text using TTS
|
||||
// Function to read text using TTS (for manual button clicks)
|
||||
async function speakText(text, playButton, messageId) {
|
||||
console.log("speakText called with:", {text: text, messageId, autoPlay: autoPlayTTS});
|
||||
const voice = document.getElementById("voice-select").value;
|
||||
const cacheKey = `${messageId}-${voice}`; // Unique cache key for each message and voice
|
||||
|
||||
|
|
@ -377,6 +409,121 @@ async function speakText(text, playButton, messageId) {
|
|||
}
|
||||
}
|
||||
|
||||
// Function to read text using TTS (for queued auto-play)
|
||||
async function speakTextQueued(text, playButton, messageId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const voice = document.getElementById("voice-select").value;
|
||||
const cacheKey = `${messageId}-${voice}`;
|
||||
const cleanText = text.replace(/[^a-zA-Z0-9\s.,!?]/g, '');
|
||||
|
||||
const playAudio = (audio) => {
|
||||
audio.onended = () => {
|
||||
console.log("TTS finished for:", messageId);
|
||||
resolve();
|
||||
};
|
||||
audio.onerror = () => {
|
||||
console.error("TTS audio error for:", messageId);
|
||||
reject(new Error("Audio playback failed"));
|
||||
};
|
||||
audio.play().catch(reject);
|
||||
};
|
||||
|
||||
// Check if audio is cached
|
||||
if (audioCache[cacheKey]) {
|
||||
playAudio(audioCache[cacheKey]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch new audio
|
||||
fetch(TTS_API_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${API_KEY}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'tts-1',
|
||||
voice: voice,
|
||||
input: cleanText
|
||||
})
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response.blob();
|
||||
})
|
||||
.then(audioBlob => {
|
||||
const audioUrl = URL.createObjectURL(audioBlob);
|
||||
const audio = new Audio(audioUrl);
|
||||
audio.playbackRate = 0.9;
|
||||
audioCache[cacheKey] = audio;
|
||||
playAudio(audio);
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
// Function to add TTS to queue
|
||||
function queueTTS(text, playButton, messageId) {
|
||||
ttsQueue.push({ text, playButton, messageId });
|
||||
console.log("Added to TTS queue:", messageId, "Queue length:", ttsQueue.length);
|
||||
processNextTTS();
|
||||
}
|
||||
|
||||
// Function to process the next TTS in queue
|
||||
async function processNextTTS() {
|
||||
if (isPlayingTTS || ttsQueue.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
isPlayingTTS = true;
|
||||
const { text, playButton, messageId } = ttsQueue.shift();
|
||||
console.log("Processing TTS from queue:", messageId);
|
||||
|
||||
try {
|
||||
await speakTextQueued(text, playButton, messageId);
|
||||
} catch (error) {
|
||||
console.error("TTS error:", error);
|
||||
}
|
||||
|
||||
isPlayingTTS = false;
|
||||
// Process next item in queue
|
||||
setTimeout(processNextTTS, 100);
|
||||
}
|
||||
|
||||
// Function to update auto-play TTS button display
|
||||
function updateAutoPlayTTSDisplay() {
|
||||
const autoPlayBtn = document.getElementById("auto-play-tts-btn");
|
||||
const autoPlayBtnMobile = document.getElementById("auto-play-tts-btn-mobile");
|
||||
|
||||
if (autoPlayTTS) {
|
||||
autoPlayBtn.textContent = "Auto-Play TTS: ON";
|
||||
autoPlayBtn.style.backgroundColor = "#4CAF50";
|
||||
autoPlayBtnMobile.textContent = "Auto-Play TTS: ON";
|
||||
autoPlayBtnMobile.style.backgroundColor = "#4CAF50";
|
||||
} else {
|
||||
autoPlayBtn.textContent = "Auto-Play TTS: OFF";
|
||||
autoPlayBtn.style.backgroundColor = "#f44336";
|
||||
autoPlayBtnMobile.textContent = "Auto-Play TTS: OFF";
|
||||
autoPlayBtnMobile.style.backgroundColor = "#f44336";
|
||||
// Clear queue when turning off
|
||||
ttsQueue = [];
|
||||
isPlayingTTS = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Function to toggle auto-play TTS
|
||||
function toggleAutoPlayTTS() {
|
||||
autoPlayTTS = !autoPlayTTS;
|
||||
console.log("Auto-play TTS toggled to:", autoPlayTTS);
|
||||
|
||||
// Save to localStorage
|
||||
localStorage.setItem('autoPlayTTS', autoPlayTTS.toString());
|
||||
|
||||
updateAutoPlayTTSDisplay();
|
||||
}
|
||||
|
||||
// Function to toggle audio playback
|
||||
function toggleAudioPlayback(audio, playButton) {
|
||||
if (currentAudio && currentAudio !== audio) {
|
||||
|
|
@ -467,6 +614,20 @@ socket.on("chat_message", (data) => {
|
|||
// Scroll to the bottom of the chat container to show the new message.
|
||||
if (data.id) {
|
||||
document.getElementById("chat").scrollTop = document.getElementById("chat").scrollHeight;
|
||||
|
||||
// Auto-play TTS if enabled and message has content - AFTER buttons are created
|
||||
if (autoPlayTTS && data.content && data.content.trim() !== "") {
|
||||
setTimeout(() => {
|
||||
// Find the play button after buttons have been created
|
||||
const buttons = messageWrapper.querySelectorAll("button");
|
||||
const playButton = Array.from(buttons).find(btn => btn.textContent === "Play");
|
||||
console.log("Auto-play TTS: enabled=", autoPlayTTS, "content=", data.content, "playButton=", playButton);
|
||||
if (playButton) {
|
||||
console.log("Queueing TTS for message:", data.id);
|
||||
queueTTS(data.content, playButton, data.id);
|
||||
}
|
||||
}, 100); // Short delay to let buttons be created
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -630,6 +791,17 @@ socket.on("message_chunk", (data) => {
|
|||
|
||||
// Append the button container before the message content
|
||||
messageWrapper.insertBefore(buttonContainer, targetMessageElement);
|
||||
|
||||
// Auto-play TTS if enabled and message is complete (only when streaming finishes)
|
||||
if (autoPlayTTS && data.is_complete && messageBuffers[data.id] && messageBuffers[data.id].trim() !== "") {
|
||||
const playButton = buttonContainer.querySelector("button");
|
||||
if (playButton && playButton.textContent === "Play") {
|
||||
setTimeout(() => {
|
||||
const fullText = targetMessageElement.textContent || targetMessageElement.innerText;
|
||||
queueTTS(fullText, playButton, data.id);
|
||||
}, 500); // Small delay to let the message render
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -853,5 +1025,121 @@ socket.on("set_background", (data) => {
|
|||
chat.style.backgroundSize = "auto"; // Ensures the image is not stretched
|
||||
});
|
||||
|
||||
// Activity management functions
|
||||
function refreshActivityList() {
|
||||
fetch('/api/activities')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const activitySelect = document.getElementById('activity-select');
|
||||
const activitySelectMobile = document.getElementById('activity-select-mobile');
|
||||
|
||||
// Clear existing options except the first one for desktop
|
||||
while (activitySelect.options.length > 1) {
|
||||
activitySelect.remove(1);
|
||||
}
|
||||
|
||||
// Clear existing options except the first one for mobile
|
||||
while (activitySelectMobile.options.length > 1) {
|
||||
activitySelectMobile.remove(1);
|
||||
}
|
||||
|
||||
// Add activities to both dropdowns
|
||||
data.activities.forEach(activity => {
|
||||
const option = document.createElement('option');
|
||||
option.value = activity;
|
||||
option.textContent = activity;
|
||||
activitySelect.appendChild(option);
|
||||
|
||||
const optionMobile = document.createElement('option');
|
||||
optionMobile.value = activity;
|
||||
optionMobile.textContent = activity;
|
||||
activitySelectMobile.appendChild(optionMobile);
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching activities:', error);
|
||||
alert('Failed to fetch activities');
|
||||
});
|
||||
}
|
||||
|
||||
function loadSelectedActivity() {
|
||||
const activitySelect = document.getElementById('activity-select');
|
||||
const selectedActivity = activitySelect.value;
|
||||
|
||||
if (!selectedActivity) {
|
||||
alert('Please select an activity');
|
||||
return;
|
||||
}
|
||||
|
||||
// Send command to load activity
|
||||
socket.emit("chat_message", {
|
||||
"username": username,
|
||||
"message": `/activity ${selectedActivity}`,
|
||||
"model": document.getElementById("model-select").value,
|
||||
"room_name": room_name
|
||||
});
|
||||
}
|
||||
|
||||
function loadSelectedActivityMobile() {
|
||||
const activitySelectMobile = document.getElementById('activity-select-mobile');
|
||||
const selectedActivity = activitySelectMobile.value;
|
||||
|
||||
if (!selectedActivity) {
|
||||
alert('Please select an activity');
|
||||
return;
|
||||
}
|
||||
|
||||
// Send command to load activity
|
||||
socket.emit("chat_message", {
|
||||
"username": username,
|
||||
"message": `/activity ${selectedActivity}`,
|
||||
"model": document.getElementById("model-select").value,
|
||||
"room_name": room_name
|
||||
});
|
||||
}
|
||||
|
||||
function cancelActivity() {
|
||||
if (confirm('Are you sure you want to cancel the current activity?')) {
|
||||
socket.emit("chat_message", {
|
||||
"username": username,
|
||||
"message": "/activity cancel",
|
||||
"model": document.getElementById("model-select").value,
|
||||
"room_name": room_name
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Socket event for activity status updates
|
||||
socket.on("activity_status", (data) => {
|
||||
const currentActivityInfo = document.getElementById('current-activity-info');
|
||||
const activityListSection = document.getElementById('activity-list-section');
|
||||
const currentActivityName = document.getElementById('current-activity-name');
|
||||
const currentActivityInfoMobile = document.getElementById('current-activity-info-mobile');
|
||||
const activityListSectionMobile = document.getElementById('activity-list-section-mobile');
|
||||
const currentActivityNameMobile = document.getElementById('current-activity-name-mobile');
|
||||
|
||||
if (data.active) {
|
||||
currentActivityInfo.style.display = 'block';
|
||||
activityListSection.style.display = 'none';
|
||||
currentActivityName.textContent = data.activity_name || 'Unknown';
|
||||
currentActivityInfoMobile.style.display = 'block';
|
||||
activityListSectionMobile.style.display = 'none';
|
||||
currentActivityNameMobile.textContent = data.activity_name || 'Unknown';
|
||||
} else {
|
||||
currentActivityInfo.style.display = 'none';
|
||||
activityListSection.style.display = 'block';
|
||||
currentActivityInfoMobile.style.display = 'none';
|
||||
activityListSectionMobile.style.display = 'block';
|
||||
}
|
||||
});
|
||||
|
||||
// Load activities on page load
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
refreshActivityList();
|
||||
|
||||
// Request current activity status
|
||||
socket.emit("get_activity_status", {"room_name": room_name});
|
||||
});
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue