use crate::admin::entry_builder::DynamicAdminEntry;
use crate::admin::AdminEntry;
use crate::ai::ContextConfig;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Confidence {
High,
Medium,
}
impl Confidence {
pub fn as_str(self) -> &'static str {
match self {
Confidence::High => "High",
Confidence::Medium => "Medium",
}
}
pub fn pill_class(self) -> &'static str {
match self {
Confidence::High => "badge-success",
Confidence::Medium => "badge-warning",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Suggestion {
pub model_display: String,
pub model_singular: String,
pub admin_name: String,
pub field: String,
pub prompt: String,
pub reason: String,
pub action: &'static str,
pub confidence: Confidence,
}
impl Suggestion {
pub fn url_path(&self) -> String {
format!(
"/admin/suggestions/{admin}/{field}",
admin = self.admin_name,
field = self.field,
)
}
}
pub fn derive_suggestions(
entries: &[AdminEntry],
context: Option<&ContextConfig>,
) -> Vec<Suggestion> {
let Some(ctx) = context else {
return Vec::new();
};
let Some(schema) = ctx.industry_schema() else {
return Vec::new();
};
let industry = ctx.industry.as_deref().unwrap_or("").to_string();
let mut out: Vec<Suggestion> = Vec::new();
for entry in entries.iter().filter(|e| !e.core) {
let field_names: Vec<&str> = entry.fields.iter().map(|f| f.name).collect();
let covers_any = schema
.required_fields
.iter()
.any(|req| field_names.contains(&req.as_str()));
if !covers_any {
continue;
}
for req in &schema.required_fields {
if field_names.contains(&req.as_str()) {
continue;
}
let prompt = format!("add {req} to {admin}", admin = entry.admin_name);
out.push(Suggestion {
model_display: entry.display_name.to_string(),
model_singular: entry.singular_name.to_string(),
admin_name: entry.admin_name.to_string(),
field: req.clone(),
prompt,
reason: format!("{industry} industry convention"),
action: "add_field",
confidence: Confidence::High,
});
}
}
out
}
pub fn find_suggestion(
entries: &[AdminEntry],
context: Option<&ContextConfig>,
admin_name: &str,
field: &str,
) -> Option<Suggestion> {
derive_suggestions(entries, context)
.into_iter()
.find(|s| s.admin_name == admin_name && s.field == field)
}
pub fn derive_suggestions_from_entries(
_entries: &[DynamicAdminEntry],
_context: Option<&ContextConfig>,
) -> Vec<Suggestion> {
Vec::new()
}
pub fn find_suggestion_from_entries(
_entries: &[DynamicAdminEntry],
_context: Option<&ContextConfig>,
_admin_name: &str,
_field: &str,
) -> Option<Suggestion> {
None
}
pub fn derive_relation_suggestions(schema: &crate::schema::Schema) -> Vec<Suggestion> {
let mut out: Vec<Suggestion> = Vec::new();
for model in schema.models.iter().filter(|m| !m.core) {
for field in &model.fields {
if field.name == "id" || !field.name.ends_with("_id") {
continue;
}
if field.relation.is_some() {
continue;
}
let stem = &field.name[..field.name.len() - 3];
if stem.is_empty() {
continue;
}
let mut candidates: Vec<&crate::schema::SchemaModel> = schema
.models
.iter()
.filter(|m| {
m.singular_name.eq_ignore_ascii_case(stem) || m.name.eq_ignore_ascii_case(stem)
})
.collect();
candidates.dedup_by(|a, b| a.name == b.name);
if candidates.len() != 1 {
continue;
}
let target = candidates[0];
if target.name == model.name {
continue;
}
out.push(Suggestion {
model_display: model.display_name.clone(),
model_singular: model.singular_name.clone(),
admin_name: model.admin_name.clone(),
field: field.name.clone(),
prompt: format!(
"link {from} to {to}",
from = model.singular_name,
to = target.singular_name,
),
reason: format!(
"`{}` looks like a foreign key to `{}` but no relation is recorded.",
field.name, target.singular_name,
),
action: "add_relation",
confidence: Confidence::Medium,
});
}
}
out
}
pub fn find_relation_suggestion(
schema: &crate::schema::Schema,
admin_name: &str,
field: &str,
) -> Option<Suggestion> {
derive_relation_suggestions(schema)
.into_iter()
.find(|s| s.admin_name == admin_name && s.field == field)
}