///|
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()
}