grok-digest 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,22 +1,31 @@
1
1
  import { TOPICS } from "./topics.js";
2
+ export const VIEWS = ["today", "inbox", "done", "all"];
3
+ const DEFAULT_VIEW = "today";
2
4
  export function isTopic(value) {
3
5
  return TOPICS.some((topic) => topic === value);
4
6
  }
7
+ function isView(value) {
8
+ return VIEWS.some((view) => view === value);
9
+ }
5
10
  export function parseFilter(search) {
6
11
  const params = new URLSearchParams(search);
7
12
  const topic = params.get("topic");
13
+ const view = params.get("view");
8
14
  return {
9
15
  query: params.get("q") ?? "",
10
16
  topic: isTopic(topic) ? topic : null,
17
+ view: isView(view) ? view : DEFAULT_VIEW,
11
18
  };
12
19
  }
13
- export function filterSearch({ query, topic }) {
20
+ export function filterSearch({ query, topic, view }) {
14
21
  const params = new URLSearchParams();
15
22
  const trimmed = query.trim();
16
23
  if (trimmed !== "")
17
24
  params.set("q", trimmed);
18
25
  if (topic !== null)
19
26
  params.set("topic", topic);
27
+ if (view !== DEFAULT_VIEW)
28
+ params.set("view", view);
20
29
  const search = params.toString();
21
30
  return search === "" ? "" : `?${search}`;
22
31
  }
@@ -1,5 +1,6 @@
1
1
  export declare const bookmarks: {
2
2
  postedAt: Date;
3
+ savedAt: Date;
3
4
  keyPoints: string[];
4
5
  id: string;
5
6
  url: string;
@@ -8,6 +9,7 @@ export declare const bookmarks: {
8
9
  handle: string;
9
10
  };
10
11
  topic: "ai-agents" | "models-releases" | "dev-tools" | "startups-business" | "japan-tech" | "design" | "other";
12
+ tldr: string;
11
13
  summary: string;
12
14
  text: string;
13
15
  }[];
@@ -0,0 +1,17 @@
1
+ export declare const REVIEW_SIZE = 5;
2
+ export type ReviewPick = {
3
+ id: string;
4
+ reason: string;
5
+ };
6
+ type Saved = {
7
+ id: string;
8
+ savedAt: Date;
9
+ };
10
+ export declare function localDay(date: Date, timeZone?: string): string;
11
+ export declare function dailyReview({ bookmarks, done, today, timeZone, }: {
12
+ bookmarks: readonly Saved[];
13
+ done: ReadonlySet<string>;
14
+ today: string;
15
+ timeZone?: string;
16
+ }): ReviewPick[];
17
+ export {};
@@ -0,0 +1,50 @@
1
+ export const REVIEW_SIZE = 5;
2
+ const REVIEW_AGES = new Map([
3
+ [1, "Saved yesterday"],
4
+ [7, "Saved a week ago"],
5
+ [30, "Saved a month ago"],
6
+ [90, "Saved 3 months ago"],
7
+ ]);
8
+ const BACKLOG_REASON = "From your backlog";
9
+ const DAY_MS = 86_400_000;
10
+ export function localDay(date, timeZone) {
11
+ return new Intl.DateTimeFormat("en-CA", { timeZone, year: "numeric", month: "2-digit", day: "2-digit" }).format(date);
12
+ }
13
+ function daysBetween(from, to) {
14
+ return Math.round((Date.parse(to) - Date.parse(from)) / DAY_MS);
15
+ }
16
+ function anniversaryReason(saved, today) {
17
+ const years = Number(today.slice(0, 4)) - Number(saved.slice(0, 4));
18
+ if (years < 1 || saved.slice(5) !== today.slice(5))
19
+ return undefined;
20
+ return years === 1 ? "Saved a year ago today" : `Saved ${years} years ago today`;
21
+ }
22
+ function hash(text) {
23
+ let value = 0x811c9dc5;
24
+ for (const char of text) {
25
+ value ^= char.codePointAt(0) ?? 0;
26
+ value = Math.imul(value, 0x01000193) >>> 0;
27
+ }
28
+ return value;
29
+ }
30
+ export function dailyReview({ bookmarks, done, today, timeZone, }) {
31
+ const candidates = bookmarks
32
+ .filter(({ id }) => !done.has(id))
33
+ .map(({ id, savedAt }) => {
34
+ const saved = localDay(savedAt, timeZone);
35
+ const age = daysBetween(saved, today);
36
+ return { id, age, reason: REVIEW_AGES.get(age) ?? anniversaryReason(saved, today) };
37
+ })
38
+ .filter(({ age }) => age >= 1);
39
+ const due = candidates
40
+ .flatMap(({ id, age, reason }) => (reason === undefined ? [] : [{ id, age, reason }]))
41
+ .toSorted((a, b) => b.age - a.age)
42
+ .slice(0, REVIEW_SIZE)
43
+ .map(({ id, reason }) => ({ id, reason }));
44
+ const backlog = candidates
45
+ .filter(({ reason }) => reason === undefined)
46
+ .toSorted((a, b) => hash(`${today}:${a.id}`) - hash(`${today}:${b.id}`))
47
+ .slice(0, REVIEW_SIZE - due.length)
48
+ .map(({ id }) => ({ id, reason: BACKLOG_REASON }));
49
+ return [...due, ...backlog];
50
+ }
@@ -1,13 +1,19 @@
1
1
  import type { Bookmark } from "./bookmarks.ts";
2
+ import type { Filter } from "./filter.ts";
2
3
  import { type Topic } from "./topics.ts";
3
4
  export type SearchEntry = {
4
5
  id: string;
5
6
  topic: Topic;
7
+ savedAt: Date;
6
8
  text: string;
7
9
  };
8
10
  export declare function toSearchEntry(bookmark: Bookmark): SearchEntry;
9
- export declare function matchingIds({ entries, query, topic, }: {
11
+ export type Triage = {
12
+ done: ReadonlySet<string>;
13
+ review: ReadonlySet<string>;
14
+ };
15
+ export declare function matchingIds({ entries, filter: { query, topic, view }, triage, }: {
10
16
  entries: readonly SearchEntry[];
11
- query: string;
12
- topic: Topic | null;
17
+ filter: Filter;
18
+ triage: Triage;
13
19
  }): Set<string>;
@@ -3,17 +3,24 @@ function normalize(text) {
3
3
  return text.normalize("NFKC").toLowerCase();
4
4
  }
5
5
  export function toSearchEntry(bookmark) {
6
- const { id, topic, author, summary, text, keyPoints } = bookmark;
6
+ const { id, topic, savedAt, author, tldr, summary, text, keyPoints } = bookmark;
7
7
  return {
8
8
  id,
9
9
  topic,
10
- text: normalize([author.name, `@${author.handle}`, TOPIC_LABELS[topic], summary, text, ...keyPoints].join("\n")),
10
+ savedAt,
11
+ text: normalize([author.name, `@${author.handle}`, TOPIC_LABELS[topic], tldr, summary, text, ...keyPoints].join("\n")),
11
12
  };
12
13
  }
13
- export function matchingIds({ entries, query, topic, }) {
14
+ const IN_VIEW = {
15
+ today: (id, { review }) => review.has(id),
16
+ inbox: (id, { done }) => !done.has(id),
17
+ done: (id, { done }) => done.has(id),
18
+ all: () => true,
19
+ };
20
+ export function matchingIds({ entries, filter: { query, topic, view }, triage, }) {
14
21
  const terms = normalize(query).split(/\s+/).filter(Boolean);
15
22
  return new Set(entries
16
23
  .filter((entry) => topic === null || entry.topic === topic)
17
- .filter((entry) => terms.every((term) => entry.text.includes(term)))
24
+ .filter((entry) => terms.length === 0 ? IN_VIEW[view](entry.id, triage) : terms.every((term) => entry.text.includes(term)))
18
25
  .map((entry) => entry.id));
19
26
  }
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  import BookmarkCard from "../components/BookmarkCard.astro";
3
- import { BookmarkFilters } from "../components/BookmarkFilters";
3
+ import { BookmarkControls } from "../components/BookmarkControls";
4
4
  import { ThemeToggle } from "../components/ThemeToggle";
5
5
  import Layout from "../layouts/Layout.astro";
6
6
  import { bookmarks } from "../lib/load-bookmarks";
@@ -22,11 +22,19 @@ const designFeedHref = `${base}rss/design.xml`;
22
22
  <ThemeToggle client:load />
23
23
  </header>
24
24
 
25
+ <noscript>
26
+ <style is:inline>
27
+ [data-bookmark-list] {
28
+ visibility: visible !important;
29
+ }
30
+ </style>
31
+ </noscript>
32
+
25
33
  <main class="flex flex-1 flex-col gap-6 pb-16">
26
34
  {bookmarks.length > 0 ? (
27
35
  <>
28
- <BookmarkFilters client:only="react" entries={entries} />
29
- <ul class="flex flex-col gap-4 sm:gap-5">
36
+ <BookmarkControls client:only="react" entries={entries} />
37
+ <ul data-bookmark-list class="flex flex-col gap-4 not-data-ready:invisible sm:gap-5">
30
38
  {bookmarks.map((bookmark) => (
31
39
  <BookmarkCard bookmark={bookmark} />
32
40
  ))}
@@ -1,6 +1,6 @@
1
1
  @import "tailwindcss";
2
2
  @import "tw-animate-css";
3
- @import "shadcn/tailwind.css";
3
+ @import "./shadcn-tailwind.css";
4
4
  @import "@fontsource-variable/geist";
5
5
  @import "@fontsource-variable/source-serif-4";
6
6