use crate::monitor::dashboard::PalaceRow;
use super::types::{
DRAWER_SNIPPET_FALLBACK_MAX, DrawerInfo, DreamStats, MemoryDetail, MemoryEvent,
NO_CREATOR_LABEL, RecallHit,
};
use super::types::PalaceWire;
pub fn parse_recall_hits(raw: &serde_json::Value) -> Vec<RecallHit> {
let serde_json::Value::Array(items) = raw else {
return Vec::new();
};
items
.iter()
.map(|item| {
let palace_id = item
.get("palace_id")
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string();
let snippet = item
.get("content")
.and_then(|v| v.as_str())
.unwrap_or_default()
.lines()
.next()
.unwrap_or_default()
.trim()
.to_string();
let score = item.get("score").and_then(|v| v.as_f64()).unwrap_or(0.0) as f32;
RecallHit {
palace_id,
snippet,
score,
}
})
.collect()
}
pub fn parse_dream_stats(raw: &serde_json::Value) -> DreamStats {
let u64_of = |key: &str| raw.get(key).and_then(|v| v.as_u64()).unwrap_or(0);
DreamStats {
merged: u64_of("merged"),
pruned: u64_of("pruned"),
compacted: u64_of("compacted"),
}
}
pub fn parse_memory_event(value: &serde_json::Value) -> Option<MemoryEvent> {
let tag = value.get("type").and_then(|v| v.as_str())?;
let str_of = |key: &str| {
value
.get(key)
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string()
};
let u64_of = |key: &str| value.get(key).and_then(|v| v.as_u64()).unwrap_or(0);
match tag {
"palace_created" => Some(MemoryEvent::PalaceCreated {
name: str_of("name"),
}),
"drawer_added" => Some(MemoryEvent::DrawerAdded {
palace_id: str_of("palace_id"),
drawer_count: u64_of("drawer_count"),
content_preview: str_of("content_preview"),
}),
"drawer_deleted" => Some(MemoryEvent::DrawerDeleted {
palace_id: str_of("palace_id"),
drawer_count: u64_of("drawer_count"),
}),
"dream_completed" => Some(MemoryEvent::DreamCompleted {
merged: u64_of("merged"),
pruned: u64_of("pruned"),
compacted: u64_of("compacted"),
}),
_ => None,
}
}
pub fn parse_palaces(raw: &serde_json::Value) -> Vec<PalaceRow> {
let array = match raw {
serde_json::Value::Array(items) => items.clone(),
serde_json::Value::Object(obj) => match obj.get("palaces") {
Some(serde_json::Value::Array(items)) => items.clone(),
_ => Vec::new(),
},
_ => Vec::new(),
};
array
.into_iter()
.filter_map(|v| serde_json::from_value::<PalaceWire>(v).ok())
.map(|p| PalaceRow {
id: p.id,
name: p.name,
vector_count: p.vector_count,
drawer_count: p.drawer_count,
last_write_at: p.last_write_at,
description: p.description,
kg_triple_count: p.kg_triple_count,
node_count: p.node_count,
edge_count: p.edge_count,
community_count: p.community_count,
is_compacting: p.is_compacting,
})
.collect()
}
pub fn parse_memory_details(raw: &serde_json::Value) -> Vec<MemoryDetail> {
let array: Vec<serde_json::Value> = match raw {
serde_json::Value::Array(items) => items.clone(),
serde_json::Value::Object(obj) => match obj.get("drawers") {
Some(serde_json::Value::Array(items)) => items.clone(),
_ => Vec::new(),
},
_ => Vec::new(),
};
array
.into_iter()
.map(|item| {
let id = item
.get("id")
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string();
let content = item
.get("content")
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string();
let tags: Vec<String> = item
.get("tags")
.and_then(|v| v.as_array())
.map(|a| {
a.iter()
.filter_map(|t| t.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
let created_at = item
.get("created_at")
.and_then(|v| v.as_str())
.and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok())
.map(|dt| dt.with_timezone(&chrono::Utc));
MemoryDetail {
id,
content,
tags,
created_at,
}
})
.collect()
}
pub fn parse_drawers(raw: &serde_json::Value) -> Vec<DrawerInfo> {
let array: Vec<serde_json::Value> = match raw {
serde_json::Value::Array(items) => items.clone(),
serde_json::Value::Object(obj) => match obj.get("drawers") {
Some(serde_json::Value::Array(items)) => items.clone(),
_ => Vec::new(),
},
_ => Vec::new(),
};
array
.into_iter()
.map(|item| {
let id = item
.get("id")
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string();
let created_at = item
.get("created_at")
.and_then(|v| v.as_str())
.and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok())
.map(|dt| dt.with_timezone(&chrono::Utc));
let tags: Vec<String> = item
.get("tags")
.and_then(|v| v.as_array())
.map(|a| {
a.iter()
.filter_map(|t| t.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
let creator = creator_label(&tags);
let snippet = item
.get("snippet")
.and_then(|v| v.as_str())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.or_else(|| {
item.get("content")
.and_then(|v| v.as_str())
.map(client_snippet)
.filter(|s| !s.is_empty())
});
DrawerInfo {
id,
created_at,
creator,
tags,
snippet,
}
})
.collect()
}
fn client_snippet(content: &str) -> String {
let normalised: String = content.split_whitespace().collect::<Vec<_>>().join(" ");
if normalised.chars().count() <= DRAWER_SNIPPET_FALLBACK_MAX {
normalised
} else {
let kept: String = normalised
.chars()
.take(DRAWER_SNIPPET_FALLBACK_MAX.saturating_sub(1))
.collect();
format!("{kept}…")
}
}
pub fn creator_label(tags: &[String]) -> String {
for tag in tags {
if tag.starts_with("msg:from=")
|| tag.starts_with("tag:creator:")
|| tag.starts_with("creator:")
{
return tag.clone();
}
}
NO_CREATOR_LABEL.to_string()
}