use actix_web::{web, HttpResponse, Responder};
use async_graphql::{
Context, EmptySubscription, FieldResult, InputObject, Object, Schema, SimpleObject, ID,
};
use async_graphql_actix_web::{GraphQLRequest, GraphQLResponse};
use chrono::{DateTime, Utc};
use std::sync::Arc;
use super::state::AppState;
pub type ChasmSchema = Schema<QueryRoot, MutationRoot, EmptySubscription>;
#[derive(SimpleObject, Clone)]
pub struct Workspace {
pub id: ID,
pub name: String,
pub path: String,
pub provider: String,
pub session_count: i32,
pub last_harvested: Option<DateTime<Utc>>,
pub created_at: DateTime<Utc>,
}
#[derive(SimpleObject, Clone)]
pub struct Session {
pub id: ID,
pub title: String,
pub workspace_id: Option<ID>,
pub provider: String,
pub model: Option<String>,
pub message_count: i32,
pub token_count: i32,
pub archived: bool,
pub tags: Vec<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(SimpleObject, Clone)]
pub struct Message {
pub id: ID,
pub session_id: ID,
pub role: String,
pub content: String,
pub model: Option<String>,
pub token_count: i32,
pub created_at: DateTime<Utc>,
}
#[derive(SimpleObject, Clone)]
pub struct Provider {
pub id: ID,
pub name: String,
pub provider_type: String,
pub enabled: bool,
pub base_url: Option<String>,
}
#[derive(SimpleObject, Clone)]
pub struct Agent {
pub id: ID,
pub name: String,
pub description: Option<String>,
pub system_prompt: Option<String>,
pub provider_id: Option<ID>,
pub model: String,
pub temperature: f64,
pub created_at: DateTime<Utc>,
}
#[derive(SimpleObject, Clone)]
pub struct StatsOverview {
pub total_sessions: i32,
pub total_messages: i32,
pub total_tokens: i64,
pub active_providers: i32,
pub workspaces: i32,
pub sessions_today: i32,
pub messages_today: i32,
}
#[derive(SimpleObject, Clone)]
pub struct SearchResult {
pub sessions: Vec<Session>,
pub messages: Vec<Message>,
pub total: i32,
}
#[derive(InputObject)]
pub struct CreateSessionInput {
pub title: String,
pub workspace_id: Option<ID>,
pub provider: String,
}
#[derive(InputObject)]
pub struct UpdateSessionInput {
pub title: Option<String>,
pub tags: Option<Vec<String>>,
pub archived: Option<bool>,
}
#[derive(InputObject)]
pub struct CreateAgentInput {
pub name: String,
pub description: Option<String>,
pub system_prompt: Option<String>,
pub provider_id: Option<ID>,
pub model: String,
pub temperature: Option<f64>,
}
#[derive(InputObject, Default)]
pub struct SessionFilter {
pub workspace_id: Option<ID>,
pub provider: Option<String>,
pub archived: Option<bool>,
pub search: Option<String>,
}
#[derive(InputObject, Default)]
pub struct Pagination {
pub limit: Option<i32>,
pub offset: Option<i32>,
}
pub struct QueryRoot;
#[Object]
impl QueryRoot {
async fn workspaces(
&self,
ctx: &Context<'_>,
pagination: Option<Pagination>,
) -> FieldResult<Vec<Workspace>> {
let _state = ctx.data::<Arc<AppState>>()?;
let pagination = pagination.unwrap_or_default();
let _limit = pagination.limit.unwrap_or(20);
let _offset = pagination.offset.unwrap_or(0);
Ok(vec![])
}
async fn workspace(&self, ctx: &Context<'_>, id: ID) -> FieldResult<Option<Workspace>> {
let _state = ctx.data::<Arc<AppState>>()?;
let _id = id.to_string();
Ok(None)
}
async fn sessions(
&self,
ctx: &Context<'_>,
filter: Option<SessionFilter>,
pagination: Option<Pagination>,
) -> FieldResult<Vec<Session>> {
let _state = ctx.data::<Arc<AppState>>()?;
let _filter = filter.unwrap_or_default();
let pagination = pagination.unwrap_or_default();
let _limit = pagination.limit.unwrap_or(20);
let _offset = pagination.offset.unwrap_or(0);
Ok(vec![])
}
async fn session(&self, ctx: &Context<'_>, id: ID) -> FieldResult<Option<Session>> {
let _state = ctx.data::<Arc<AppState>>()?;
let _id = id.to_string();
Ok(None)
}
async fn messages(
&self,
ctx: &Context<'_>,
session_id: ID,
pagination: Option<Pagination>,
) -> FieldResult<Vec<Message>> {
let _state = ctx.data::<Arc<AppState>>()?;
let _session_id = session_id.to_string();
let pagination = pagination.unwrap_or_default();
let _limit = pagination.limit.unwrap_or(100);
let _offset = pagination.offset.unwrap_or(0);
Ok(vec![])
}
async fn providers(&self, ctx: &Context<'_>) -> FieldResult<Vec<Provider>> {
let _state = ctx.data::<Arc<AppState>>()?;
Ok(vec![
Provider {
id: "copilot".into(),
name: "GitHub Copilot".to_string(),
provider_type: "copilot".to_string(),
enabled: true,
base_url: None,
},
Provider {
id: "cursor".into(),
name: "Cursor".to_string(),
provider_type: "cursor".to_string(),
enabled: true,
base_url: None,
},
Provider {
id: "chatgpt".into(),
name: "ChatGPT".to_string(),
provider_type: "chatgpt".to_string(),
enabled: true,
base_url: Some("https://chatgpt.com".to_string()),
},
])
}
async fn agents(&self, ctx: &Context<'_>) -> FieldResult<Vec<Agent>> {
let _state = ctx.data::<Arc<AppState>>()?;
Ok(vec![])
}
async fn agent(&self, ctx: &Context<'_>, id: ID) -> FieldResult<Option<Agent>> {
let _state = ctx.data::<Arc<AppState>>()?;
let _id = id.to_string();
Ok(None)
}
async fn stats(&self, ctx: &Context<'_>) -> FieldResult<StatsOverview> {
let _state = ctx.data::<Arc<AppState>>()?;
Ok(StatsOverview {
total_sessions: 0,
total_messages: 0,
total_tokens: 0,
active_providers: 3,
workspaces: 0,
sessions_today: 0,
messages_today: 0,
})
}
async fn search(
&self,
ctx: &Context<'_>,
query: String,
limit: Option<i32>,
) -> FieldResult<SearchResult> {
let _state = ctx.data::<Arc<AppState>>()?;
let _limit = limit.unwrap_or(20);
let _query = query;
Ok(SearchResult {
sessions: vec![],
messages: vec![],
total: 0,
})
}
}
pub struct MutationRoot;
#[Object]
impl MutationRoot {
async fn create_session(
&self,
ctx: &Context<'_>,
input: CreateSessionInput,
) -> FieldResult<Session> {
let _state = ctx.data::<Arc<AppState>>()?;
Ok(Session {
id: uuid::Uuid::new_v4().to_string().into(),
title: input.title,
workspace_id: input.workspace_id,
provider: input.provider,
model: None,
message_count: 0,
token_count: 0,
archived: false,
tags: vec![],
created_at: Utc::now(),
updated_at: Utc::now(),
})
}
async fn update_session(
&self,
ctx: &Context<'_>,
id: ID,
input: UpdateSessionInput,
) -> FieldResult<Session> {
let _state = ctx.data::<Arc<AppState>>()?;
let _id = id.to_string();
Ok(Session {
id,
title: input.title.unwrap_or_default(),
workspace_id: None,
provider: "unknown".to_string(),
model: None,
message_count: 0,
token_count: 0,
archived: input.archived.unwrap_or(false),
tags: input.tags.unwrap_or_default(),
created_at: Utc::now(),
updated_at: Utc::now(),
})
}
async fn delete_session(&self, ctx: &Context<'_>, id: ID) -> FieldResult<bool> {
let _state = ctx.data::<Arc<AppState>>()?;
let _id = id.to_string();
Ok(true)
}
async fn archive_session(&self, ctx: &Context<'_>, id: ID) -> FieldResult<Session> {
let _state = ctx.data::<Arc<AppState>>()?;
Ok(Session {
id,
title: "Archived".to_string(),
workspace_id: None,
provider: "unknown".to_string(),
model: None,
message_count: 0,
token_count: 0,
archived: true,
tags: vec![],
created_at: Utc::now(),
updated_at: Utc::now(),
})
}
async fn create_agent(&self, ctx: &Context<'_>, input: CreateAgentInput) -> FieldResult<Agent> {
let _state = ctx.data::<Arc<AppState>>()?;
Ok(Agent {
id: uuid::Uuid::new_v4().to_string().into(),
name: input.name,
description: input.description,
system_prompt: input.system_prompt,
provider_id: input.provider_id,
model: input.model,
temperature: input.temperature.unwrap_or(0.7),
created_at: Utc::now(),
})
}
async fn delete_agent(&self, ctx: &Context<'_>, id: ID) -> FieldResult<bool> {
let _state = ctx.data::<Arc<AppState>>()?;
let _id = id.to_string();
Ok(true)
}
async fn harvest(&self, ctx: &Context<'_>, providers: Option<Vec<String>>) -> FieldResult<i32> {
let _state = ctx.data::<Arc<AppState>>()?;
let _providers = providers;
Ok(0)
}
async fn sync(&self, ctx: &Context<'_>) -> FieldResult<bool> {
let _state = ctx.data::<Arc<AppState>>()?;
Ok(true)
}
}
pub async fn graphql_handler(
schema: web::Data<ChasmSchema>,
req: GraphQLRequest,
) -> GraphQLResponse {
schema.execute(req.into_inner()).await.into()
}
pub async fn graphql_playground() -> impl Responder {
HttpResponse::Ok()
.content_type("text/html")
.body(GRAPHQL_PLAYGROUND_HTML)
}
pub async fn graphql_sdl(schema: web::Data<ChasmSchema>) -> impl Responder {
HttpResponse::Ok()
.content_type("text/plain")
.body(schema.sdl())
}
pub fn create_schema(state: Arc<AppState>) -> ChasmSchema {
Schema::build(QueryRoot, MutationRoot, EmptySubscription)
.data(state)
.finish()
}
pub fn configure_graphql_routes(cfg: &mut web::ServiceConfig, schema: ChasmSchema) {
cfg.app_data(web::Data::new(schema))
.service(
web::resource("/graphql")
.route(web::get().to(graphql_handler))
.route(web::post().to(graphql_handler)),
)
.route("/graphql/playground", web::get().to(graphql_playground))
.route("/graphql/sdl", web::get().to(graphql_sdl));
}
const GRAPHQL_PLAYGROUND_HTML: &str = r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chasm GraphQL Playground</title>
<style>
body {
margin: 0;
padding: 0;
height: 100vh;
overflow: hidden;
}
.custom-header {
background: linear-gradient(135deg, #e535ab 0%, #9c27b0 100%);
color: white;
padding: 12px 24px;
display: flex;
align-items: center;
gap: 16px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
.custom-header h1 {
margin: 0;
font-size: 20px;
font-weight: 600;
}
.custom-header .badge {
background: rgba(255,255,255,0.2);
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
}
.custom-header a {
color: white;
text-decoration: none;
margin-left: auto;
opacity: 0.9;
font-size: 14px;
}
.custom-header a:hover {
opacity: 1;
}
#graphiql {
height: calc(100vh - 48px);
}
</style>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/graphiql@3/graphiql.min.css" />
</head>
<body>
<div class="custom-header">
<h1>◈ Chasm GraphQL</h1>
<span class="badge">v1.3.0</span>
<a href="/docs">REST API →</a>
</div>
<div id="graphiql"></div>
<script crossorigin src="https://cdn.jsdelivr.net/npm/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://cdn.jsdelivr.net/npm/react-dom@18/umd/react-dom.production.min.js"></script>
<script crossorigin src="https://cdn.jsdelivr.net/npm/graphiql@3/graphiql.min.js"></script>
<script>
const fetcher = GraphiQL.createFetcher({
url: '/graphql',
});
const root = ReactDOM.createRoot(document.getElementById('graphiql'));
root.render(
React.createElement(GraphiQL, {
fetcher,
defaultEditorToolsVisibility: true,
})
);
</script>
</body>
</html>
"#;