use actix::Addr;
use actix_web::middleware::Logger;
use actix_web::web::Data;
use actix_web::{get, web, App, HttpRequest, HttpServer, Responder};
use actix_web::{post, HttpResponse, Result};
use actix_web_actors::ws;
use colored::Colorize;
use pax_generation::{AIModel, PaxAppGenerator};
use serde_json::json;
use std::net::TcpListener;
use std::{env, fs};
use env_logger;
use std::io::Write;
use crate::helpers::PAX_BADGE;
use crate::{RunContext, RunTarget};
use notify::{Error, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
use pax_manifest::PaxManifest;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::{Arc, Mutex};
use std::time::{SystemTime, UNIX_EPOCH};
use websocket::PrivilegedAgentWebSocket;
use websocket::SocketMessageAccumulator;
#[allow(unused)]
mod llm;
pub mod static_server;
pub mod websocket;
pub struct AppState {
serve_dir: Mutex<PathBuf>,
userland_project_root: Mutex<PathBuf>,
active_websocket_client: Mutex<Option<Addr<PrivilegedAgentWebSocket>>>,
request_id_counter: Mutex<usize>,
manifest: Mutex<Option<PaxManifest>>,
last_written_timestamp: Mutex<SystemTime>,
}
impl AppState {
pub fn new_empty() -> Self {
Self {
serve_dir: Mutex::new(PathBuf::new()),
userland_project_root: Mutex::new(PathBuf::new()),
active_websocket_client: Mutex::new(None),
request_id_counter: Mutex::new(0),
manifest: Mutex::new(None),
last_written_timestamp: Mutex::new(UNIX_EPOCH),
}
}
pub fn new(serve_dir: PathBuf, project_root: PathBuf, manifest: PaxManifest) -> Self {
AppState {
serve_dir: Mutex::new(serve_dir),
userland_project_root: Mutex::new(project_root),
active_websocket_client: Mutex::new(None),
request_id_counter: Mutex::new(0),
manifest: Mutex::new(Some(manifest)),
last_written_timestamp: Mutex::new(SystemTime::now()),
}
}
fn generate_request_id(&self) -> usize {
let mut counter = self.request_id_counter.lock().unwrap();
*counter += 1;
*counter
}
pub fn update_last_written_timestamp(&self) {
let mut last_written = self.last_written_timestamp.lock().unwrap();
*last_written = SystemTime::now();
}
}
#[get("/ws")]
pub async fn web_socket(
req: HttpRequest,
stream: web::Payload,
state: web::Data<AppState>,
) -> impl Responder {
ws::WsResponseBuilder::new(PrivilegedAgentWebSocket::new(state), &req, stream)
.frame_size(2_000_000)
.start()
}
#[allow(unused_assignments)]
pub fn start_server(
static_file_path: &str,
src_folder_to_watch: &str,
manifest: PaxManifest,
) -> std::io::Result<()> {
std::env::set_var("RUST_LOG", "actix_web=info");
env_logger::Builder::from_env(env_logger::Env::default())
.format(|buf, record| writeln!(buf, "{} 🍱 Served {}", *PAX_BADGE, record.args()))
.init();
let initial_state = AppState::new(
PathBuf::from(static_file_path),
PathBuf::from_str(src_folder_to_watch).unwrap(),
manifest,
);
let fs_path = initial_state.serve_dir.lock().unwrap().clone();
let state = Data::new(initial_state);
let _watcher = setup_file_watcher(state.clone(), src_folder_to_watch)
.expect("Failed to setup file watcher");
let runtime = actix_web::rt::System::new().block_on(async {
let mut port = 8080;
let server = loop {
if TcpListener::bind(("127.0.0.1", port)).is_ok() {
println!(
"{} 🗂️ Serving static files from {}",
*PAX_BADGE,
&fs_path.to_str().unwrap()
);
let address_msg = format!("http://127.0.0.1:{}", port).blue();
let server_running_at_msg = format!("Server running at {}", address_msg).bold();
println!("{} 📠 {}", *PAX_BADGE, server_running_at_msg);
break HttpServer::new(move || {
App::new()
.wrap(Logger::new("| %s | %U"))
.app_data(state.clone())
.service(ai_page)
.service(ai_submit)
.service(web_socket)
.service(
actix_files::Files::new("/*", fs_path.clone()).index_file("index.html"),
)
})
.bind(("127.0.0.1", port))
.expect("Error binding to address")
.workers(2);
} else {
port += 1; }
};
server.run().await
});
runtime
}
#[derive(Default)]
pub enum FileContent {
Pax(String),
Rust(String),
#[default]
Unknown,
}
#[derive(Default)]
struct WatcherFileChanged {
pub contents: FileContent,
pub path: String,
}
impl actix::Message for WatcherFileChanged {
type Result = ();
}
pub fn setup_file_watcher(state: Data<AppState>, path: &str) -> Result<RecommendedWatcher, Error> {
let mut watcher = RecommendedWatcher::new(
move |res: Result<Event, Error>| match res {
Ok(e) => {
if let Some(addr) = &*state.active_websocket_client.lock().unwrap() {
let now = SystemTime::now();
let last_written = *state.last_written_timestamp.lock().unwrap();
if now
.duration_since(last_written)
.unwrap_or_default()
.as_millis()
> 1000
{
if let EventKind::Modify(_) = e.kind {
if let Some(path) = e.paths.first() {
match fs::read_to_string(path) {
Ok(contents) => {
let extension = path.extension();
let msg = WatcherFileChanged {
contents: match extension.and_then(|e| e.to_str()) {
Some("pax") => FileContent::Pax(contents),
Some("rs") => FileContent::Rust(contents),
_ => FileContent::Unknown,
},
path: path.to_str().unwrap().to_string(),
};
addr.do_send(msg);
state.update_last_written_timestamp();
}
Err(_) => (),
}
}
}
}
}
}
Err(e) => {
println!("File system watch error: {:?}", e);
}
},
Default::default(),
)?;
watcher.watch(Path::new(path), RecursiveMode::Recursive)?;
Ok(watcher)
}
#[get("/ai")]
async fn ai_page() -> Result<HttpResponse> {
let html_content = fs::read_to_string("static/ai_chat.html")?;
Ok(HttpResponse::Ok()
.content_type("text/html")
.body(html_content))
}
#[post("/ai")]
async fn ai_submit(message: web::Json<AiMessage>, state: web::Data<AppState>) -> HttpResponse {
let userland_project_root = state.userland_project_root.lock().unwrap().clone();
let claude_api_key = match env::var("ANTHROPIC_API_KEY") {
Ok(key) => key,
Err(_) => {
return HttpResponse::InternalServerError().json(json!({
"status": "error",
"message": "ANTHROPIC_API_KEY not set in environment"
}))
}
};
let pax_app_generator = PaxAppGenerator::new(claude_api_key, AIModel::Claude3);
let output = userland_project_root.clone().join("src");
match pax_app_generator
.generate_app(&message.message, Some(&output), true)
.await
{
Ok(_) => {
match perform_build_and_update_state(&state, userland_project_root.to_str().unwrap()) {
Ok(_) => HttpResponse::Ok().json(json!({
"status": "success",
"response": "App generated and built successfully.",
})),
Err(e) => {
println!("Error performing build and updating state: {:?}", e);
HttpResponse::InternalServerError().json(json!({
"status": "error",
"message": "Failed to build the generated app"
}))
}
}
}
Err(e) => {
println!("Error generating app: {:?}", e);
HttpResponse::InternalServerError().json(json!({
"status": "error",
"message": "Failed to generate app"
}))
}
}
}
#[derive(Deserialize)]
struct AiMessage {
message: String,
}
fn create_designer_run_context() -> RunContext {
RunContext {
target: RunTarget::Web,
project_path: PathBuf::from("../pax-designer".to_string()),
verbose: false,
should_also_run: false,
is_libdev_mode: true,
should_run_designer: true,
process_child_ids: Arc::new(Mutex::new(vec![])),
is_release: false,
}
}
fn perform_build() -> std::io::Result<(PaxManifest, Option<PathBuf>)> {
let ctx = create_designer_run_context();
crate::perform_build(&ctx).map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
}
fn perform_build_and_update_state(state: &AppState, folder_to_watch: &str) -> std::io::Result<()> {
let (manifest, fs_path) = perform_build()?;
*state.serve_dir.lock().unwrap() = fs_path.expect("serve directory should exist");
*state.userland_project_root.lock().unwrap() = PathBuf::from_str(folder_to_watch).unwrap();
*state.manifest.lock().unwrap() = Some(manifest);
Ok(())
}