pub(crate) mod read;
use std::collections::BTreeMap;
use argonauts_core::app::{AppEvent, ViewData};
use argonauts_core::behavior_map::{
BehaviorMap, BehaviorRow, Comparison, Grouping, MapAction, MapGroup, MapRequest, Placing, Scope,
};
use argonauts_core::deep_map::says_more_than;
use argonauts_core::display_names::DisplayNames;
use argonauts_core::settings::behaviors::BehaviorMap as MapSetting;
use time::{Duration, OffsetDateTime};
pub(crate) use read::Recorded;
use read::{Records, project_name, records};
use crate::behaviors::deep::places::{KeptPlaces, kept_places};
use crate::behaviors::map::{self, Counted, NoOverlay, Point};
use crate::controller::{Ctx, Effect};
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct Placed {
pub label: String,
pub place: (f64, f64),
}
const LISTED: usize = 8;
const SUMMARIZED_ALIKE: usize = 14;
pub(crate) fn labelled(recorded: &Recorded) -> Option<Placed> {
let (x, from_top) = recorded.behavior.place()?;
Some(Placed {
label: recorded.behavior.words(),
place: (f64::from(x), 1.0 - f64::from(from_top)),
})
}
pub(crate) async fn view(ctx: &mut Ctx, request: MapRequest) -> anyhow::Result<Vec<Effect>> {
let paths = ctx.env.paths.clone();
let here = ctx.env.state_dir.clone();
let names = ctx.names.names();
let wanted = request.map.unwrap_or(ctx.settings().behaviors.map);
let map = tokio::task::spawn_blocking(move || {
let only = match request.scope {
Scope::Everywhere => None,
Scope::ThisProject => Some(here.as_path()),
};
let since = request
.period
.days()
.map(|days| OffsetDateTime::now_utc() - Duration::days(days));
let records = records(&paths, only, since);
let kept = kept_places(&paths, &names);
let mut map = match &kept {
Some(kept) if wanted == MapSetting::Deeper => {
build_deeper(&records, request, &names, kept)
}
_ => build(&records, request, &names),
};
map.offer_deeper = kept.is_none() && !map.actions.is_empty();
map.here = Some(project_name(&here));
map
})
.await?;
ctx.send(AppEvent::View {
data: Box::new(ViewData::Behaviors { map: Box::new(map) }),
});
Ok(Vec::new())
}
struct Spot {
x: f32,
y: f32,
behavior: String,
label: String,
summary: Option<String>,
}
pub(crate) fn build(records: &Records, request: MapRequest, names: &DisplayNames) -> BehaviorMap {
build_placed(records, request, names, Placing::Labels, &|recorded| {
let placed = labelled(recorded)?;
let point = map::jittered(placed.place, &recorded.key);
Some(Spot {
x: point.x as f32,
y: point.y as f32,
behavior: placed.label.clone(),
label: placed.label,
summary: None,
})
})
}
pub(crate) fn build_deeper(
records: &Records,
request: MapRequest,
names: &DisplayNames,
kept: &KeptPlaces,
) -> BehaviorMap {
let placing = Placing::Summaries {
by: kept.by.clone(),
};
let mut out = build_placed(records, request, names, placing, &|recorded| {
let place = kept.of(&recorded.key)?;
let label = recorded.behavior.words();
Some(Spot {
x: place.x,
y: place.y,
behavior: place.summary.clone(),
label,
summary: Some(place.summary.clone()),
})
});
out.unsummarized = out.unlabelled;
out.unlabelled = 0;
out.by_words = kept.by_words;
out
}
fn build_placed(
records: &Records,
request: MapRequest,
names: &DisplayNames,
placing: Placing,
spot: &dyn Fn(&Recorded) -> Option<Spot>,
) -> BehaviorMap {
let mut out = BehaviorMap {
request,
placing,
runs: records.runs,
conversations: records.conversations,
offer_deeper: true,
..BehaviorMap::default()
};
let mut groups: Vec<MapGroup> = Vec::new();
let mut projects: Vec<String> = Vec::new();
for recorded in &records.actions {
let Some(spot) = spot(recorded) else {
out.unlabelled += 1;
continue;
};
let group = group_of(recorded, request.grouping, names);
let at = match groups.iter().position(|known| known.name == group.name) {
Some(at) => at,
None => {
groups.push(group);
groups.len() - 1
}
};
groups[at].actions += 1;
if !projects.contains(&recorded.project) {
projects.push(recorded.project.clone());
}
out.actions.push(MapAction {
x: spot.x,
y: spot.y,
group: at,
behavior: spot.behavior,
label: spot.label,
summary: spot.summary,
agent: recorded.agent.clone(),
model: names
.model(&recorded.cli, recorded.model.as_deref())
.unwrap_or_else(|| names.cli(&recorded.cli)),
cli: names.cli(&recorded.cli),
role: recorded.role.clone(),
workflow: recorded.workflow.clone(),
project: recorded.project.clone(),
run: recorded.run.clone(),
at: recorded.at,
nth: recorded.nth,
of: recorded.of,
tool: recorded.tool.clone(),
input: recorded.what.clone(),
place: recorded.place.clone(),
run_shows: recorded.run_shows.clone(),
});
}
out.groups = order_groups(groups, &mut out.actions);
out.projects = projects;
out.comparisons = compare_all(&out);
out
}
fn order_groups(groups: Vec<MapGroup>, actions: &mut [MapAction]) -> Vec<MapGroup> {
let mut order: Vec<usize> = (0..groups.len()).collect();
order.sort_by(|a, b| groups[*b].actions.cmp(&groups[*a].actions).then(a.cmp(b)));
let mut moved = vec![0; groups.len()];
for (new, old) in order.iter().enumerate() {
moved[*old] = new;
}
for action in actions.iter_mut() {
action.group = moved[action.group];
}
order.into_iter().map(|old| groups[old].clone()).collect()
}
fn group_of(recorded: &Recorded, grouping: Grouping, names: &DisplayNames) -> MapGroup {
let plain = |name: &str| MapGroup {
name: name.to_owned(),
..MapGroup::default()
};
match grouping {
Grouping::Model => MapGroup {
name: names
.model(&recorded.cli, recorded.model.as_deref())
.unwrap_or_else(|| names.cli(&recorded.cli)),
agent: Some(recorded.cli.clone()),
model: recorded.model.clone(),
actions: 0,
},
Grouping::Cli => MapGroup {
name: names.cli(&recorded.cli),
agent: Some(recorded.cli.clone()),
..MapGroup::default()
},
Grouping::Role => plain(&recorded.role),
Grouping::Workflow => plain(&recorded.workflow),
Grouping::Project => plain(&recorded.project),
Grouping::Outcome => plain(outcome_group(recorded)),
}
}
fn outcome_group(recorded: &Recorded) -> &'static str {
match recorded.outcome {
Some(outcome) => outcome.words(),
None if recorded.run.is_none() => "in conversation",
None if !recorded.in_task => "no task",
None => "not finished",
}
}
fn compare_all(map: &BehaviorMap) -> Vec<Comparison> {
let points: Vec<Vec<Point>> = (0..map.groups.len())
.map(|group| {
map.in_group(group)
.map(|(_, action)| Point {
x: f64::from(action.x),
y: f64::from(action.y),
})
.collect()
})
.collect();
let total = map.actions.len();
map::differences(&points)
.into_iter()
.enumerate()
.map(|(group, difference)| {
let mine = map.groups[group].actions;
let rows = match (&map.placing, &difference) {
(Placing::Labels, _) => label_rows(map, group, mine, total - mine),
(Placing::Summaries { .. }, Ok(difference)) => {
summary_rows(map, group, &points[group], difference)
}
(Placing::Summaries { .. }, Err(_)) => Vec::new(),
};
let (overlay, why_not) = match difference {
Ok(difference) => (Some(difference.scaled()), None),
Err(why) => (None, Some(why_not(why, map.request.grouping))),
};
Comparison {
overlay,
why_not,
rows,
mine,
others: total - mine,
}
})
.collect()
}
fn summary_rows(
map: &BehaviorMap,
group: usize,
points: &[Point],
difference: &map::Difference,
) -> Vec<BehaviorRow> {
let own: Vec<usize> = map.in_group(group).map(|(at, _)| at).collect();
let near = |at: usize| {
let here = &map.actions[at];
let mut by_distance: Vec<usize> = (0..map.actions.len()).collect();
let distance = |other: &usize| {
let there = &map.actions[*other];
(here.x - there.x).hypot(here.y - there.y)
};
by_distance.sort_by(|a, b| distance(a).total_cmp(&distance(b)).then(a.cmp(b)));
by_distance.truncate(SUMMARIZED_ALIKE);
by_distance
};
map::farthest_first(points, &difference.at_points, LISTED)
.into_iter()
.filter_map(|chosen| own.get(chosen).copied())
.map(|at| {
let action = &map.actions[at];
let lit = near(at);
let mine = lit
.iter()
.filter(|a| map.actions[**a].group == group)
.count();
BehaviorRow {
behavior: action.behavior.clone(),
label: Some(action.label.clone()),
mine,
others: lit.len() - mine,
lit,
new: says_more_than(&action.label),
}
})
.collect()
}
fn why_not(why: NoOverlay, grouping: Grouping) -> String {
match why {
NoOverlay::Small(1) => format!(
"1 action is too few to draw where it acts: it takes {}",
map::SMALLEST
),
NoOverlay::Small(count) => format!(
"{count} actions are too few to draw where it acts: it takes {}",
map::SMALLEST
),
NoOverlay::Alone => format!(
"No other {} has {} actions to compare it with",
grouping.word(),
map::SMALLEST
),
}
}
fn label_rows(map: &BehaviorMap, group: usize, mine: usize, others: usize) -> Vec<BehaviorRow> {
let mut counts: BTreeMap<&str, (usize, usize)> = BTreeMap::new();
for action in &map.actions {
let count = counts.entry(action.behavior.as_str()).or_default();
if action.group == group {
count.0 += 1;
} else {
count.1 += 1;
}
}
let counted = counts
.into_iter()
.map(|(behavior, (mine, others))| Counted {
behavior: behavior.to_owned(),
mine,
others,
})
.collect();
map::by_share(counted, mine, others)
.into_iter()
.map(|counted| BehaviorRow {
lit: map
.actions
.iter()
.enumerate()
.filter(|(_, action)| action.behavior == counted.behavior)
.map(|(at, _)| at)
.collect(),
behavior: counted.behavior,
label: None,
mine: counted.mine,
others: counted.others,
new: false,
})
.collect()
}
#[cfg(test)]
mod tests;