Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,30 @@ npm run dev # or bun run dev

The UI will be available at `http://localhost:3000`.

## Documentation

The documentation is located in the `docs/` directory and built with [VuePress](https://vuepress.vuejs.org/).

### Local Preview

1. Install dependencies:
```bash
cd docs
npm install
```
2. Start the local docs server:
```bash
npx vuepress dev
```
3. Open your browser to [http://localhost:8080](http://localhost:8080)

### Build Static Docs

To generate static files for deployment:
```bash
npx vuepress build
```
The output will be in `docs/.vuepress/dist`.

## Contributing

Expand Down
106 changes: 106 additions & 0 deletions backend/src/controllers/prompts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -381,3 +381,109 @@ impl IntoResponse for CompletionResponse {
}
}
}

// --- Prompt Directory API ---
use crate::db::models::{PromptDirectory, PromptComponent};
use axum::extract::Query;
use serde::Deserialize;

#[derive(Deserialize)]
pub struct DirectoryQuery {
pub parent_id: Option<i64>,
}

pub async fn create_directory(
State(state): State<AppState>,
Json(payload): Json<PromptDirectory>,
) -> Result<Json<i64>, AppError> {
let id = state.db.prompt.create_directory(&payload.name, payload.parent_id).await?;
Ok(Json(id))
}

pub async fn get_directory(
Path(id): Path<i64>,
State(state): State<AppState>,
) -> Result<Json<Option<PromptDirectory>>, AppError> {
let dir = state.db.prompt.get_directory(id).await?;
Ok(Json(dir))
}

pub async fn list_directories(
State(state): State<AppState>,
Query(query): Query<DirectoryQuery>,
) -> Result<Json<Vec<PromptDirectory>>, AppError> {
let dirs = state.db.prompt.list_directories(query.parent_id).await?;
Ok(Json(dirs))
}

pub async fn update_directory(
Path(id): Path<i64>,
State(state): State<AppState>,
Json(payload): Json<PromptDirectory>,
) -> Result<Json<bool>, AppError> {
let updated = state.db.prompt.update_directory(id, &payload.name, payload.parent_id).await?;
Ok(Json(updated))
}

pub async fn delete_directory(
Path(id): Path<i64>,
State(state): State<AppState>,
) -> Result<Json<bool>, AppError> {
let deleted = state.db.prompt.delete_directory(id).await?;
Ok(Json(deleted))
}

// --- Prompt Component API ---
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

directories and components should be their own distinct and separate controllers.

#[derive(Deserialize)]
pub struct CreateComponentRequest {
pub name: String,
pub content: String,
pub description: Option<String>,
}

pub async fn create_component(
State(state): State<AppState>,
Json(payload): Json<CreateComponentRequest>,
) -> Result<Json<i64>, AppError> {
let id = state.db.prompt.create_component(&payload.name, &payload.content, payload.description.as_deref()).await?;
Ok(Json(id))
}

pub async fn get_component(
Path(id): Path<i64>,
State(state): State<AppState>,
) -> Result<Json<Option<PromptComponent>>, AppError> {
let comp = state.db.prompt.get_component(id).await?;
Ok(Json(comp))
}

pub async fn list_components(
State(state): State<AppState>,
) -> Result<Json<Vec<PromptComponent>>, AppError> {
let comps = state.db.prompt.list_components().await?;
Ok(Json(comps))
}

#[derive(Deserialize)]
pub struct UpdateComponentRequest {
pub name: String,
pub content: String,
pub description: Option<String>,
}

pub async fn update_component(
Path(id): Path<i64>,
State(state): State<AppState>,
Json(payload): Json<UpdateComponentRequest>,
) -> Result<Json<bool>, AppError> {
let updated = state.db.prompt.update_component(id, &payload.name, &payload.content, payload.description.as_deref()).await?;
Ok(Json(updated))
}

pub async fn delete_component(
Path(id): Path<i64>,
State(state): State<AppState>,
) -> Result<Json<bool>, AppError> {
let deleted = state.db.prompt.delete_component(id).await?;
Ok(Json(deleted))
}
34 changes: 33 additions & 1 deletion backend/src/db/models.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use anyhow::Result;
use crate::db::types::models::ModelProviderRow;
use crate::db::types::models::{ModelProviderRow, Prompt, PromptComponent, PromptDirectory};

#[derive(Clone, Debug)]
pub struct ModelRepository {
Expand Down Expand Up @@ -160,3 +160,35 @@ impl ModelRepository {
Ok(model)
}
}

// --- Prompt Directory ---
#[derive(Debug, Clone)]
pub struct PromptDirectory {
pub id: i64,
pub name: String,
pub parent_id: Option<i64>,
pub created_at: chrono::NaiveDateTime,
pub updated_at: chrono::NaiveDateTime,
}

// --- Prompt Component ---
#[derive(Debug, Clone)]
pub struct PromptComponent {
pub id: i64,
pub name: String,
pub content: String,
pub description: Option<String>,
pub created_at: chrono::NaiveDateTime,
pub updated_at: chrono::NaiveDateTime,
}

