2025-11-20 13:28:35 -03:00
|
|
|
use axum::{
|
2025-11-21 23:23:53 -03:00
|
|
|
extract::{Path, State},
|
2025-11-20 13:28:35 -03:00
|
|
|
http::StatusCode,
|
2026-01-04 08:48:27 -03:00
|
|
|
response::{Html, IntoResponse, Json},
|
2025-11-22 12:26:16 -03:00
|
|
|
routing::{get, post},
|
|
|
|
|
Router,
|
2025-11-20 13:28:35 -03:00
|
|
|
};
|
2025-10-18 12:03:07 -03:00
|
|
|
use log::{error, info};
|
2025-11-27 23:10:43 -03:00
|
|
|
use serde::Deserialize;
|
2025-11-20 13:28:35 -03:00
|
|
|
use serde_json::Value;
|
|
|
|
|
use std::sync::Arc;
|
|
|
|
|
|
2025-11-29 16:29:28 -03:00
|
|
|
use crate::core::urls::ApiUrls;
|
2025-10-18 12:03:07 -03:00
|
|
|
use crate::shared::state::AppState;
|
2025-11-20 13:28:35 -03:00
|
|
|
|
2025-11-22 13:24:53 -03:00
|
|
|
pub mod conversations;
|
2025-11-21 23:23:53 -03:00
|
|
|
pub mod service;
|
|
|
|
|
use service::{DefaultTranscriptionService, MeetingService};
|
|
|
|
|
|
2025-11-22 12:26:16 -03:00
|
|
|
pub fn configure() -> Router<Arc<AppState>> {
|
|
|
|
|
Router::new()
|
2025-11-29 16:29:28 -03:00
|
|
|
.route(ApiUrls::VOICE_START, post(voice_start))
|
|
|
|
|
.route(ApiUrls::VOICE_STOP, post(voice_stop))
|
|
|
|
|
.route(ApiUrls::MEET_CREATE, post(create_meeting))
|
|
|
|
|
.route(ApiUrls::MEET_ROOMS, get(list_rooms))
|
2025-12-28 11:50:50 -03:00
|
|
|
.route(ApiUrls::MEET_PARTICIPANTS, get(all_participants))
|
|
|
|
|
.route(ApiUrls::MEET_RECENT, get(recent_meetings))
|
|
|
|
|
.route(ApiUrls::MEET_SCHEDULED, get(scheduled_meetings))
|
2025-11-22 12:26:16 -03:00
|
|
|
.route(
|
2025-12-21 23:40:43 -03:00
|
|
|
&ApiUrls::MEET_ROOM_BY_ID.replace(":id", "{room_id}"),
|
2025-11-29 16:29:28 -03:00
|
|
|
get(get_room),
|
|
|
|
|
)
|
|
|
|
|
.route(
|
2025-12-21 23:40:43 -03:00
|
|
|
&ApiUrls::MEET_JOIN.replace(":id", "{room_id}"),
|
2025-11-29 16:29:28 -03:00
|
|
|
post(join_room),
|
|
|
|
|
)
|
|
|
|
|
.route(
|
2025-12-21 23:40:43 -03:00
|
|
|
&ApiUrls::MEET_TRANSCRIPTION.replace(":id", "{room_id}"),
|
2025-11-22 12:26:16 -03:00
|
|
|
post(start_transcription),
|
|
|
|
|
)
|
2025-11-29 16:29:28 -03:00
|
|
|
.route(ApiUrls::MEET_TOKEN, post(get_meeting_token))
|
|
|
|
|
.route(ApiUrls::MEET_INVITE, post(send_meeting_invites))
|
|
|
|
|
.route(ApiUrls::WS_MEET, get(meeting_websocket))
|
2025-11-22 13:24:53 -03:00
|
|
|
.route(
|
|
|
|
|
"/conversations/create",
|
|
|
|
|
post(conversations::create_conversation),
|
|
|
|
|
)
|
|
|
|
|
.route(
|
Add .env.example with comprehensive configuration template
The commit adds a complete example environment configuration file
documenting all available settings for BotServer, including logging,
database, server, drive, LLM, Redis, email, and feature flags.
Also removes hardcoded environment variable usage throughout the
codebase, replacing them with configuration via config.csv or
appropriate defaults. This includes:
- WhatsApp, Teams, Instagram adapter configurations
- Weather API key handling
- Email and directory service configurations
- Console feature conditionally compiles monitoring code
- Improved logging configuration with library suppression
2025-11-28 13:19:03 -03:00
|
|
|
"/conversations/{id}/join",
|
2025-11-22 13:24:53 -03:00
|
|
|
post(conversations::join_conversation),
|
|
|
|
|
)
|
|
|
|
|
.route(
|
|
|
|
|
"/conversations/:id/leave",
|
|
|
|
|
post(conversations::leave_conversation),
|
|
|
|
|
)
|
|
|
|
|
.route(
|
|
|
|
|
"/conversations/:id/members",
|
|
|
|
|
get(conversations::get_conversation_members),
|
|
|
|
|
)
|
|
|
|
|
.route(
|
|
|
|
|
"/conversations/:id/messages",
|
|
|
|
|
get(conversations::get_conversation_messages),
|
|
|
|
|
)
|
|
|
|
|
.route(
|
|
|
|
|
"/conversations/:id/messages/send",
|
|
|
|
|
post(conversations::send_message),
|
|
|
|
|
)
|
|
|
|
|
.route(
|
|
|
|
|
"/conversations/:id/messages/:message_id/edit",
|
|
|
|
|
post(conversations::edit_message),
|
|
|
|
|
)
|
|
|
|
|
.route(
|
|
|
|
|
"/conversations/:id/messages/:message_id/delete",
|
|
|
|
|
post(conversations::delete_message),
|
|
|
|
|
)
|
|
|
|
|
.route(
|
|
|
|
|
"/conversations/:id/messages/:message_id/react",
|
|
|
|
|
post(conversations::react_to_message),
|
|
|
|
|
)
|
|
|
|
|
.route(
|
|
|
|
|
"/conversations/:id/messages/:message_id/pin",
|
|
|
|
|
post(conversations::pin_message),
|
|
|
|
|
)
|
|
|
|
|
.route(
|
|
|
|
|
"/conversations/:id/messages/search",
|
|
|
|
|
get(conversations::search_messages),
|
|
|
|
|
)
|
|
|
|
|
.route(
|
|
|
|
|
"/conversations/:id/calls/start",
|
|
|
|
|
post(conversations::start_call),
|
|
|
|
|
)
|
|
|
|
|
.route(
|
|
|
|
|
"/conversations/:id/calls/join",
|
|
|
|
|
post(conversations::join_call),
|
|
|
|
|
)
|
|
|
|
|
.route(
|
|
|
|
|
"/conversations/:id/calls/leave",
|
|
|
|
|
post(conversations::leave_call),
|
|
|
|
|
)
|
|
|
|
|
.route(
|
|
|
|
|
"/conversations/:id/calls/mute",
|
|
|
|
|
post(conversations::mute_call),
|
|
|
|
|
)
|
|
|
|
|
.route(
|
|
|
|
|
"/conversations/:id/calls/unmute",
|
|
|
|
|
post(conversations::unmute_call),
|
|
|
|
|
)
|
|
|
|
|
.route(
|
|
|
|
|
"/conversations/:id/screen/share",
|
|
|
|
|
post(conversations::start_screen_share),
|
|
|
|
|
)
|
|
|
|
|
.route(
|
|
|
|
|
"/conversations/:id/screen/stop",
|
|
|
|
|
post(conversations::stop_screen_share),
|
|
|
|
|
)
|
|
|
|
|
.route(
|
|
|
|
|
"/conversations/:id/recording/start",
|
|
|
|
|
post(conversations::start_recording),
|
|
|
|
|
)
|
|
|
|
|
.route(
|
|
|
|
|
"/conversations/:id/recording/stop",
|
|
|
|
|
post(conversations::stop_recording),
|
|
|
|
|
)
|
|
|
|
|
.route(
|
|
|
|
|
"/conversations/:id/whiteboard/create",
|
|
|
|
|
post(conversations::create_whiteboard),
|
|
|
|
|
)
|
|
|
|
|
.route(
|
|
|
|
|
"/conversations/:id/whiteboard/collaborate",
|
|
|
|
|
post(conversations::collaborate_whiteboard),
|
|
|
|
|
)
|
2025-11-22 12:26:16 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
|
|
|
pub struct CreateMeetingRequest {
|
|
|
|
|
pub name: String,
|
|
|
|
|
pub created_by: String,
|
|
|
|
|
pub settings: Option<service::MeetingSettings>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
|
|
|
pub struct JoinRoomRequest {
|
|
|
|
|
pub participant_name: String,
|
|
|
|
|
pub participant_id: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
|
|
|
pub struct GetTokenRequest {
|
|
|
|
|
pub room_id: String,
|
|
|
|
|
pub user_id: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
|
|
|
pub struct SendInvitesRequest {
|
|
|
|
|
pub room_id: String,
|
|
|
|
|
pub emails: Vec<String>,
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-20 13:28:35 -03:00
|
|
|
pub async fn voice_start(
|
|
|
|
|
State(data): State<Arc<AppState>>,
|
|
|
|
|
Json(info): Json<Value>,
|
|
|
|
|
) -> impl IntoResponse {
|
2025-10-18 12:03:07 -03:00
|
|
|
let session_id = info
|
|
|
|
|
.get("session_id")
|
|
|
|
|
.and_then(|s| s.as_str())
|
|
|
|
|
.unwrap_or("");
|
|
|
|
|
let user_id = info
|
|
|
|
|
.get("user_id")
|
|
|
|
|
.and_then(|u| u.as_str())
|
|
|
|
|
.unwrap_or("user");
|
2025-11-20 13:28:35 -03:00
|
|
|
|
2025-10-18 12:03:07 -03:00
|
|
|
info!(
|
|
|
|
|
"Voice session start request - session: {}, user: {}",
|
|
|
|
|
session_id, user_id
|
|
|
|
|
);
|
2025-11-20 13:28:35 -03:00
|
|
|
|
2025-10-18 12:03:07 -03:00
|
|
|
match data
|
|
|
|
|
.voice_adapter
|
|
|
|
|
.start_voice_session(session_id, user_id)
|
|
|
|
|
.await
|
|
|
|
|
{
|
|
|
|
|
Ok(token) => {
|
|
|
|
|
info!(
|
2025-12-28 11:50:50 -03:00
|
|
|
"Voice session started successfully for session {session_id}"
|
2025-10-18 12:03:07 -03:00
|
|
|
);
|
2025-11-20 13:28:35 -03:00
|
|
|
(
|
|
|
|
|
StatusCode::OK,
|
|
|
|
|
Json(serde_json::json!({"token": token, "status": "started"})),
|
|
|
|
|
)
|
2025-10-18 12:03:07 -03:00
|
|
|
}
|
|
|
|
|
Err(e) => {
|
|
|
|
|
error!(
|
2025-12-28 11:50:50 -03:00
|
|
|
"Failed to start voice session for session {session_id}: {e}"
|
2025-10-18 12:03:07 -03:00
|
|
|
);
|
2025-11-20 13:28:35 -03:00
|
|
|
(
|
|
|
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
|
|
|
Json(serde_json::json!({"error": e.to_string()})),
|
|
|
|
|
)
|
2025-10-18 12:03:07 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-11-20 13:28:35 -03:00
|
|
|
|
|
|
|
|
pub async fn voice_stop(
|
|
|
|
|
State(data): State<Arc<AppState>>,
|
|
|
|
|
Json(info): Json<Value>,
|
|
|
|
|
) -> impl IntoResponse {
|
2025-10-18 12:03:07 -03:00
|
|
|
let session_id = info
|
|
|
|
|
.get("session_id")
|
|
|
|
|
.and_then(|s| s.as_str())
|
|
|
|
|
.unwrap_or("");
|
2025-11-20 13:28:35 -03:00
|
|
|
|
2025-10-18 12:03:07 -03:00
|
|
|
match data.voice_adapter.stop_voice_session(session_id).await {
|
|
|
|
|
Ok(()) => {
|
|
|
|
|
info!(
|
2025-12-28 11:50:50 -03:00
|
|
|
"Voice session stopped successfully for session {session_id}"
|
2025-10-18 12:03:07 -03:00
|
|
|
);
|
2025-11-20 13:28:35 -03:00
|
|
|
(
|
|
|
|
|
StatusCode::OK,
|
|
|
|
|
Json(serde_json::json!({"status": "stopped"})),
|
|
|
|
|
)
|
2025-10-18 12:03:07 -03:00
|
|
|
}
|
|
|
|
|
Err(e) => {
|
|
|
|
|
error!(
|
2025-12-28 11:50:50 -03:00
|
|
|
"Failed to stop voice session for session {session_id}: {e}"
|
2025-10-18 12:03:07 -03:00
|
|
|
);
|
2025-11-20 13:28:35 -03:00
|
|
|
(
|
|
|
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
|
|
|
Json(serde_json::json!({"error": e.to_string()})),
|
|
|
|
|
)
|
2025-10-18 12:03:07 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-11-21 23:23:53 -03:00
|
|
|
|
|
|
|
|
pub async fn create_meeting(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
Json(payload): Json<CreateMeetingRequest>,
|
|
|
|
|
) -> impl IntoResponse {
|
|
|
|
|
let transcription_service = Arc::new(DefaultTranscriptionService);
|
|
|
|
|
let meeting_service = MeetingService::new(state.clone(), transcription_service);
|
|
|
|
|
|
|
|
|
|
match meeting_service
|
|
|
|
|
.create_room(payload.name, payload.created_by, payload.settings)
|
|
|
|
|
.await
|
|
|
|
|
{
|
|
|
|
|
Ok(room) => {
|
|
|
|
|
info!("Created meeting room: {}", room.id);
|
|
|
|
|
(StatusCode::OK, Json(serde_json::json!(room)))
|
|
|
|
|
}
|
|
|
|
|
Err(e) => {
|
2025-12-28 11:50:50 -03:00
|
|
|
error!("Failed to create meeting room: {e}");
|
2025-11-21 23:23:53 -03:00
|
|
|
(
|
|
|
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
|
|
|
Json(serde_json::json!({"error": e.to_string()})),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-04 08:48:27 -03:00
|
|
|
pub async fn list_rooms(State(state): State<Arc<AppState>>) -> Html<String> {
|
2025-11-21 23:23:53 -03:00
|
|
|
let transcription_service = Arc::new(DefaultTranscriptionService);
|
|
|
|
|
let meeting_service = MeetingService::new(state.clone(), transcription_service);
|
|
|
|
|
|
|
|
|
|
let rooms = meeting_service.rooms.read().await;
|
|
|
|
|
|
2026-01-04 08:48:27 -03:00
|
|
|
if rooms.is_empty() {
|
|
|
|
|
return Html(r##"<div class="empty-state">
|
|
|
|
|
<div class="empty-icon">📹</div>
|
|
|
|
|
<p>No active rooms</p>
|
|
|
|
|
<p class="empty-hint">Create a new meeting to get started</p>
|
|
|
|
|
</div>"##.to_string());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut html = String::new();
|
|
|
|
|
for room in rooms.values() {
|
|
|
|
|
let participant_count = room.participants.len();
|
|
|
|
|
html.push_str(&format!(
|
|
|
|
|
r##"<div class="room-card" data-room-id="{id}">
|
|
|
|
|
<div class="room-icon">📹</div>
|
|
|
|
|
<div class="room-info">
|
|
|
|
|
<h3 class="room-name">{name}</h3>
|
|
|
|
|
<span class="room-participants">{count} participant(s)</span>
|
|
|
|
|
</div>
|
|
|
|
|
<button class="btn-join" hx-post="/api/meet/rooms/{id}/join" hx-target="#meeting-room" hx-swap="outerHTML">Join</button>
|
|
|
|
|
</div>"##,
|
|
|
|
|
id = room.id,
|
|
|
|
|
name = room.name,
|
|
|
|
|
count = participant_count,
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Html(html)
|
2025-11-21 23:23:53 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn get_room(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
Path(room_id): Path<String>,
|
|
|
|
|
) -> impl IntoResponse {
|
|
|
|
|
let transcription_service = Arc::new(DefaultTranscriptionService);
|
|
|
|
|
let meeting_service = MeetingService::new(state.clone(), transcription_service);
|
|
|
|
|
|
|
|
|
|
let rooms = meeting_service.rooms.read().await;
|
|
|
|
|
match rooms.get(&room_id) {
|
|
|
|
|
Some(room) => (StatusCode::OK, Json(serde_json::json!(room))),
|
|
|
|
|
None => (
|
|
|
|
|
StatusCode::NOT_FOUND,
|
|
|
|
|
Json(serde_json::json!({"error": "Room not found"})),
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn join_room(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
Path(room_id): Path<String>,
|
|
|
|
|
Json(payload): Json<JoinRoomRequest>,
|
|
|
|
|
) -> impl IntoResponse {
|
|
|
|
|
let transcription_service = Arc::new(DefaultTranscriptionService);
|
|
|
|
|
let meeting_service = MeetingService::new(state.clone(), transcription_service);
|
|
|
|
|
|
|
|
|
|
match meeting_service
|
|
|
|
|
.join_room(&room_id, payload.participant_name, payload.participant_id)
|
|
|
|
|
.await
|
|
|
|
|
{
|
|
|
|
|
Ok(participant) => {
|
2025-12-28 11:50:50 -03:00
|
|
|
info!("Participant {} joined room {room_id}", participant.id);
|
2025-11-21 23:23:53 -03:00
|
|
|
(StatusCode::OK, Json(serde_json::json!(participant)))
|
|
|
|
|
}
|
|
|
|
|
Err(e) => {
|
2025-12-28 11:50:50 -03:00
|
|
|
error!("Failed to join room {room_id}: {e}");
|
2025-11-21 23:23:53 -03:00
|
|
|
(
|
|
|
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
|
|
|
Json(serde_json::json!({"error": e.to_string()})),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn start_transcription(
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
Path(room_id): Path<String>,
|
|
|
|
|
) -> impl IntoResponse {
|
|
|
|
|
let transcription_service = Arc::new(DefaultTranscriptionService);
|
|
|
|
|
let meeting_service = MeetingService::new(state.clone(), transcription_service);
|
|
|
|
|
|
|
|
|
|
match meeting_service.start_transcription(&room_id).await {
|
2025-12-28 11:50:50 -03:00
|
|
|
Ok(()) => {
|
|
|
|
|
info!("Started transcription for room {room_id}");
|
2025-11-21 23:23:53 -03:00
|
|
|
(
|
|
|
|
|
StatusCode::OK,
|
|
|
|
|
Json(serde_json::json!({"status": "transcription_started"})),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
Err(e) => {
|
2025-12-28 11:50:50 -03:00
|
|
|
error!("Failed to start transcription for room {room_id}: {e}");
|
2025-11-21 23:23:53 -03:00
|
|
|
(
|
|
|
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
|
|
|
Json(serde_json::json!({"error": e.to_string()})),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn get_meeting_token(
|
|
|
|
|
State(_state): State<Arc<AppState>>,
|
|
|
|
|
Json(payload): Json<GetTokenRequest>,
|
|
|
|
|
) -> impl IntoResponse {
|
|
|
|
|
let token = format!(
|
|
|
|
|
"meet_token_{}_{}_{}",
|
|
|
|
|
payload.room_id,
|
|
|
|
|
payload.user_id,
|
|
|
|
|
uuid::Uuid::new_v4()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
(
|
|
|
|
|
StatusCode::OK,
|
|
|
|
|
Json(serde_json::json!({
|
|
|
|
|
"token": token,
|
|
|
|
|
"room_id": payload.room_id,
|
|
|
|
|
"user_id": payload.user_id
|
|
|
|
|
})),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn send_meeting_invites(
|
|
|
|
|
State(_state): State<Arc<AppState>>,
|
|
|
|
|
Json(payload): Json<SendInvitesRequest>,
|
|
|
|
|
) -> impl IntoResponse {
|
|
|
|
|
info!("Sending meeting invites for room {}", payload.room_id);
|
2025-12-23 18:40:58 -03:00
|
|
|
|
2025-11-21 23:23:53 -03:00
|
|
|
(
|
|
|
|
|
StatusCode::OK,
|
|
|
|
|
Json(serde_json::json!({
|
|
|
|
|
"status": "invites_sent",
|
|
|
|
|
"recipients": payload.emails
|
|
|
|
|
})),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn meeting_websocket(
|
|
|
|
|
ws: axum::extract::ws::WebSocketUpgrade,
|
|
|
|
|
State(state): State<Arc<AppState>>,
|
|
|
|
|
) -> impl IntoResponse {
|
|
|
|
|
ws.on_upgrade(|socket| handle_meeting_socket(socket, state))
|
|
|
|
|
}
|
|
|
|
|
|
feat(autotask): Implement AutoTask system with intent classification and app generation
- Add IntentClassifier with 7 intent types (APP_CREATE, TODO, MONITOR, ACTION, SCHEDULE, GOAL, TOOL)
- Add AppGenerator with LLM-powered app structure analysis
- Add DesignerAI for modifying apps through conversation
- Add app_server for serving generated apps with clean URLs
- Add db_api for CRUD operations on bot database tables
- Add ask_later keyword for pending info collection
- Add migration 6.1.1 with tables: pending_info, auto_tasks, execution_plans, task_approvals, task_decisions, safety_audit_log, generated_apps, intent_classifications, designer_changes
- Write apps to S3 drive and sync to SITE_ROOT for serving
- Clean URL structure: /apps/{app_name}/
- Integrate with DriveMonitor for file sync
Based on Chapter 17 - Autonomous Tasks specification
2025-12-27 21:10:09 -03:00
|
|
|
#[allow(clippy::unused_async)]
|
2025-11-21 23:23:53 -03:00
|
|
|
async fn handle_meeting_socket(_socket: axum::extract::ws::WebSocket, _state: Arc<AppState>) {
|
|
|
|
|
info!("Meeting WebSocket connection established");
|
|
|
|
|
}
|
2025-12-10 23:50:06 -03:00
|
|
|
|
2026-01-04 08:48:27 -03:00
|
|
|
pub async fn all_participants(State(_state): State<Arc<AppState>>) -> Html<String> {
|
|
|
|
|
Html(r##"<div class="empty-state">
|
|
|
|
|
<p>No participants</p>
|
|
|
|
|
</div>"##.to_string())
|
2025-12-10 23:50:06 -03:00
|
|
|
}
|
|
|
|
|
|
2026-01-04 08:48:27 -03:00
|
|
|
pub async fn recent_meetings(State(_state): State<Arc<AppState>>) -> Html<String> {
|
|
|
|
|
Html(r##"<div class="empty-state">
|
|
|
|
|
<div class="empty-icon">📋</div>
|
|
|
|
|
<p>No recent meetings</p>
|
|
|
|
|
</div>"##.to_string())
|
2025-12-10 23:50:06 -03:00
|
|
|
}
|
|
|
|
|
|
2026-01-04 08:48:27 -03:00
|
|
|
pub async fn scheduled_meetings(State(_state): State<Arc<AppState>>) -> Html<String> {
|
|
|
|
|
Html(r##"<div class="empty-state">
|
|
|
|
|
<div class="empty-icon">📅</div>
|
|
|
|
|
<p>No scheduled meetings</p>
|
|
|
|
|
</div>"##.to_string())
|
2025-12-10 23:50:06 -03:00
|
|
|
}
|