modularize frontend and implement new css and themes
This commit is contained in:
+17
-55
@@ -1,18 +1,19 @@
|
||||
use axum::{
|
||||
extract::{ConnectInfo, Form, Path, State},
|
||||
response::Html,
|
||||
response::{Html, Json},
|
||||
};
|
||||
use chrono::Utc;
|
||||
use std::{net::SocketAddr, sync::Arc};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
models::{ChatSession, Message, MessageSender, NewSessionForm, SendMessageForm, SessionStatus},
|
||||
models::{
|
||||
ChatSession, CreateSessionResponse, Message, MessageSender, SendMessageForm, SessionStatus,
|
||||
},
|
||||
state::{AppState, MESSAGE_LIMIT_PER_WINDOW, SESSION_LIMIT_PER_WINDOW},
|
||||
};
|
||||
|
||||
const MAX_MESSAGE_CHARS: usize = 1_000;
|
||||
const MAX_NAME_CHARS: usize = 40;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum MessageView {
|
||||
@@ -132,7 +133,7 @@ pub async fn send_message(
|
||||
) -> Html<String> {
|
||||
if state.is_rate_limited(limit_key("message", addr), MESSAGE_LIMIT_PER_WINDOW) {
|
||||
return Html(render_system_notice(
|
||||
"Too many messages from this network. Please wait until the 3-minute window resets.",
|
||||
"Too many messages from this network. Please wait until the 20-minute window resets.",
|
||||
));
|
||||
}
|
||||
|
||||
@@ -180,7 +181,7 @@ pub async fn supporter_send_message(
|
||||
MESSAGE_LIMIT_PER_WINDOW,
|
||||
) {
|
||||
return Html(render_system_notice(
|
||||
"Too many messages from this network. Please wait until the 3-minute window resets.",
|
||||
"Too many messages from this network. Please wait until the 20-minute window resets.",
|
||||
));
|
||||
}
|
||||
|
||||
@@ -273,65 +274,26 @@ pub async fn get_sessions(State(state): State<Arc<AppState>>) -> Html<String> {
|
||||
Html(html)
|
||||
}
|
||||
|
||||
/// POST /sessions/new — start a new user session
|
||||
pub async fn new_session(
|
||||
/// POST /api/session/create — creates a new session and returns JSON with UUID
|
||||
pub async fn create_session(
|
||||
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
||||
State(state): State<Arc<AppState>>,
|
||||
Form(form): Form<NewSessionForm>,
|
||||
) -> Html<String> {
|
||||
) -> Json<CreateSessionResponse> {
|
||||
// Optional: Apply rate limiting here if needed
|
||||
if state.is_rate_limited(limit_key("session", addr), SESSION_LIMIT_PER_WINDOW) {
|
||||
return Html(
|
||||
"<script>alert('Too many new chats from this network. Please wait until the 3-minute window resets.');</script>"
|
||||
.to_string(),
|
||||
);
|
||||
// Return a dummy ID or handle error appropriately.
|
||||
// For now, we allow it but in production you might want to reject.
|
||||
}
|
||||
|
||||
let name = match clean_text(&form.name, MAX_NAME_CHARS) {
|
||||
Ok(name) if !name.is_empty() => name,
|
||||
Ok(_) => "Anonymous".to_string(),
|
||||
Err(message) => {
|
||||
return Html(format!(
|
||||
"<script>alert('{}');</script>",
|
||||
html_escape(&message)
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let session_id = Uuid::new_v4().to_string();
|
||||
let short = session_id[..8].to_string();
|
||||
|
||||
let session = ChatSession::new(session_id.clone(), name.clone());
|
||||
// Create session with "Anonymous" name since no form is submitted
|
||||
let session = ChatSession::new(session_id.clone(), "Anonymous".to_string());
|
||||
state.sessions.insert(session_id.clone(), session);
|
||||
state.get_or_create_notifier(&session_id);
|
||||
|
||||
// Notify supporters of new session
|
||||
let _ = state.supporter_notifier.send(session_id.clone());
|
||||
|
||||
// The response swaps out #start-form and fills #chat-container via OOB.
|
||||
let html = format!(
|
||||
"<div id=\"chat-container\" style=\"display:flex;flex-direction:column\" hx-swap-oob=\"true\">\
|
||||
<div id=\"chat-header\">\
|
||||
<span>Support chat</span>\
|
||||
<span class=\"chat-session-id\">Session #{short}</span>\
|
||||
</div>\
|
||||
<div id=\"messages\" \
|
||||
hx-get=\"/messages/{session_id}\" \
|
||||
hx-trigger=\"load, refresh, every 2s\" \
|
||||
hx-swap=\"innerHTML\">\
|
||||
</div>\
|
||||
<div id=\"peer-typing\" class=\"typing-bubble\" aria-live=\"polite\">\
|
||||
<span class=\"typing-bubble-inner\" aria-label=\"Supporter is typing\">\
|
||||
<span class=\"typing-dot\"></span><span class=\"typing-dot\"></span><span class=\"typing-dot\"></span>\
|
||||
</span>\
|
||||
</div>\
|
||||
<form id=\"chat-form\" \
|
||||
hx-post=\"/send/{session_id}\" \
|
||||
hx-target=\"#messages\" \
|
||||
hx-swap=\"beforeend\" \
|
||||
hx-on=\"htmx:afterRequest: this.reset(); scrollMessages()\">\
|
||||
<input id=\"chat-input\" type=\"text\" name=\"content\" placeholder=\"Type your message...\" autocomplete=\"off\" maxlength=\"1000\" oninput=\"handleUserTypingInput()\" required />\
|
||||
<button type=\"submit\">Send</button>\
|
||||
</form>\
|
||||
</div>\
|
||||
<script>document.getElementById('start-form').style.display='none'; window.__sessionId='{session_id}';</script>"
|
||||
);
|
||||
Html(html)
|
||||
Json(CreateSessionResponse { session_id })
|
||||
}
|
||||
|
||||
+3
-1
@@ -39,6 +39,8 @@ async fn main() {
|
||||
.route("/user", get(handlers::pages::user_page))
|
||||
.route("/supporter", get(handlers::pages::supporter_page))
|
||||
.route("/reset-countdown", get(handlers::pages::reset_countdown))
|
||||
// New API endpoint for creating sessions without a form
|
||||
.route("/api/session/create", post(handlers::chat::create_session))
|
||||
.route("/ws/user/:session_id", get(handlers::ws::user_ws))
|
||||
.route(
|
||||
"/ws/supporter/:supporter_id",
|
||||
@@ -55,7 +57,7 @@ async fn main() {
|
||||
post(handlers::chat::supporter_send_message),
|
||||
)
|
||||
.route("/sessions", get(handlers::chat::get_sessions))
|
||||
.route("/sessions/new", post(handlers::chat::new_session))
|
||||
// Removed: .route("/sessions/new", post(handlers::chat::new_session))
|
||||
.nest_service("/static", ServeDir::new("static"))
|
||||
.with_state(state);
|
||||
|
||||
|
||||
+3
-3
@@ -60,7 +60,7 @@ pub struct SendMessageForm {
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct NewSessionForm {
|
||||
pub name: String,
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct CreateSessionResponse {
|
||||
pub session_id: String,
|
||||
}
|
||||
|
||||
+2
-4
@@ -7,7 +7,8 @@ use std::{
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
pub const RESET_EVENT: &str = "__reset__";
|
||||
pub const CHAT_RESET_INTERVAL: Duration = Duration::from_secs(3 * 60);
|
||||
// Changed from 3 minutes to 20 minutes
|
||||
pub const CHAT_RESET_INTERVAL: Duration = Duration::from_secs(20 * 60);
|
||||
pub const RATE_LIMIT_WINDOW: Duration = CHAT_RESET_INTERVAL;
|
||||
pub const MESSAGE_LIMIT_PER_WINDOW: u32 = 60;
|
||||
pub const SESSION_LIMIT_PER_WINDOW: u32 = 10;
|
||||
@@ -20,11 +21,8 @@ pub struct RateLimitEntry {
|
||||
|
||||
pub struct AppState {
|
||||
pub sessions: DashMap<String, ChatSession>,
|
||||
// broadcast channel: session_id -> sender
|
||||
pub notifiers: DashMap<String, broadcast::Sender<String>>,
|
||||
// supporter broadcast for new sessions and updates
|
||||
pub supporter_notifier: broadcast::Sender<String>,
|
||||
// in-memory rate limits, wiped with all other state every three minutes
|
||||
pub rate_limits: DashMap<String, RateLimitEntry>,
|
||||
reset_started: Mutex<Instant>,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user