// --- Prompt (partial, for directory support) ---
#[derive(Debug, Clone)]
pub struct Prompt {
pub id: i64,
pub key: String,
pub current_prompt_version_id: Option<i64>,
pub directory_id: Option<i64>,
pub created_at: chrono::NaiveDateTime,
pub updated_at: chrono::NaiveDateTime,
}
147 changes: 146 additions & 1 deletion backend/src/db/prompts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,152 @@ impl PromptRepository {
// Return the updated prompt
self.get_prompt(prompt_id).await
}

// --- Prompt Directory CRUD ---
pub async fn create_directory(&self, name: &str, parent_id: Option<i64>) -> Result<i64> {
let rec = sqlx::query!(
r#"INSERT INTO prompt_directory (name, parent_id) VALUES (?, ?)"#,
name,
parent_id
)
.execute(&self.pool)
.await?;
Ok(rec.last_insert_rowid())
}

pub async fn get_directory(&self, id: i64) -> Result<Option<PromptDirectory>> {
let rec = sqlx::query_as!(PromptDirectory,
r#"SELECT id, name, parent_id, created_at, updated_at FROM prompt_directory WHERE id = ?"#,
id
)
.fetch_optional(&self.pool)
.await?;
Ok(rec)
}

pub async fn list_directories(&self, parent_id: Option<i64>) -> Result<Vec<PromptDirectory>> {
let recs = sqlx::query_as!(PromptDirectory,
r#"SELECT id, name, parent_id, created_at, updated_at FROM prompt_directory WHERE parent_id IS ?"#,
parent_id
)
.fetch_all(&self.pool)
.await?;
Ok(recs)
}

pub async fn update_directory(&self, id: i64, name: &str, parent_id: Option<i64>) -> Result<bool> {
let rows = sqlx::query!(
r#"UPDATE prompt_directory SET name = ?, parent_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?"#,
name, parent_id, id
)
.execute(&self.pool)
.await?
.rows_affected();
Ok(rows > 0)
}

pub async fn delete_directory(&self, id: i64) -> Result<bool> {
let rows = sqlx::query!(
r#"DELETE FROM prompt_directory WHERE id = ?"#,
id
)
.execute(&self.pool)
.await?
.rows_affected();
Ok(rows > 0)
}

// --- Prompt Component CRUD ---
pub async fn create_component(&self, name: &str, content: &str, description: Option<&str>) -> Result<i64> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same thing here, directories and components should be their own distinct DB repos.

let rec = sqlx::query!(
r#"INSERT INTO prompt_component (name, content, description) VALUES (?, ?, ?)"#,
name, content, description
)
.execute(&self.pool)
.await?;
Ok(rec.last_insert_rowid())
}

pub async fn get_component(&self, id: i64) -> Result<Option<PromptComponent>> {
let rec = sqlx::query_as!(PromptComponent,
r#"SELECT id, name, content, description, created_at, updated_at FROM prompt_component WHERE id = ?"#,
id
)
.fetch_optional(&self.pool)
.await?;
Ok(rec)
}

pub async fn list_components(&self) -> Result<Vec<PromptComponent>> {
let recs = sqlx::query_as!(PromptComponent,
r#"SELECT id, name, content, description, created_at, updated_at FROM prompt_component ORDER BY created_at DESC"#
)
.fetch_all(&self.pool)
.await?;
Ok(recs)
}

pub async fn update_component(&self, id: i64, name: &str, content: &str, description: Option<&str>) -> Result<bool> {
let rows = sqlx::query!(
r#"UPDATE prompt_component SET name = ?, content = ?, description = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?"#,
name, content, description, id
)
.execute(&self.pool)
.await?
.rows_affected();
Ok(rows > 0)
}

pub async fn delete_component(&self, id: i64) -> Result<bool> {
let rows = sqlx::query!(
r#"DELETE FROM prompt_component WHERE id = ?"#,
id
)
.execute(&self.pool)
.await?
.rows_affected();
Ok(rows > 0)
}

// Utility: Recursively resolve {{component:component_name}} in prompt content
pub async fn resolve_components_in_text(&self, text: &str) -> Result<String> {
use regex::Regex;
let re = Regex::new(r"\{\{component:([a-zA-Z0-9_\- ]+)}}")?;
let mut resolved = text.to_string();
let mut changed = true;
while changed {
changed = false;
let mut new_text = resolved.clone();
for cap in re.captures_iter(&resolved) {
let comp_name = &cap[1];
let comp = sqlx::query!(
r#"SELECT content FROM prompt_component WHERE name = ? LIMIT 1"#,
comp_name
)
.fetch_optional(&self.pool)
.await?;
if let Some(row) = comp {
let comp_content = row.content;
new_text = new_text.replace(&cap[0], &comp_content);
changed = true;
}
}
resolved = new_text;
}
Ok(resolved)
}

// Wrap get_prompt to resolve components in system/user fields
pub async fn get_prompt_with_components(&self, id: i64) -> Result<PromptRowWithModel> {
let mut prompt = self.get_prompt(id).await?;
if let Some(system) = &prompt.system {
prompt.system = Some(self.resolve_components_in_text(system).await?);
}
if let Some(user) = &prompt.user {
prompt.user = Some(self.resolve_components_in_text(user).await?);
}
Ok(prompt)
}
}

fn generate_diff(text1: &str, text2: &str) -> String {
Expand All @@ -573,4 +719,3 @@ fn generate_diff(text1: &str, text2: &str) -> String {

diff_string
}

Loading