Enhance user experience with multiple improvements
- Add username field to right sidebar and mobile modal with 'guest' default
- Implement real-time username sync with URL query string updates
- Add opencompletion.com button and new room creation in left sidebar
- Implement room name slugification (e.g. "a whole new world" → "a-whole-new-world")
- Create shared utils.js for common functions like slugify
- Add single search result auto-redirect functionality
- Remove redundant UI elements ("Create New Room" header, docs link)
- Preserve user settings (username, model, voice) across redirects and room creation
Technical improvements:
- Consolidated duplicate code into shared utility functions
- Enhanced search logic with parameter preservation
- Improved mobile/desktop sync for all input fields
- Better URL handling and query string management
This commit is contained in:
parent
88f68e4adc
commit
acdf653eaa
5 changed files with 267 additions and 17 deletions
21
app.py
21
app.py
|
|
@ -22,6 +22,8 @@ from flask import (
|
|||
send_from_directory,
|
||||
jsonify,
|
||||
Response,
|
||||
redirect,
|
||||
url_for,
|
||||
)
|
||||
|
||||
from flask_socketio import SocketIO, emit, join_room, leave_room
|
||||
|
|
@ -365,6 +367,25 @@ def search_page():
|
|||
# Call the function to search messages
|
||||
search_results = search_messages(keywords)
|
||||
|
||||
# If there's exactly one search result, redirect directly to that room
|
||||
if len(search_results) == 1:
|
||||
room_result = search_results[0]
|
||||
room_name = room_result["room_name"]
|
||||
|
||||
# Build the redirect URL with current parameters
|
||||
redirect_params = {}
|
||||
if username and username != "guest":
|
||||
redirect_params["username"] = username
|
||||
|
||||
# Preserve other URL parameters like model, voice, etc.
|
||||
for param in ["model", "voice"]:
|
||||
value = request.args.get(param)
|
||||
if value:
|
||||
redirect_params[param] = value
|
||||
|
||||
redirect_url = url_for("chat", room_name=room_name, **redirect_params)
|
||||
return redirect(redirect_url)
|
||||
|
||||
return render_template(
|
||||
"search.html",
|
||||
rooms=rooms,
|
||||
|
|
|
|||
12
static/js/utils.js
Normal file
12
static/js/utils.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
/**
|
||||
* Utility functions for the OpenCompletion application
|
||||
*/
|
||||
|
||||
/**
|
||||
* Convert a string to a URL-friendly slug
|
||||
* @param {string} str - The string to slugify
|
||||
* @returns {string} - The slugified string
|
||||
*/
|
||||
function slugify(str) {
|
||||
return str.toLowerCase().replace(/\s+/g, '-').replace(/[^\w-]+/g, '');
|
||||
}
|
||||
|
|
@ -22,6 +22,9 @@
|
|||
|
||||
<!-- Include DOMPurify to sanitize HTML and prevent XSS attacks -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/dompurify@2/dist/purify.min.js"></script>
|
||||
|
||||
<!-- Include utility functions -->
|
||||
<script src="{{ url_for('static', filename='js/utils.js') }}"></script>
|
||||
|
||||
<script>
|
||||
// Connect to the server using socket.io
|
||||
|
|
@ -134,6 +137,85 @@
|
|||
#rooms-list {
|
||||
border-right: 1px solid #e1e1e1;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* Styling for site header */
|
||||
#site-header {
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#opencompletion-btn {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
#opencompletion-btn:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
/* Styling for new room creation section */
|
||||
#new-room-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 10px;
|
||||
border: 1px solid #e1e1e1;
|
||||
border-radius: 5px;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
#new-room-section h4 {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 14px;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
#new-room-name {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
resize: vertical;
|
||||
margin-bottom: 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#create-room-btn {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
#create-room-btn:hover {
|
||||
background-color: #218838;
|
||||
}
|
||||
|
||||
/* Styling for public rooms header */
|
||||
#public-rooms-header {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
#public-rooms-header h4 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: #495057;
|
||||
border-bottom: 1px solid #e1e1e1;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
/* Styling for the unordered list in the rooms list */
|
||||
|
|
@ -347,7 +429,6 @@
|
|||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="https://github.com/russellballestrini/opencompletion#interacting-with-language-models" target="_blank">🚀 docs for models & other commands, also try /help</a>
|
||||
<!-- Search form -->
|
||||
<div id="search-form" style="width: 90%;">
|
||||
<form action="/search" method="get">
|
||||
|
|
@ -364,6 +445,10 @@
|
|||
<div id="room-list-modal-content">
|
||||
<button id="close-modal-button" onclick="closeModal()">×</button>
|
||||
<div id="utility-belt-mobile">
|
||||
<div id="username-chooser-mobile">
|
||||
<label for="username-input-mobile">Username:</label>
|
||||
<input type="text" id="username-input-mobile" placeholder="guest" maxlength="50" style="width: 100%; padding: 4px; margin-top: 2px; border: 1px solid #ccc; border-radius: 3px;">
|
||||
</div>
|
||||
<div id="model-chooser-mobile">
|
||||
<label for="model-select-mobile">Choose Model:</label>
|
||||
<select id="model-select-mobile">
|
||||
|
|
@ -428,6 +513,24 @@
|
|||
<!-- Chatroom list -->
|
||||
<div class="main-container">
|
||||
<div id="rooms-list">
|
||||
<!-- opencompletion.com button -->
|
||||
<div id="site-header">
|
||||
<button id="opencompletion-btn" onclick="window.open('https://opencompletion.com', '_blank')">
|
||||
opencompletion.com
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- New room creation section -->
|
||||
<div id="new-room-section">
|
||||
<textarea id="new-room-name" placeholder="Enter room name..." rows="2" maxlength="100"></textarea>
|
||||
<button id="create-room-btn" onclick="createNewRoom()">Create Room</button>
|
||||
</div>
|
||||
|
||||
<!-- Public rooms header -->
|
||||
<div id="public-rooms-header">
|
||||
<h4>Public Rooms</h4>
|
||||
</div>
|
||||
|
||||
<ul id="rooms-list-ul">
|
||||
<!-- Loop through rooms and create list items for each room -->
|
||||
{% for room in rooms %}
|
||||
|
|
@ -477,9 +580,20 @@
|
|||
const modal = document.getElementById("room-list-modal");
|
||||
const modalContent = document.getElementById("room-list-modal-content");
|
||||
const closeButton = document.getElementById("close-modal-button");
|
||||
|
||||
// Clone the site header, new room section, public rooms header, and rooms list
|
||||
const siteHeader = document.getElementById("site-header").cloneNode(true);
|
||||
const newRoomSection = document.getElementById("new-room-section").cloneNode(true);
|
||||
const publicRoomsHeader = document.getElementById("public-rooms-header").cloneNode(true);
|
||||
const roomsList = document.getElementById("rooms-list-ul").cloneNode(true);
|
||||
document.getElementById("rooms-list-modal-content").innerHTML = ''; // Clear previous content
|
||||
|
||||
// Clear previous content and add all sections
|
||||
document.getElementById("rooms-list-modal-content").innerHTML = '';
|
||||
document.getElementById("rooms-list-modal-content").appendChild(siteHeader);
|
||||
document.getElementById("rooms-list-modal-content").appendChild(newRoomSection);
|
||||
document.getElementById("rooms-list-modal-content").appendChild(publicRoomsHeader);
|
||||
document.getElementById("rooms-list-modal-content").appendChild(roomsList);
|
||||
|
||||
modal.style.display = "flex";
|
||||
modalContent.style.display = "block";
|
||||
closeButton.style.display = "block";
|
||||
|
|
@ -494,6 +608,44 @@
|
|||
modalContent.style.display = "none";
|
||||
closeButton.style.display = "none";
|
||||
}
|
||||
|
||||
// Function to create a new room
|
||||
function createNewRoom() {
|
||||
const roomName = document.getElementById("new-room-name").value.trim();
|
||||
|
||||
if (!roomName) {
|
||||
alert("Please enter a room name.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Slugify the room name to make it URL-friendly
|
||||
const slugifiedRoomName = slugify(roomName);
|
||||
|
||||
if (!slugifiedRoomName) {
|
||||
alert("Please enter a valid room name with at least some letters or numbers.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current URL parameters to maintain username, model, voice, etc.
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const currentParams = urlParams.toString();
|
||||
|
||||
// Navigate to the new room using the slugified name
|
||||
window.location.href = `/chat/${slugifiedRoomName}${currentParams ? '?' + currentParams : ''}`;
|
||||
}
|
||||
|
||||
// Add event listener for Enter key in the new room textarea
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const newRoomTextarea = document.getElementById("new-room-name");
|
||||
if (newRoomTextarea) {
|
||||
newRoomTextarea.addEventListener("keydown", function(event) {
|
||||
if (event.key === "Enter" && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
createNewRoom();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Add event listener to the hamburger button
|
||||
document.getElementById("hamburger-button").addEventListener("click", openModal);
|
||||
|
|
|
|||
|
|
@ -20,6 +20,10 @@
|
|||
<a href="/download_chat_history_md?room_name={{ room_name }}" download="{{ room_name }}.md">Markdown</a>
|
||||
</div>
|
||||
<br>
|
||||
<div>
|
||||
<label for="username-input">Username</label>
|
||||
<input type="text" id="username-input" placeholder="guest" maxlength="50" style="width: 100%; padding: 4px; margin-top: 2px; border: 1px solid #ccc; border-radius: 3px;">
|
||||
</div>
|
||||
<div>
|
||||
<label for="model-select">Model</label>
|
||||
<select id="model-select">
|
||||
|
|
@ -82,7 +86,14 @@
|
|||
const API_KEY = "dummy-api-key";
|
||||
const TTS_API_URL = "https://speech.ai.unturf.com/v1/audio/speech";
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const username = urlParams.get("username");
|
||||
let username = urlParams.get("username") || "guest"; // Default to "guest" if no username in URL
|
||||
|
||||
// If no username was in the URL, add it now
|
||||
if (!urlParams.get("username")) {
|
||||
const newUrl = new URL(window.location.href);
|
||||
newUrl.searchParams.set("username", username);
|
||||
window.history.replaceState({}, '', newUrl);
|
||||
}
|
||||
const room_name = "{{ room_name }}";
|
||||
|
||||
// Global constants for valid voices
|
||||
|
|
@ -119,24 +130,35 @@ function sanitizeUsername(username) {
|
|||
return username.split(',')[0].trim();
|
||||
}
|
||||
|
||||
// Function to sync dropdowns and update the query string
|
||||
function syncDropdownsAndQueryString() {
|
||||
const sanitizedUsername = sanitizeUsername(username);
|
||||
// Function to sync all inputs and update the query string
|
||||
function syncInputsAndQueryString() {
|
||||
const usernameInputDesktop = document.getElementById("username-input");
|
||||
const usernameInputMobile = document.getElementById("username-input-mobile");
|
||||
const modelSelectDesktop = document.getElementById("model-select");
|
||||
const voiceSelectDesktop = document.getElementById("voice-select");
|
||||
const modelSelectMobile = document.getElementById("model-select-mobile");
|
||||
const voiceSelectMobile = document.getElementById("voice-select-mobile");
|
||||
|
||||
// Determine the current model and voice from any dropdown
|
||||
// Get current values
|
||||
const currentUsername = usernameInputDesktop?.value || username || "guest";
|
||||
const currentModel = modelSelectDesktop.value;
|
||||
const currentVoice = VALID_VOICES.includes(voiceSelectDesktop.value) ? voiceSelectDesktop.value : 'onyx';
|
||||
|
||||
// Sync both desktop and mobile dropdowns
|
||||
// Update global username variable
|
||||
username = currentUsername;
|
||||
const sanitizedUsername = sanitizeUsername(username);
|
||||
|
||||
// Sync username inputs
|
||||
if (usernameInputDesktop) usernameInputDesktop.value = sanitizedUsername;
|
||||
if (usernameInputMobile) usernameInputMobile.value = sanitizedUsername;
|
||||
|
||||
// Sync dropdowns
|
||||
modelSelectDesktop.value = currentModel;
|
||||
voiceSelectDesktop.value = currentVoice;
|
||||
modelSelectMobile.value = currentModel;
|
||||
voiceSelectMobile.value = currentVoice;
|
||||
if (modelSelectMobile) modelSelectMobile.value = currentModel;
|
||||
if (voiceSelectMobile) voiceSelectMobile.value = currentVoice;
|
||||
|
||||
// Update URL
|
||||
const newUrl = new URL(window.location.href);
|
||||
newUrl.searchParams.set("username", sanitizedUsername);
|
||||
newUrl.searchParams.set("model", currentModel);
|
||||
|
|
@ -144,6 +166,11 @@ function syncDropdownsAndQueryString() {
|
|||
window.history.replaceState({}, '', newUrl);
|
||||
}
|
||||
|
||||
// Backward compatibility
|
||||
function syncDropdownsAndQueryString() {
|
||||
syncInputsAndQueryString();
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', (event) => {
|
||||
const chatContainer = document.getElementById("chat");
|
||||
const modelSelectDesktop = document.getElementById("model-select");
|
||||
|
|
@ -204,13 +231,21 @@ document.addEventListener('DOMContentLoaded', (event) => {
|
|||
userHasScrolledUp = distanceFromBottom > 5;
|
||||
});
|
||||
|
||||
// Set initial model and voice from query string
|
||||
// Set initial model, voice, and username from query string
|
||||
const initialModel = urlParams.get("model") || "None";
|
||||
const initialVoice = urlParams.get("voice") || "onyx";
|
||||
const initialUsername = username; // Already set to URL param or "guest"
|
||||
|
||||
modelSelectDesktop.value = initialModel;
|
||||
voiceSelectDesktop.value = initialVoice;
|
||||
modelSelectMobile.value = initialModel;
|
||||
voiceSelectMobile.value = initialVoice;
|
||||
|
||||
// Set initial username values
|
||||
const usernameInputDesktop = document.getElementById("username-input");
|
||||
const usernameInputMobile = document.getElementById("username-input-mobile");
|
||||
if (usernameInputDesktop) usernameInputDesktop.value = initialUsername;
|
||||
if (usernameInputMobile) usernameInputMobile.value = initialUsername;
|
||||
|
||||
// Add event listeners for desktop dropdowns
|
||||
modelSelectDesktop.addEventListener("change", () => {
|
||||
|
|
@ -245,6 +280,39 @@ document.addEventListener('DOMContentLoaded', (event) => {
|
|||
syncDropdownsAndQueryString();
|
||||
isSyncingDropdowns = false;
|
||||
});
|
||||
|
||||
// Add event listeners for username inputs
|
||||
if (usernameInputDesktop) {
|
||||
usernameInputDesktop.addEventListener("input", () => {
|
||||
if (isSyncingDropdowns) return;
|
||||
isSyncingDropdowns = true;
|
||||
if (usernameInputMobile) {
|
||||
usernameInputMobile.value = usernameInputDesktop.value;
|
||||
}
|
||||
syncInputsAndQueryString();
|
||||
isSyncingDropdowns = false;
|
||||
});
|
||||
|
||||
usernameInputDesktop.addEventListener("blur", () => {
|
||||
syncInputsAndQueryString();
|
||||
});
|
||||
}
|
||||
|
||||
if (usernameInputMobile) {
|
||||
usernameInputMobile.addEventListener("input", () => {
|
||||
if (isSyncingDropdowns) return;
|
||||
isSyncingDropdowns = true;
|
||||
if (usernameInputDesktop) {
|
||||
usernameInputDesktop.value = usernameInputMobile.value;
|
||||
}
|
||||
syncInputsAndQueryString();
|
||||
isSyncingDropdowns = false;
|
||||
});
|
||||
|
||||
usernameInputMobile.addEventListener("blur", () => {
|
||||
syncInputsAndQueryString();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Socket event when the user connects
|
||||
|
|
@ -252,8 +320,8 @@ socket.on("connect", () => {
|
|||
// Sanitize the username before joining
|
||||
const sanitizedUsername = sanitizeUsername(username);
|
||||
socket.emit("join", {"username": sanitizedUsername, "room_name": room_name});
|
||||
// Sync dropdowns and update the query string
|
||||
syncDropdownsAndQueryString();
|
||||
// Sync inputs and update the query string
|
||||
syncInputsAndQueryString();
|
||||
});
|
||||
|
||||
// Function to update the active and inactive user lists in the DOM
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
<title>Chatroom</title>
|
||||
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.min.js"></script>
|
||||
<script src="{{ url_for('static', filename='js/utils.js') }}"></script>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
|
|
@ -52,10 +53,6 @@
|
|||
</div>
|
||||
|
||||
<script>
|
||||
function slugify(str) {
|
||||
return str.toLowerCase().replace(/\s+/g, '-').replace(/[^\w-]+/g, '');
|
||||
}
|
||||
|
||||
document.getElementById('join-room-form').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
const username = document.getElementById('username').value;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue