///|
pub(all) struct HnItem {
  id : Int
  by : String?
  time : Int?
  text : String?
  deleted : Bool?
  dead : Bool?
  kids : Array[Int]?
  url : String?
  score : Int?
  title : String?
  descendants : Int?
} derive(FromJson, ToJson, @debug.Debug)

///|
pub(all) struct CommentBrief {
  id : Int
  by : String
  text : String
  replies : Int
} derive(ToJson, @debug.Debug)

///|
pub(all) struct StoryBrief {
  id : Int
  title : String
  url : String
  score : Int
  comments : Int
  hot_comments : Array[CommentBrief]
} derive(ToJson, @debug.Debug)

///|
pub(all) struct BriefStats {
  stories : Int
  hot_comments : Int
  total_score : Int
  total_discussion_comments : Int
} derive(ToJson, @debug.Debug)

///|
pub fn HnItem::is_visible(self : HnItem) -> Bool {
  self.deleted != Some(true) && self.dead != Some(true)
}

///|
pub fn HnItem::reply_count(self : HnItem) -> Int {
  match self.kids {
    Some(kids) => kids.length()
    None => 0
  }
}

///|
pub fn HnItem::to_comment_brief(self : HnItem) -> CommentBrief {
  {
    id: self.id,
    by: self.by.unwrap_or("unknown"),
    text: clean_hn_text(self.text.unwrap_or("")),
    replies: self.reply_count(),
  }
}

///|
pub fn HnItem::to_story_brief(
  self : HnItem,
  hot_comments : Array[CommentBrief],
) -> StoryBrief {
  {
    id: self.id,
    title: self.title.unwrap_or("(untitled)"),
    url: self.url.unwrap_or("https://news.ycombinator.com/item?id=\{self.id}"),
    score: self.score.unwrap_or(0),
    comments: self.descendants.unwrap_or(0),
    hot_comments,
  }
}

///|
pub fn clean_hn_text(text : String) -> String {
  let normalized = text
    .replace_all(old="

", new="\n") .replace_all(old="
", new="\n") .replace_all(old="
", new="\n") let without_tags = strip_html_tags(normalized) without_tags .replace_all(old=""", new="\"") .replace_all(old="'", new="'") .replace_all(old="/", new="/") .replace_all(old="&", new="&") .replace_all(old="<", new="<") .replace_all(old=">", new=">") .replace_all(old="\n ", new="\n") .replace_all(old=" ", new=" ") .replace_all(old=" ", new=" ") .replace_all(old="\n\n", new="\n") .trim() .to_owned() } ///| fn strip_html_tags(text : String) -> String { let out = StringBuilder::new(size_hint=text.length()) let mut in_tag = false for ch in text { if ch == '<' { in_tag = true } else if ch == '>' { in_tag = false out.write_char(' ') } else if !in_tag { out.write_char(ch) } } out.to_string() } ///| pub fn summary_prompt(stories : ArrayView[StoryBrief], lang : String) -> String { let out = StringBuilder::new() out.write_string( "Summarize these Hacker News stories and ranked comments in \{lang}. ", ) out.write_string( "Return concise sections for themes, why it matters, hot comment consensus, disagreements, and takeaways.\n\n", ) for index, story in stories { out.write_string( "Story \{index + 1}: \{story.title}\nscore=\{story.score}, comments=\{story.comments}\nurl=\{story.url}\n", ) for comment_index, comment in story.hot_comments { out.write_string( "- Comment \{comment_index + 1} by \{comment.by}, replies=\{comment.replies}: \{comment.text}\n", ) } out.write_string("\n") } out.to_string() } ///| pub fn stats(stories : ArrayView[StoryBrief]) -> BriefStats { let mut hot_comments = 0 let mut total_score = 0 let mut total_discussion_comments = 0 for story in stories { hot_comments += story.hot_comments.length() total_score += story.score total_discussion_comments += story.comments } { stories: stories.length(), hot_comments, total_score, total_discussion_comments, } } ///| pub fn render_markdown( stories : ArrayView[StoryBrief], summary : String, model_label~ : String, show_context? : Bool = false, ) -> String { let out = StringBuilder::new() out.write_string("# HN Brief\n\n") out.write_string("Source: Hacker News API\n\n") let stat = stats(stories) out.write_string("## Stats\n\n") out.write_string("- Stories fetched: \{stat.stories}\n") out.write_string("- Ranked comments fetched: \{stat.hot_comments}\n") out.write_string("- Total HN score: \{stat.total_score}\n") out.write_string( "- Total discussion comments: \{stat.total_discussion_comments}\n\n", ) out.write_string("## Summary (\{model_label})\n\n") out.write_string(summary) if !summary.has_suffix("\n") { out.write_string("\n") } if show_context { out.write_string("\n## Top Stories and Hot Comments\n\n") for index, story in stories { out.write_string( "### \{index + 1}. \{story.title}\n\n- Score: \{story.score}\n- Comments: \{story.comments}\n- URL: \{story.url}\n\n", ) if story.hot_comments.is_empty() { out.write_string("No visible ranked comments fetched.\n\n") } else { out.write_string("Hot comments:\n") for comment in story.hot_comments { out.write_string( "- \{comment.by} (\{comment.replies} replies): \{comment.text}\n", ) } out.write_string("\n") } } } out.to_string() }