birdclaw 0.8.2 → 0.8.4
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.
- package/CHANGELOG.md +33 -0
- package/README.md +10 -8
- package/bin/birdclaw.mjs +4 -66
- package/dist/cli/birdclaw.js +28165 -0
- package/dist/client/assets/AvatarChip-DLoqM5xD.js +1 -0
- package/dist/client/assets/ProfilePreview-BejAuYsJ.js +1 -0
- package/dist/client/assets/SavedTimelineView-BIS7HKpp.js +1 -0
- package/dist/client/assets/SmartTimestamp-BR0-9se2.js +1 -0
- package/dist/client/assets/TimelineRouteFrame-CyGPj0eM.js +1 -0
- package/dist/client/assets/blocks-7UDKQp8H.js +1 -0
- package/dist/client/assets/bookmarks-CbBkMvXk.js +1 -0
- package/dist/client/assets/chunk-C0phanGV.js +1 -0
- package/dist/client/assets/circle-check-C_Vq3ElD.js +1 -0
- package/dist/client/assets/client-stream-contracts-ys3XhWG4.js +5 -0
- package/dist/client/assets/createLucideIcon-JV8isFFR.js +1 -0
- package/dist/client/assets/data-sources-Cm2bpNvg.js +1 -0
- package/dist/client/assets/discuss-DTjaOugw.js +1 -0
- package/dist/client/assets/dms-CGd6Eogq.js +1 -0
- package/dist/client/assets/inbox-BHlGuOy2.js +1 -0
- package/dist/client/assets/index-CEIG7S6s.js +31 -0
- package/dist/client/assets/index-bpFrQ5yG.css +1 -0
- package/dist/client/assets/likes-cLrcAQem.js +1 -0
- package/dist/client/assets/links-CM9QO1LC.js +1 -0
- package/dist/client/assets/mapbox-BxTlqXt9.js +2 -0
- package/dist/client/assets/mapbox-gl-dgf_hX3a.js +3215 -0
- package/dist/client/assets/mentions-CxKCgzIf.js +1 -0
- package/dist/client/assets/network-map-B5a3whqj.js +2 -0
- package/dist/client/assets/profile-analyze-CRtNEwlb.js +1 -0
- package/dist/client/assets/profile-hydration-client-YEtJsNTv.js +1 -0
- package/dist/client/assets/profiles._handle-C82_HoKX.js +1 -0
- package/dist/client/assets/query-client-fYu2rzRC.js +17 -0
- package/dist/client/assets/rate-limits-ScaEwdVK.js +1 -0
- package/dist/client/assets/react-dom-DHl6Ug7F.js +1 -0
- package/dist/client/assets/react-nBylq1U5.js +1 -0
- package/dist/client/assets/repeat-2-xCwHM9hD.js +1 -0
- package/dist/client/assets/routes-q7ZaW4En.js +1 -0
- package/dist/client/assets/schemas-I9r6F6ya.js +64 -0
- package/dist/client/assets/search-Dq5kYsxA.js +1 -0
- package/dist/client/assets/sparkles-CQVlvHsZ.js +1 -0
- package/dist/client/assets/styles-B8s3tt3r.css +2 -0
- package/dist/client/assets/today-CeG1O7Py.js +1 -0
- package/dist/client/assets/triangle-alert-CBL7s2pf.js +1 -0
- package/dist/client/assets/useTimelineRouteData-1HsqgvUi.js +1 -0
- package/dist/client/assets/users-CHbKlqUr.js +1 -0
- package/dist/server/assets/AvatarChip-zUDAEmjL.js +105 -0
- package/dist/server/assets/FeedState-ntW0Q2GB.js +214 -0
- package/dist/server/assets/ProfileAnalysisStream-BFvhVdhn.js +198 -0
- package/dist/server/assets/ProfilePreview-C6gAor_j.js +752 -0
- package/dist/server/assets/SavedTimelineView-BG1jnBp1.js +64 -0
- package/dist/server/assets/SmartTimestamp-DfwE1NGf.js +45 -0
- package/dist/server/assets/SyncNowButton-BOn_IBXS.js +76 -0
- package/dist/server/assets/TimelineRouteFrame-CU7ilM7U.js +85 -0
- package/dist/server/assets/TweetRichText-DhCVv1-6.js +74 -0
- package/dist/server/assets/_tanstack-start-manifest_v-C-ZRnG_n.js +148 -0
- package/dist/server/assets/account-selection-CTdFGWRk.js +44 -0
- package/dist/server/assets/blocks-C1mgGTB3.js +299 -0
- package/dist/server/assets/blocks-DSgQghqP.js +17 -0
- package/dist/server/assets/bookmarks-BJN8tykY.js +14 -0
- package/dist/server/assets/client-stream-contracts-C2xbWVjm.js +769 -0
- package/dist/server/assets/data-sources-BnA1Q94p.js +180 -0
- package/dist/server/assets/discuss-CJ1dau1j.js +15 -0
- package/dist/server/assets/discuss-QTo08NCN.js +338 -0
- package/dist/server/assets/dms-BuLj4bp8.js +368 -0
- package/dist/server/assets/dms-DAyQrVM2.js +240 -0
- package/dist/server/assets/empty-plugin-adapters-DzrEkdBF.js +5 -0
- package/dist/server/assets/inbox-CkGOaW1q.js +214 -0
- package/dist/server/assets/inbox-DpWd1EzC.js +146 -0
- package/dist/server/assets/likes-DBOAtsJU.js +14 -0
- package/dist/server/assets/links-DAhSjynu.js +240 -0
- package/dist/server/assets/links-DmH5lxHO.js +468 -0
- package/dist/server/assets/mentions-BH2MzKtB.js +26 -0
- package/dist/server/assets/network-map-D3CzRcC4.js +224 -0
- package/dist/server/assets/network-map-D5DoLOwR.js +582 -0
- package/dist/server/assets/profile-analyze-BP65PED0.js +90 -0
- package/dist/server/assets/profile-analyze-C1NoM0tV.js +9 -0
- package/dist/server/assets/profile-hydration-client-GJ4kwJS8.js +133 -0
- package/dist/server/assets/profiles._handle-DjwlN_ac.js +184 -0
- package/dist/server/assets/profiles._handle-DsLNj2Ca.js +12 -0
- package/dist/server/assets/query-client-BdauHBgd.js +923 -0
- package/dist/server/assets/rate-limits-VAr2qzYD.js +262 -0
- package/dist/server/assets/route-search-BUlQ5lGM.js +117 -0
- package/dist/server/assets/router-DqkwSPiM.js +17026 -0
- package/dist/server/assets/routes-A9c9ZhwN.js +26 -0
- package/dist/server/assets/start-FaDOTmto.js +4 -0
- package/dist/server/assets/today-B5hVM57Z.js +17 -0
- package/dist/server/assets/today-fvGiZ7ud.js +303 -0
- package/dist/server/assets/ui-qbmcMSSX.js +137 -0
- package/{src/components/useDebouncedValue.ts → dist/server/assets/useDebouncedValue-DOnA6Xcy.js} +4 -4
- package/dist/server/assets/useTimelineRouteData-27cxynxa.js +1101 -0
- package/dist/server/server.js +5520 -0
- package/package.json +27 -34
- package/playwright.config.ts +0 -35
- package/scripts/browser-perf.mjs +0 -427
- package/scripts/build-docs-site.mjs +0 -967
- package/scripts/docs-site-assets.mjs +0 -311
- package/scripts/run-vitest.mjs +0 -21
- package/scripts/sanitize-node-options.mjs +0 -23
- package/scripts/start-test-server.mjs +0 -42
- package/src/cli-moderation.ts +0 -210
- package/src/cli.ts +0 -2336
- package/src/components/AccountSwitcher.tsx +0 -186
- package/src/components/AppNav.tsx +0 -118
- package/src/components/AvatarChip.tsx +0 -58
- package/src/components/BrandMark.tsx +0 -67
- package/src/components/ConversationThread.tsx +0 -131
- package/src/components/DmWorkspace.tsx +0 -286
- package/src/components/EmbeddedTweetCard.tsx +0 -55
- package/src/components/FeedState.tsx +0 -147
- package/src/components/FloatingPreview.tsx +0 -382
- package/src/components/InboxCard.tsx +0 -152
- package/src/components/LinkPreviewCard.tsx +0 -292
- package/src/components/MarkdownViewer.tsx +0 -777
- package/src/components/ProfileAnalysisStream.tsx +0 -428
- package/src/components/ProfilePreview.tsx +0 -129
- package/src/components/SavedTimelineView.tsx +0 -155
- package/src/components/SmartTimestamp.tsx +0 -72
- package/src/components/SyncNowButton.tsx +0 -140
- package/src/components/ThemeSlider.tsx +0 -80
- package/src/components/TimelineCard.tsx +0 -533
- package/src/components/TimelineRouteFrame.tsx +0 -186
- package/src/components/TweetArticleCard.tsx +0 -66
- package/src/components/TweetMediaGrid.tsx +0 -185
- package/src/components/TweetRichText.tsx +0 -159
- package/src/components/account-selection.ts +0 -64
- package/src/components/useTimelineRouteData.ts +0 -323
- package/src/lib/account-sync-job.ts +0 -666
- package/src/lib/actions-transport.ts +0 -300
- package/src/lib/api-client.ts +0 -419
- package/src/lib/archive-finder.ts +0 -185
- package/src/lib/archive-import.ts +0 -2764
- package/src/lib/authored-live.ts +0 -1072
- package/src/lib/avatar-cache.ts +0 -349
- package/src/lib/backup.ts +0 -2741
- package/src/lib/bird-actions.ts +0 -294
- package/src/lib/bird-command.ts +0 -130
- package/src/lib/bird.ts +0 -1140
- package/src/lib/blocklist.ts +0 -136
- package/src/lib/blocks-write.ts +0 -167
- package/src/lib/blocks.ts +0 -389
- package/src/lib/bookmark-sync-job.ts +0 -536
- package/src/lib/client-cache.ts +0 -109
- package/src/lib/config.ts +0 -224
- package/src/lib/conversation-surface.ts +0 -205
- package/src/lib/data-sources.ts +0 -219
- package/src/lib/db.ts +0 -1135
- package/src/lib/dms-live.ts +0 -933
- package/src/lib/effect-runtime.ts +0 -45
- package/src/lib/follow-graph.ts +0 -1097
- package/src/lib/geocoding.ts +0 -296
- package/src/lib/http-effect.ts +0 -222
- package/src/lib/identity-search-index.ts +0 -369
- package/src/lib/inbox.ts +0 -241
- package/src/lib/link-index.ts +0 -641
- package/src/lib/link-insights.ts +0 -860
- package/src/lib/link-preview-metadata.ts +0 -754
- package/src/lib/location.ts +0 -137
- package/src/lib/media-fetch.ts +0 -860
- package/src/lib/media-includes.ts +0 -165
- package/src/lib/mention-threads-live.ts +0 -917
- package/src/lib/mentions-export.ts +0 -147
- package/src/lib/mentions-live.ts +0 -1238
- package/src/lib/moderation-target.ts +0 -216
- package/src/lib/moderation-write.ts +0 -131
- package/src/lib/mutes-write.ts +0 -167
- package/src/lib/mutes.ts +0 -84
- package/src/lib/ndjson-stream.ts +0 -106
- package/src/lib/network-map.ts +0 -382
- package/src/lib/openai.ts +0 -117
- package/src/lib/period-digest.ts +0 -1552
- package/src/lib/present.ts +0 -118
- package/src/lib/profile-affiliation-hydration.ts +0 -228
- package/src/lib/profile-affiliations.ts +0 -262
- package/src/lib/profile-analysis.ts +0 -1272
- package/src/lib/profile-bio-entities.ts +0 -318
- package/src/lib/profile-history.ts +0 -164
- package/src/lib/profile-hydration.ts +0 -180
- package/src/lib/profile-replies.ts +0 -77
- package/src/lib/profile-resolver.ts +0 -650
- package/src/lib/queries.ts +0 -2381
- package/src/lib/research.ts +0 -652
- package/src/lib/search-discussion.ts +0 -1016
- package/src/lib/seed.ts +0 -613
- package/src/lib/sqlite.ts +0 -147
- package/src/lib/sync-cache.ts +0 -65
- package/src/lib/theme-transition.ts +0 -113
- package/src/lib/theme.tsx +0 -165
- package/src/lib/timeline-collections-live.ts +0 -581
- package/src/lib/timeline-live.ts +0 -452
- package/src/lib/tweet-account-edges.ts +0 -48
- package/src/lib/tweet-lookup.ts +0 -46
- package/src/lib/tweet-render.ts +0 -430
- package/src/lib/tweet-search-live.ts +0 -566
- package/src/lib/types.ts +0 -723
- package/src/lib/ui.ts +0 -529
- package/src/lib/url-expansion-store.ts +0 -110
- package/src/lib/url-expansion.ts +0 -343
- package/src/lib/url-safety.ts +0 -220
- package/src/lib/web-sync.ts +0 -516
- package/src/lib/whois.ts +0 -1079
- package/src/lib/x-profile.ts +0 -365
- package/src/lib/x-web.ts +0 -199
- package/src/lib/xurl-rate-limits.ts +0 -267
- package/src/lib/xurl.ts +0 -1677
- package/src/routeTree.gen.ts +0 -723
- package/src/router.tsx +0 -20
- package/src/routes/__root.tsx +0 -79
- package/src/routes/api/action.tsx +0 -147
- package/src/routes/api/avatar.tsx +0 -50
- package/src/routes/api/blocks.tsx +0 -35
- package/src/routes/api/conversation.tsx +0 -45
- package/src/routes/api/data-sources.tsx +0 -24
- package/src/routes/api/inbox.tsx +0 -43
- package/src/routes/api/link-insights.tsx +0 -85
- package/src/routes/api/link-preview.tsx +0 -47
- package/src/routes/api/network-map.tsx +0 -55
- package/src/routes/api/period-digest.tsx +0 -101
- package/src/routes/api/profile-analysis.tsx +0 -97
- package/src/routes/api/profile-hydrate.tsx +0 -54
- package/src/routes/api/query.tsx +0 -115
- package/src/routes/api/search-discussion.tsx +0 -113
- package/src/routes/api/status.tsx +0 -28
- package/src/routes/api/sync.tsx +0 -105
- package/src/routes/api/xurl-rate-limits.tsx +0 -24
- package/src/routes/blocks.tsx +0 -377
- package/src/routes/bookmarks.tsx +0 -18
- package/src/routes/data-sources.tsx +0 -257
- package/src/routes/discuss.tsx +0 -419
- package/src/routes/dms.tsx +0 -432
- package/src/routes/inbox.tsx +0 -226
- package/src/routes/index.tsx +0 -32
- package/src/routes/likes.tsx +0 -18
- package/src/routes/links.tsx +0 -994
- package/src/routes/mentions.tsx +0 -32
- package/src/routes/network-map.tsx +0 -1035
- package/src/routes/profile-analyze.tsx +0 -112
- package/src/routes/profiles.$handle.tsx +0 -228
- package/src/routes/rate-limits.tsx +0 -309
- package/src/routes/today.tsx +0 -511
- package/src/styles.css +0 -257
- package/tsconfig.json +0 -29
- package/vite.config.ts +0 -29
- /package/{public → dist/client}/birdclaw-mark.png +0 -0
- /package/{public → dist/client}/favicon.ico +0 -0
- /package/{public → dist/client}/logo192.png +0 -0
- /package/{public → dist/client}/logo512.png +0 -0
- /package/{public → dist/client}/manifest.json +0 -0
- /package/{public → dist/client}/robots.txt +0 -0
package/src/routes/today.tsx
DELETED
|
@@ -1,511 +0,0 @@
|
|
|
1
|
-
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
-
import { CheckCircle2, Loader2, RefreshCw, Sparkles } from "lucide-react";
|
|
3
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
-
import { MarkdownViewer } from "#/components/MarkdownViewer";
|
|
5
|
-
import type {
|
|
6
|
-
PeriodDigestContext,
|
|
7
|
-
PeriodDigestRunResult,
|
|
8
|
-
PeriodDigestStreamEvent,
|
|
9
|
-
} from "#/lib/period-digest";
|
|
10
|
-
import type { ProfileRecord } from "#/lib/types";
|
|
11
|
-
import {
|
|
12
|
-
cx,
|
|
13
|
-
errorCopyClass,
|
|
14
|
-
pageHeaderActionsClass,
|
|
15
|
-
pageHeaderClass,
|
|
16
|
-
pageHeaderRowClass,
|
|
17
|
-
pageSubtitleClass,
|
|
18
|
-
pageTitleClass,
|
|
19
|
-
secondaryButtonClass,
|
|
20
|
-
segmentActiveClass,
|
|
21
|
-
segmentClass,
|
|
22
|
-
segmentedClass,
|
|
23
|
-
} from "#/lib/ui";
|
|
24
|
-
|
|
25
|
-
export const Route = createFileRoute("/today")({
|
|
26
|
-
component: TodayRoute,
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
type PeriodOption = "today" | "24h" | "yesterday" | "week";
|
|
30
|
-
type HydrateProfileResult = {
|
|
31
|
-
handle: string;
|
|
32
|
-
status: "hit" | "miss" | "error";
|
|
33
|
-
profile?: ProfileRecord;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
const PROFILE_HYDRATION_LIMIT = 12;
|
|
37
|
-
const PROFILE_HYDRATION_DELAY_MS = 300;
|
|
38
|
-
|
|
39
|
-
const periods: Array<{ value: PeriodOption; label: string }> = [
|
|
40
|
-
{ value: "today", label: "Today" },
|
|
41
|
-
{ value: "24h", label: "24h" },
|
|
42
|
-
{ value: "yesterday", label: "Yesterday" },
|
|
43
|
-
{ value: "week", label: "Week" },
|
|
44
|
-
];
|
|
45
|
-
|
|
46
|
-
function digestUrl(
|
|
47
|
-
period: PeriodOption,
|
|
48
|
-
includeDms: boolean,
|
|
49
|
-
refresh: boolean,
|
|
50
|
-
) {
|
|
51
|
-
const url = new URL("/api/period-digest", window.location.origin);
|
|
52
|
-
url.searchParams.set("period", period);
|
|
53
|
-
url.searchParams.set("includeDms", String(includeDms));
|
|
54
|
-
url.searchParams.set("maxTweets", "5000");
|
|
55
|
-
url.searchParams.set("maxLinks", "20");
|
|
56
|
-
// Cloudflare caps proxied requests; live timeline sync remains a separate job/UI action.
|
|
57
|
-
url.searchParams.set("liveSync", "false");
|
|
58
|
-
if (refresh) {
|
|
59
|
-
url.searchParams.set("refresh", "true");
|
|
60
|
-
}
|
|
61
|
-
return url;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async function digestRequestError(response: Response) {
|
|
65
|
-
const status = `${String(response.status)}${response.statusText ? ` ${response.statusText}` : ""}`;
|
|
66
|
-
let detail = "";
|
|
67
|
-
try {
|
|
68
|
-
const contentType = response.headers.get("content-type") ?? "";
|
|
69
|
-
if (contentType.includes("application/json")) {
|
|
70
|
-
const payload = (await response.json()) as {
|
|
71
|
-
error?: unknown;
|
|
72
|
-
message?: unknown;
|
|
73
|
-
};
|
|
74
|
-
if (typeof payload.message === "string") detail = payload.message;
|
|
75
|
-
else if (typeof payload.error === "string") detail = payload.error;
|
|
76
|
-
} else {
|
|
77
|
-
detail = (await response.text()).trim();
|
|
78
|
-
}
|
|
79
|
-
} catch {
|
|
80
|
-
detail = "";
|
|
81
|
-
}
|
|
82
|
-
if (response.status === 524) {
|
|
83
|
-
return new Error(
|
|
84
|
-
"Digest startup timed out at Cloudflare (524). Retry to open a new stream.",
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
return new Error(
|
|
88
|
-
detail
|
|
89
|
-
? `Digest request failed (${status}): ${detail}`
|
|
90
|
-
: `Digest request failed (${status})`,
|
|
91
|
-
);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function digestStreamError(cause: unknown, phase: string) {
|
|
95
|
-
const message = cause instanceof Error ? cause.message : String(cause);
|
|
96
|
-
if (
|
|
97
|
-
cause instanceof TypeError &&
|
|
98
|
-
/network error|failed to fetch|load failed/i.test(message)
|
|
99
|
-
) {
|
|
100
|
-
return `Digest connection was interrupted while ${phase.toLowerCase()}. Retry to continue.`;
|
|
101
|
-
}
|
|
102
|
-
if (cause instanceof SyntaxError) {
|
|
103
|
-
return `Digest stream returned invalid data while ${phase.toLowerCase()}. Retry to continue.`;
|
|
104
|
-
}
|
|
105
|
-
return message || "Digest failed";
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function formatCounts(context: PeriodDigestContext | null) {
|
|
109
|
-
if (!context) return "Local Twitter memory, summarized as it streams.";
|
|
110
|
-
const counts = context.counts;
|
|
111
|
-
return [
|
|
112
|
-
`${String(counts.home)} home`,
|
|
113
|
-
`${String(counts.mentions)} mentions`,
|
|
114
|
-
`${String(counts.links)} links`,
|
|
115
|
-
context.includeDms ? `${String(counts.dms)} DMs` : null,
|
|
116
|
-
]
|
|
117
|
-
.filter(Boolean)
|
|
118
|
-
.join(" · ");
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function normalizeHandle(value: string) {
|
|
122
|
-
return value.trim().replace(/^@/, "").toLowerCase();
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function collectProfilesForHydration(result: PeriodDigestRunResult) {
|
|
126
|
-
const handles = new Set<string>();
|
|
127
|
-
const tweetIds = new Set<string>();
|
|
128
|
-
for (const id of result.digest.sourceTweetIds) tweetIds.add(id);
|
|
129
|
-
for (const topic of result.digest.keyTopics) {
|
|
130
|
-
for (const id of topic.tweetIds) tweetIds.add(id);
|
|
131
|
-
}
|
|
132
|
-
for (const link of result.digest.notableLinks) {
|
|
133
|
-
for (const id of link.sourceTweetIds) tweetIds.add(id);
|
|
134
|
-
}
|
|
135
|
-
for (const item of result.digest.actionItems) {
|
|
136
|
-
if (item.tweetId) tweetIds.add(item.tweetId);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const tweetsById = new Map(
|
|
140
|
-
result.context.tweets.flatMap((tweet) => [
|
|
141
|
-
[tweet.id, tweet],
|
|
142
|
-
[`tweet_${tweet.id}`, tweet],
|
|
143
|
-
]),
|
|
144
|
-
);
|
|
145
|
-
for (const id of tweetIds) {
|
|
146
|
-
const tweet = tweetsById.get(id);
|
|
147
|
-
if (!tweet) continue;
|
|
148
|
-
const handle = normalizeHandle(tweet.author);
|
|
149
|
-
if (handle) handles.add(handle);
|
|
150
|
-
if (handles.size >= PROFILE_HYDRATION_LIMIT) return [...handles];
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
for (const tweet of result.context.tweets) {
|
|
154
|
-
const handle = normalizeHandle(tweet.author);
|
|
155
|
-
if (handle) handles.add(handle);
|
|
156
|
-
if (handles.size >= PROFILE_HYDRATION_LIMIT) return [...handles];
|
|
157
|
-
}
|
|
158
|
-
return [...handles];
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function applyHydratedProfilesToContext(
|
|
162
|
-
context: PeriodDigestContext,
|
|
163
|
-
profilesByHandle: Map<string, ProfileRecord>,
|
|
164
|
-
) {
|
|
165
|
-
let changed = false;
|
|
166
|
-
const tweets = context.tweets.map((tweet) => {
|
|
167
|
-
const profile = profilesByHandle.get(normalizeHandle(tweet.author));
|
|
168
|
-
if (!profile || profile === tweet.authorProfile) return tweet;
|
|
169
|
-
changed = true;
|
|
170
|
-
return {
|
|
171
|
-
...tweet,
|
|
172
|
-
author: profile.handle,
|
|
173
|
-
name: profile.displayName,
|
|
174
|
-
authorProfile: profile,
|
|
175
|
-
};
|
|
176
|
-
});
|
|
177
|
-
return changed ? { ...context, tweets } : context;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function applyHydratedProfilesToResult(
|
|
181
|
-
result: PeriodDigestRunResult,
|
|
182
|
-
profiles: ProfileRecord[],
|
|
183
|
-
) {
|
|
184
|
-
const profilesByHandle = new Map(
|
|
185
|
-
profiles.map((profile) => [normalizeHandle(profile.handle), profile]),
|
|
186
|
-
);
|
|
187
|
-
if (profilesByHandle.size === 0) return result;
|
|
188
|
-
const context = applyHydratedProfilesToContext(
|
|
189
|
-
result.context,
|
|
190
|
-
profilesByHandle,
|
|
191
|
-
);
|
|
192
|
-
return context === result.context ? result : { ...result, context };
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function useDigestStream(period: PeriodOption, includeDms: boolean) {
|
|
196
|
-
const [markdown, setMarkdown] = useState("");
|
|
197
|
-
const [context, setContext] = useState<PeriodDigestContext | null>(null);
|
|
198
|
-
const [result, setResult] = useState<PeriodDigestRunResult | null>(null);
|
|
199
|
-
const [error, setError] = useState<string | null>(null);
|
|
200
|
-
const [loading, setLoading] = useState(false);
|
|
201
|
-
const [status, setStatus] = useState("Starting digest");
|
|
202
|
-
const abortRef = useRef<AbortController | null>(null);
|
|
203
|
-
const requestIdRef = useRef(0);
|
|
204
|
-
const hydratedHandlesRef = useRef(new Set<string>());
|
|
205
|
-
const hydratedProfilesRef = useRef(new Map<string, ProfileRecord>());
|
|
206
|
-
|
|
207
|
-
const run = useCallback(
|
|
208
|
-
(refresh = false) => {
|
|
209
|
-
abortRef.current?.abort();
|
|
210
|
-
const controller = new AbortController();
|
|
211
|
-
const requestId = requestIdRef.current + 1;
|
|
212
|
-
requestIdRef.current = requestId;
|
|
213
|
-
abortRef.current = controller;
|
|
214
|
-
const isActiveRequest = () =>
|
|
215
|
-
abortRef.current === controller &&
|
|
216
|
-
requestIdRef.current === requestId &&
|
|
217
|
-
!controller.signal.aborted;
|
|
218
|
-
setMarkdown("");
|
|
219
|
-
setContext(null);
|
|
220
|
-
setResult(null);
|
|
221
|
-
setError(null);
|
|
222
|
-
setStatus("Starting digest");
|
|
223
|
-
setLoading(true);
|
|
224
|
-
let latestStatus = "Starting digest";
|
|
225
|
-
let completed = false;
|
|
226
|
-
|
|
227
|
-
fetch(digestUrl(period, includeDms, refresh), {
|
|
228
|
-
cache: "no-store",
|
|
229
|
-
signal: controller.signal,
|
|
230
|
-
})
|
|
231
|
-
.then(async (response) => {
|
|
232
|
-
if (!response.ok) {
|
|
233
|
-
throw await digestRequestError(response);
|
|
234
|
-
}
|
|
235
|
-
if (!response.body) {
|
|
236
|
-
throw new Error("Digest request failed: empty response body");
|
|
237
|
-
}
|
|
238
|
-
const reader = response.body.getReader();
|
|
239
|
-
const decoder = new TextDecoder();
|
|
240
|
-
let buffer = "";
|
|
241
|
-
const pump = (): Promise<void> =>
|
|
242
|
-
reader.read().then(({ done, value }) => {
|
|
243
|
-
if (!isActiveRequest()) return;
|
|
244
|
-
if (done) {
|
|
245
|
-
if (!completed) {
|
|
246
|
-
throw new Error(
|
|
247
|
-
`Digest connection closed while ${latestStatus.toLowerCase()}. Retry to continue.`,
|
|
248
|
-
);
|
|
249
|
-
}
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
buffer += decoder.decode(value, { stream: true });
|
|
253
|
-
let newline = buffer.indexOf("\n");
|
|
254
|
-
while (newline >= 0) {
|
|
255
|
-
const line = buffer.slice(0, newline).trim();
|
|
256
|
-
buffer = buffer.slice(newline + 1);
|
|
257
|
-
if (line) {
|
|
258
|
-
const event = JSON.parse(line) as PeriodDigestStreamEvent;
|
|
259
|
-
if (!isActiveRequest()) return;
|
|
260
|
-
if (event.type === "status") {
|
|
261
|
-
latestStatus = event.detail
|
|
262
|
-
? `${event.label} · ${event.detail}`
|
|
263
|
-
: event.label;
|
|
264
|
-
setStatus(latestStatus);
|
|
265
|
-
} else if (event.type === "start") {
|
|
266
|
-
setContext(event.context);
|
|
267
|
-
} else if (event.type === "delta") {
|
|
268
|
-
latestStatus = "Streaming AI summary";
|
|
269
|
-
setStatus(latestStatus);
|
|
270
|
-
setMarkdown((current) => current + event.delta);
|
|
271
|
-
} else if (event.type === "done") {
|
|
272
|
-
completed = true;
|
|
273
|
-
setResult(event.result);
|
|
274
|
-
setContext(event.result.context);
|
|
275
|
-
setMarkdown(event.result.markdown);
|
|
276
|
-
setStatus(
|
|
277
|
-
event.result.cached ? "Loaded cached report" : "Ready",
|
|
278
|
-
);
|
|
279
|
-
} else if (event.type === "error") {
|
|
280
|
-
completed = true;
|
|
281
|
-
setError(event.error);
|
|
282
|
-
setStatus("Digest failed");
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
newline = buffer.indexOf("\n");
|
|
286
|
-
}
|
|
287
|
-
return pump();
|
|
288
|
-
});
|
|
289
|
-
return pump();
|
|
290
|
-
})
|
|
291
|
-
.catch((cause: unknown) => {
|
|
292
|
-
if (!isActiveRequest()) return;
|
|
293
|
-
setError(digestStreamError(cause, latestStatus));
|
|
294
|
-
setStatus("Digest failed");
|
|
295
|
-
})
|
|
296
|
-
.finally(() => {
|
|
297
|
-
if (isActiveRequest()) {
|
|
298
|
-
setLoading(false);
|
|
299
|
-
}
|
|
300
|
-
});
|
|
301
|
-
},
|
|
302
|
-
[includeDms, period],
|
|
303
|
-
);
|
|
304
|
-
|
|
305
|
-
useEffect(() => {
|
|
306
|
-
run(false);
|
|
307
|
-
return () => abortRef.current?.abort();
|
|
308
|
-
}, [run]);
|
|
309
|
-
|
|
310
|
-
useEffect(() => {
|
|
311
|
-
if (!result) return;
|
|
312
|
-
if (hydratedProfilesRef.current.size > 0) {
|
|
313
|
-
const cachedProfiles = [...hydratedProfilesRef.current.values()];
|
|
314
|
-
setResult((current) =>
|
|
315
|
-
current
|
|
316
|
-
? applyHydratedProfilesToResult(current, cachedProfiles)
|
|
317
|
-
: current,
|
|
318
|
-
);
|
|
319
|
-
setContext((current) =>
|
|
320
|
-
current
|
|
321
|
-
? applyHydratedProfilesToContext(current, hydratedProfilesRef.current)
|
|
322
|
-
: current,
|
|
323
|
-
);
|
|
324
|
-
}
|
|
325
|
-
const handles = collectProfilesForHydration(result).filter(
|
|
326
|
-
(handle) => !hydratedHandlesRef.current.has(handle),
|
|
327
|
-
);
|
|
328
|
-
if (handles.length === 0) return;
|
|
329
|
-
|
|
330
|
-
const controller = new AbortController();
|
|
331
|
-
const url = new URL("/api/profile-hydrate", window.location.origin);
|
|
332
|
-
url.searchParams.set("handles", handles.join(","));
|
|
333
|
-
|
|
334
|
-
let idleId: number | null = null;
|
|
335
|
-
const runHydration = () => {
|
|
336
|
-
fetch(url, { signal: controller.signal })
|
|
337
|
-
.then((response) => response.json())
|
|
338
|
-
.then((response: { results?: HydrateProfileResult[] }) => {
|
|
339
|
-
for (const handle of handles) hydratedHandlesRef.current.add(handle);
|
|
340
|
-
const profiles =
|
|
341
|
-
response.results
|
|
342
|
-
?.map((item) => item.profile)
|
|
343
|
-
.filter((profile): profile is ProfileRecord =>
|
|
344
|
-
Boolean(profile),
|
|
345
|
-
) ?? [];
|
|
346
|
-
if (profiles.length === 0) return;
|
|
347
|
-
for (const profile of profiles) {
|
|
348
|
-
hydratedProfilesRef.current.set(
|
|
349
|
-
normalizeHandle(profile.handle),
|
|
350
|
-
profile,
|
|
351
|
-
);
|
|
352
|
-
}
|
|
353
|
-
setResult((current) =>
|
|
354
|
-
current
|
|
355
|
-
? applyHydratedProfilesToResult(current, profiles)
|
|
356
|
-
: current,
|
|
357
|
-
);
|
|
358
|
-
const profilesByHandle = new Map(
|
|
359
|
-
profiles.map((profile) => [
|
|
360
|
-
normalizeHandle(profile.handle),
|
|
361
|
-
profile,
|
|
362
|
-
]),
|
|
363
|
-
);
|
|
364
|
-
setContext((current) =>
|
|
365
|
-
current
|
|
366
|
-
? applyHydratedProfilesToContext(current, profilesByHandle)
|
|
367
|
-
: current,
|
|
368
|
-
);
|
|
369
|
-
})
|
|
370
|
-
.catch((error: unknown) => {
|
|
371
|
-
if (error instanceof DOMException && error.name === "AbortError") {
|
|
372
|
-
return;
|
|
373
|
-
}
|
|
374
|
-
console.warn("Profile hydration failed", error);
|
|
375
|
-
});
|
|
376
|
-
};
|
|
377
|
-
const timer = window.setTimeout(() => {
|
|
378
|
-
if ("requestIdleCallback" in window) {
|
|
379
|
-
idleId = window.requestIdleCallback(runHydration, { timeout: 2500 });
|
|
380
|
-
} else {
|
|
381
|
-
runHydration();
|
|
382
|
-
}
|
|
383
|
-
}, PROFILE_HYDRATION_DELAY_MS);
|
|
384
|
-
|
|
385
|
-
return () => {
|
|
386
|
-
controller.abort();
|
|
387
|
-
window.clearTimeout(timer);
|
|
388
|
-
if (idleId !== null && "cancelIdleCallback" in window) {
|
|
389
|
-
window.cancelIdleCallback(idleId);
|
|
390
|
-
}
|
|
391
|
-
};
|
|
392
|
-
}, [result]);
|
|
393
|
-
|
|
394
|
-
return { context, error, loading, markdown, result, run, status };
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
function TodayRoute() {
|
|
398
|
-
const [period, setPeriod] = useState<PeriodOption>("today");
|
|
399
|
-
const [includeDms, setIncludeDms] = useState(false);
|
|
400
|
-
const { context, error, loading, markdown, result, run, status } =
|
|
401
|
-
useDigestStream(period, includeDms);
|
|
402
|
-
const sourceLabel = useMemo(
|
|
403
|
-
() => formatCounts(result?.context ?? context),
|
|
404
|
-
[context, result],
|
|
405
|
-
);
|
|
406
|
-
|
|
407
|
-
return (
|
|
408
|
-
<div className="flex min-h-screen flex-col">
|
|
409
|
-
<header className={pageHeaderClass}>
|
|
410
|
-
<div className={pageHeaderRowClass}>
|
|
411
|
-
<div className="min-w-0">
|
|
412
|
-
<h1 className={pageTitleClass}>What happened</h1>
|
|
413
|
-
<p className={pageSubtitleClass}>{sourceLabel}</p>
|
|
414
|
-
</div>
|
|
415
|
-
<div className={pageHeaderActionsClass}>
|
|
416
|
-
<button
|
|
417
|
-
type="button"
|
|
418
|
-
className={secondaryButtonClass}
|
|
419
|
-
onClick={() => run(true)}
|
|
420
|
-
disabled={loading}
|
|
421
|
-
>
|
|
422
|
-
<RefreshCw
|
|
423
|
-
className={cx("size-4", loading && "animate-spin")}
|
|
424
|
-
aria-hidden="true"
|
|
425
|
-
/>
|
|
426
|
-
Refresh
|
|
427
|
-
</button>
|
|
428
|
-
</div>
|
|
429
|
-
</div>
|
|
430
|
-
<div className="flex flex-wrap items-center gap-2 px-4 pb-3">
|
|
431
|
-
<div className={segmentedClass} aria-label="Digest period">
|
|
432
|
-
{periods.map((item) => (
|
|
433
|
-
<button
|
|
434
|
-
key={item.value}
|
|
435
|
-
type="button"
|
|
436
|
-
className={cx(
|
|
437
|
-
segmentClass,
|
|
438
|
-
period === item.value && segmentActiveClass,
|
|
439
|
-
)}
|
|
440
|
-
onClick={() => setPeriod(item.value)}
|
|
441
|
-
>
|
|
442
|
-
{item.label}
|
|
443
|
-
</button>
|
|
444
|
-
))}
|
|
445
|
-
</div>
|
|
446
|
-
<label className="inline-flex items-center gap-2 rounded-full border border-[var(--line)] px-3 py-1 text-[13px] font-medium text-[var(--ink-soft)]">
|
|
447
|
-
<input
|
|
448
|
-
type="checkbox"
|
|
449
|
-
checked={includeDms}
|
|
450
|
-
onChange={(event) => setIncludeDms(event.currentTarget.checked)}
|
|
451
|
-
/>
|
|
452
|
-
DMs
|
|
453
|
-
</label>
|
|
454
|
-
</div>
|
|
455
|
-
</header>
|
|
456
|
-
|
|
457
|
-
{error ? (
|
|
458
|
-
<div
|
|
459
|
-
className={cx(
|
|
460
|
-
errorCopyClass,
|
|
461
|
-
"flex items-center justify-between gap-3",
|
|
462
|
-
)}
|
|
463
|
-
role="alert"
|
|
464
|
-
>
|
|
465
|
-
<span>{error}</span>
|
|
466
|
-
<button
|
|
467
|
-
className="shrink-0 font-semibold underline underline-offset-2"
|
|
468
|
-
onClick={() => run(true)}
|
|
469
|
-
type="button"
|
|
470
|
-
>
|
|
471
|
-
Retry
|
|
472
|
-
</button>
|
|
473
|
-
</div>
|
|
474
|
-
) : null}
|
|
475
|
-
|
|
476
|
-
<div className="border-b border-[var(--line)] px-4 py-2 text-[13px] text-[var(--ink-soft)]">
|
|
477
|
-
<span className="inline-flex items-center gap-1">
|
|
478
|
-
{loading ? (
|
|
479
|
-
<Loader2 className="size-4 animate-spin" aria-hidden="true" />
|
|
480
|
-
) : markdown ? (
|
|
481
|
-
<CheckCircle2 className="size-4" aria-hidden="true" />
|
|
482
|
-
) : (
|
|
483
|
-
<Sparkles className="size-4" aria-hidden="true" />
|
|
484
|
-
)}
|
|
485
|
-
{loading
|
|
486
|
-
? status
|
|
487
|
-
: result
|
|
488
|
-
? `${result.cached ? "Cached" : "Ready"} · ${result.context.window.label}`
|
|
489
|
-
: error
|
|
490
|
-
? "Digest failed"
|
|
491
|
-
: "Ready"}
|
|
492
|
-
</span>
|
|
493
|
-
</div>
|
|
494
|
-
|
|
495
|
-
{markdown ? (
|
|
496
|
-
<MarkdownViewer
|
|
497
|
-
context={result?.context ?? context}
|
|
498
|
-
markdown={markdown}
|
|
499
|
-
/>
|
|
500
|
-
) : (
|
|
501
|
-
<div className="px-4 py-5 text-[14px] text-[var(--ink-soft)]">
|
|
502
|
-
{loading
|
|
503
|
-
? status
|
|
504
|
-
: error
|
|
505
|
-
? "No digest was generated. Retry to start a new run."
|
|
506
|
-
: "Waiting for the first tokens..."}
|
|
507
|
-
</div>
|
|
508
|
-
)}
|
|
509
|
-
</div>
|
|
510
|
-
);
|
|
511
|
-
}
|