use axum::http::header::{CACHE_CONTROL, CONTENT_TYPE};
use axum::http::HeaderValue;
use axum::response::IntoResponse;
const PLAYGROUND_HTML: &str = include_str!("../assets/playground.html");
const PLAYGROUND_MARKER: &str = "openkind playground";
pub async fn playground_page() -> impl IntoResponse {
debug_assert!(PLAYGROUND_HTML.contains(PLAYGROUND_MARKER));
(
[
(
CONTENT_TYPE,
HeaderValue::from_static("text/html; charset=utf-8"),
),
(CACHE_CONTROL, HeaderValue::from_static("no-store")),
(axum::http::header::CONTENT_SECURITY_POLICY, HeaderValue::from_static("default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self'; img-src 'self' data:; base-uri 'none'; frame-ancestors 'none'; form-action 'none'")),
(axum::http::header::X_CONTENT_TYPE_OPTIONS, HeaderValue::from_static("nosniff")),
],
PLAYGROUND_HTML,
)
}
#[derive(serde::Serialize)]
pub struct PlaygroundModel {
pub name: String,
pub description: String,
pub source: String,
pub loaded: bool,
pub manageable: bool,
}
#[async_trait::async_trait]
pub trait PlaygroundModels: Send + Sync {
async fn list(&self) -> Result<Vec<PlaygroundModel>, crate::ApiError>;
async fn set_loaded(&self, name: String, loaded: bool) -> Result<(), crate::ApiError>;
}
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct ModelAction {
name: String,
loaded: bool,
}
pub(crate) async fn list_models(
axum::extract::State(state): axum::extract::State<std::sync::Arc<crate::AppState>>,
) -> Result<impl IntoResponse, crate::ApiError> {
let manager = state.playground_models.as_ref().ok_or_else(|| {
crate::ApiError::InvalidBody("Model controls are unavailable on this daemon".into())
})?;
Ok((
[(CACHE_CONTROL, "no-store")],
axum::Json(serde_json::json!({"models": manager.list().await?})),
))
}
pub(crate) async fn change_model(
axum::extract::State(state): axum::extract::State<std::sync::Arc<crate::AppState>>,
headers: axum::http::HeaderMap,
axum::Json(action): axum::Json<ModelAction>,
) -> Result<impl IntoResponse, crate::ApiError> {
if headers
.get("x-openkind-playground")
.and_then(|v| v.to_str().ok())
!= Some("1")
|| headers
.get("sec-fetch-site")
.is_some_and(|v| v == "cross-site")
{
return Err(crate::ApiError::Unauthorized);
}
let manager = state.playground_models.as_ref().ok_or_else(|| {
crate::ApiError::InvalidBody("Model controls are unavailable on this daemon".into())
})?;
manager.set_loaded(action.name, action.loaded).await?;
Ok((
[(CACHE_CONTROL, "no-store")],
axum::Json(serde_json::json!({"ok": true})),
))
}