Dhurgh's picture
Permanently remove build artifacts from history
d0b8e8f
Raw
History Blame Contribute Delete
10.2 kB
use rustler::{NifResult, Binary};
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReviewHistory {
pub review_date: String,
pub grade: i32, // 0-5 (SM-2 grades)
pub time_spent: i64, // milliseconds
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CardMetadata {
pub id: String,
pub created_at: String,
pub updated_at: String,
pub last_reviewed: Option<String>,
pub ease_factor: f64, // SM-2: starts at 2.5
pub interval: i64, // SM-2: days between reviews
pub repetitions: i32, // SM-2: number of times reviewed
pub tags: Vec<String>,
pub related_cards: Vec<String>,
pub question: String,
pub answer: String,
pub sketch_data: Option<String>,
pub review_history: Vec<ReviewHistory>,
pub variations: Vec<CardVariation>,
pub is_mastered: bool,
pub focused_count: i32, // biometric tracking
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CardVariation {
pub timestamp: String,
pub question: String,
pub answer: String,
}
#[derive_nifizer::nif]
#[rustler::nif]
fn create_card(
id: String,
question: String,
answer: String,
tags: Vec<String>,
) -> NifResult<String> {
let card = CardMetadata {
id: id.clone(),
created_at: Utc::now().to_rfc3339(),
updated_at: Utc::now().to_rfc3339(),
last_reviewed: None,
ease_factor: 2.5,
interval: 1,
repetitions: 0,
tags,
related_cards: vec![],
question,
answer,
sketch_data: None,
review_history: vec![],
variations: vec![],
is_mastered: false,
focused_count: 0,
};
let db = sled::open("flashcards.db")
.map_err(|e| rustler::Error::Term(Box::new(e.to_string())))?;
let card_json = serde_json::to_string(&card)
.map_err(|e| rustler::Error::Term(Box::new(e.to_string())))?;
db.insert(id.as_bytes(), card_json.as_bytes())
.map_err(|e| rustler::Error::Term(Box::new(e.to_string())))?;
let _ = db.flush();
Ok(serde_json::to_string(&card).unwrap_or_default())
}
#[rustler::nif]
fn save_card(id: String, content: String) -> NifResult<String> {
let db = sled::open("flashcards.db")
.map_err(|e| rustler::Error::Term(Box::new(e.to_string())))?;
match db.get(id.as_bytes())
.map_err(|e| rustler::Error::Term(Box::new(e.to_string())))? {
Some(existing) => {
let mut card: CardMetadata = serde_json::from_slice(&existing)
.map_err(|e| rustler::Error::Term(Box::new(e.to_string())))?;
// Store old version in variations for time-travel
card.variations.push(CardVariation {
timestamp: card.updated_at.clone(),
question: card.question.clone(),
answer: card.answer.clone(),
});
// Keep only last 50 variations
if card.variations.len() > 50 {
card.variations.remove(0);
}
// Update with new content (expecting JSON)
if let Ok(update) = serde_json::from_str::<serde_json::Value>(&content) {
if let Some(q) = update.get("question").and_then(|v| v.as_str()) {
card.question = q.to_string();
}
if let Some(a) = update.get("answer").and_then(|v| v.as_str()) {
card.answer = a.to_string();
}
if let Some(tags) = update.get("tags").and_then(|v| v.as_array()) {
card.tags = tags.iter().filter_map(|t| t.as_str().map(|s| s.to_string())).collect();
}
if let Some(sketch) = update.get("sketch_data").and_then(|v| v.as_str()) {
card.sketch_data = Some(sketch.to_string());
}
}
card.updated_at = Utc::now().to_rfc3339();
let card_json = serde_json::to_string(&card)
.map_err(|e| rustler::Error::Term(Box::new(e.to_string())))?;
db.insert(id.as_bytes(), card_json.as_bytes())
.map_err(|e| rustler::Error::Term(Box::new(e.to_string())))?;
let _ = db.flush();
Ok("UPDATED".to_string())
}
None => {
db.insert(id.as_bytes(), content.as_bytes())
.map_err(|e| rustler::Error::Term(Box::new(e.to_string())))?;
let _ = db.flush();
Ok("CREATED".to_string())
}
}
}
#[rustler::nif]
fn get_card(id: String) -> NifResult<String> {
let db = sled::open("flashcards.db")
.map_err(|e| rustler::Error::Term(Box::new(e.to_string())))?;
match db.get(id.as_bytes())
.map_err(|e| rustler::Error::Term(Box::new(e.to_string())))? {
Some(ivec) => Ok(String::from_utf8_lossy(&ivec).to_string()),
None => Ok("NOT_FOUND".to_string()),
}
}
#[rustler::nif]
fn record_review(
id: String,
grade: i32,
time_spent: i64,
) -> NifResult<String> {
let db = sled::open("flashcards.db")
.map_err(|e| rustler::Error::Term(Box::new(e.to_string())))?;
match db.get(id.as_bytes())
.map_err(|e| rustler::Error::Term(Box::new(e.to_string())))? {
Some(existing) => {
let mut card: CardMetadata = serde_json::from_slice(&existing)
.map_err(|e| rustler::Error::Term(Box::new(e.to_string())))?;
// SM-2 Algorithm Implementation
card.review_history.push(ReviewHistory {
review_date: Utc::now().to_rfc3339(),
grade,
time_spent,
});
card.repetitions += 1;
card.last_reviewed = Some(Utc::now().to_rfc3339());
// SM-2 calculation
if grade >= 3 {
if card.repetitions == 1 {
card.interval = 1;
} else if card.repetitions == 2 {
card.interval = 3;
} else {
card.interval = (card.interval as f64 * card.ease_factor).ceil() as i64;
}
} else {
card.repetitions = 0;
card.interval = 1;
}
// Ease Factor adjustment
let ef = card.ease_factor + (0.1 - (5.0 - grade as f64) * (0.08 + (5.0 - grade as f64) * 0.02));
card.ease_factor = if ef < 1.3 { 1.3 } else { ef };
// Mark as mastered if reviewed 5+ times with grade 4+
if card.repetitions >= 5 && grade >= 4 {
card.is_mastered = true;
}
let card_json = serde_json::to_string(&card)
.map_err(|e| rustler::Error::Term(Box::new(e.to_string())))?;
db.insert(id.as_bytes(), card_json.as_bytes())
.map_err(|e| rustler::Error::Term(Box::new(e.to_string())))?;
let _ = db.flush();
Ok(serde_json::to_string(&card).unwrap_or_default())
}
None => Err(rustler::Error::Term(Box::new("CARD_NOT_FOUND"))),
}
}
#[rustler::nif]
fn add_relationship(card_id: String, related_id: String) -> NifResult<String> {
let db = sled::open("flashcards.db")
.map_err(|e| rustler::Error::Term(Box::new(e.to_string())))?;
match db.get(card_id.as_bytes())
.map_err(|e| rustler::Error::Term(Box::new(e.to_string())))? {
Some(existing) => {
let mut card: CardMetadata = serde_json::from_slice(&existing)
.map_err(|e| rustler::Error::Term(Box::new(e.to_string())))?;
if !card.related_cards.contains(&related_id) {
card.related_cards.push(related_id);
}
let card_json = serde_json::to_string(&card)
.map_err(|e| rustler::Error::Term(Box::new(e.to_string())))?;
db.insert(card_id.as_bytes(), card_json.as_bytes())
.map_err(|e| rustler::Error::Term(Box::new(e.to_string())))?;
let _ = db.flush();
Ok("RELATIONSHIP_ADDED".to_string())
}
None => Err(rustler::Error::Term(Box::new("CARD_NOT_FOUND"))),
}
}
#[rustler::nif]
fn get_all_cards() -> NifResult<String> {
let db = sled::open("flashcards.db")
.map_err(|e| rustler::Error::Term(Box::new(e.to_string())))?;
let cards: Vec<CardMetadata> = db.iter()
.filter_map(|item| {
item.ok().and_then(|(_, v)| {
serde_json::from_slice(&v).ok()
})
})
.collect();
Ok(serde_json::to_string(&cards).unwrap_or_default())
}
#[rustler::nif]
fn get_historical_snapshot(id: String, timestamp: String) -> NifResult<String> {
let db = sled::open("flashcards.db")
.map_err(|e| rustler::Error::Term(Box::new(e.to_string())))?;
match db.get(id.as_bytes())
.map_err(|e| rustler::Error::Term(Box::new(e.to_string())))? {
Some(existing) => {
let card: CardMetadata = serde_json::from_slice(&existing)
.map_err(|e| rustler::Error::Term(Box::new(e.to_string())))?;
// Find variation closest to timestamp
let closest = card.variations.iter()
.min_by_key(|v| {
(v.timestamp.as_str().cmp(&timestamp.as_str()),
std::cmp::Ordering::Equal)
});
if let Some(variation) = closest {
Ok(serde_json::to_string(variation).unwrap_or_default())
} else {
Ok(format!("{{\\"question\\": \\"{}\\", \\"answer\\": \\"{}\\"}}",
card.question.replace("\"", "\\\""),
card.answer.replace("\"", "\\\"")))
}
}
None => Err(rustler::Error::Term(Box::new("CARD_NOT_FOUND"))),
}
}
rustler::init!("flashsync_native", [
create_card,
save_card,
get_card,
record_review,
add_relationship,
get_all_cards,
get_historical_snapshot
]);