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
|
@@ -0,0 +1,769 @@
|
|
|
1
|
+
import { hn as tweetLinkClass, m as cx, vn as tweetMentionClass } from "./ui-qbmcMSSX.js";
|
|
2
|
+
import { r as formatCompactNumber, t as AvatarChip } from "./AvatarChip-zUDAEmjL.js";
|
|
3
|
+
import { m as renderTweetPlainText, n as useFloatingPreview, r as useAvatarPreload, s as safeHttpUrl, t as ProfilePreview } from "./ProfilePreview-C6gAor_j.js";
|
|
4
|
+
import { t as SmartTimestamp } from "./SmartTimestamp-DfwE1NGf.js";
|
|
5
|
+
import { Fragment, useCallback, useEffect, useRef, useState } from "react";
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
//#region src/components/MarkdownCitations.tsx
|
|
9
|
+
function normalizeTweetReference(value) {
|
|
10
|
+
return value.trim().replace(/^\(/, "").replace(/\)$/, "").replace(/^tweet_/, "");
|
|
11
|
+
}
|
|
12
|
+
function isNumericTweetReference(value) {
|
|
13
|
+
return /^\d{12,25}$/.test(normalizeTweetReference(value));
|
|
14
|
+
}
|
|
15
|
+
function tweetReferencesFromToken(token) {
|
|
16
|
+
return Array.from(token.matchAll(/\b(?:tweet_)?[A-Za-z0-9_:-]{3,}\b/g)).map((match) => match[0]).filter((value) => value.startsWith("tweet_") || /^\d{12,25}$/.test(value));
|
|
17
|
+
}
|
|
18
|
+
function adjacentParenthesizedTweetReferences(value, cursor) {
|
|
19
|
+
const references = [];
|
|
20
|
+
let nextCursor = cursor;
|
|
21
|
+
while (nextCursor < value.length) {
|
|
22
|
+
const match = /^\s+\((?:\s*(?:tweet_[A-Za-z0-9_:-]+|\d{12,25})\s*,?)+\)/.exec(value.slice(nextCursor));
|
|
23
|
+
if (!match) break;
|
|
24
|
+
references.push(...tweetReferencesFromToken(match[0]));
|
|
25
|
+
nextCursor += match[0].length;
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
references,
|
|
29
|
+
cursor: nextCursor
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function trailingReadableBounds(value, options = {}) {
|
|
33
|
+
let start = 0;
|
|
34
|
+
for (const separator of [
|
|
35
|
+
". ",
|
|
36
|
+
"? ",
|
|
37
|
+
"! ",
|
|
38
|
+
"; ",
|
|
39
|
+
": "
|
|
40
|
+
]) {
|
|
41
|
+
const index = value.lastIndexOf(separator);
|
|
42
|
+
if (index >= 0) start = Math.max(start, index + separator.length);
|
|
43
|
+
}
|
|
44
|
+
let end = value.length;
|
|
45
|
+
while (start < end && /\s/.test(value[start] ?? "")) start += 1;
|
|
46
|
+
while (end > start && /\s/.test(value[end - 1] ?? "")) end -= 1;
|
|
47
|
+
let clauseStart = start;
|
|
48
|
+
for (const separator of [
|
|
49
|
+
", with ",
|
|
50
|
+
", while ",
|
|
51
|
+
", and "
|
|
52
|
+
]) {
|
|
53
|
+
const index = value.lastIndexOf(separator, end);
|
|
54
|
+
if (index >= start) {
|
|
55
|
+
clauseStart = index + 2;
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (options.preferClause !== false && (clauseStart > start || end - start > 140)) {
|
|
60
|
+
while (clauseStart < end && /\s/.test(value[clauseStart] ?? "")) clauseStart += 1;
|
|
61
|
+
if (end > clauseStart) start = clauseStart;
|
|
62
|
+
}
|
|
63
|
+
return end > start ? {
|
|
64
|
+
start,
|
|
65
|
+
end
|
|
66
|
+
} : null;
|
|
67
|
+
}
|
|
68
|
+
function trimBullet(value) {
|
|
69
|
+
return value.replace(/^[-*]\s+/, "");
|
|
70
|
+
}
|
|
71
|
+
function skipRedundantSourceWords(value, cursor) {
|
|
72
|
+
const match = /^((?:\s+source\b)+)(?=\s*(?:[.,;:!?)]|$))/i.exec(value.slice(cursor));
|
|
73
|
+
return match ? cursor + match[0].length : cursor;
|
|
74
|
+
}
|
|
75
|
+
function getTweetUrl(tweet) {
|
|
76
|
+
return tweet.url || `https://x.com/${tweet.author}/status/${tweet.id}`;
|
|
77
|
+
}
|
|
78
|
+
function getFallbackTweetUrl(tweetId) {
|
|
79
|
+
return `https://x.com/i/status/${normalizeTweetReference(tweetId)}`;
|
|
80
|
+
}
|
|
81
|
+
function TweetSourceLink({ children, href, onClick, describedBy }) {
|
|
82
|
+
return /* @__PURE__ */ jsx("a", {
|
|
83
|
+
"aria-describedby": describedBy,
|
|
84
|
+
className: "rounded-sm px-0.5 text-[var(--accent)] hover:bg-[var(--accent-soft)] hover:no-underline",
|
|
85
|
+
href,
|
|
86
|
+
onClick,
|
|
87
|
+
rel: "noreferrer",
|
|
88
|
+
target: "_blank",
|
|
89
|
+
children
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
function TweetPreviewToken({ tweet, children }) {
|
|
93
|
+
const preview = useFloatingPreview();
|
|
94
|
+
useAvatarPreload(preview.referenceRef, tweet.authorProfile.id, tweet.authorProfile.avatarUrl);
|
|
95
|
+
const article = tweet.entities?.article;
|
|
96
|
+
const renderedText = renderTweetPlainText(tweet.text, tweet.entities ?? {});
|
|
97
|
+
const previewText = article ? [article.title, article.previewText].filter((value, index, values) => Boolean(value) && values.indexOf(value) === index).join("\n\n") : renderedText;
|
|
98
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
99
|
+
ref: preview.referenceRef,
|
|
100
|
+
className: "inline align-baseline",
|
|
101
|
+
...preview.referenceProps,
|
|
102
|
+
children: [/* @__PURE__ */ jsx(TweetSourceLink, {
|
|
103
|
+
describedBy: preview.open ? preview.floatingId : void 0,
|
|
104
|
+
href: getTweetUrl(tweet),
|
|
105
|
+
onClick: (event) => {
|
|
106
|
+
preview.closePreview();
|
|
107
|
+
event.currentTarget.blur();
|
|
108
|
+
},
|
|
109
|
+
children
|
|
110
|
+
}), preview.open ? /* @__PURE__ */ jsx("span", {
|
|
111
|
+
id: preview.floatingId,
|
|
112
|
+
ref: preview.floatingRef,
|
|
113
|
+
className: "fixed z-40 w-[360px] overflow-y-auto rounded-2xl border border-[var(--line)] bg-[var(--bg-elevated)] p-3 text-left text-[14px] leading-[1.4] text-[var(--ink)] shadow-[0_14px_40px_var(--shadow-strong)]",
|
|
114
|
+
role: "tooltip",
|
|
115
|
+
style: preview.floatingStyle,
|
|
116
|
+
...preview.floatingProps,
|
|
117
|
+
children: /* @__PURE__ */ jsxs("span", {
|
|
118
|
+
className: "block",
|
|
119
|
+
"data-floating-preview-content": true,
|
|
120
|
+
children: [
|
|
121
|
+
/* @__PURE__ */ jsxs("span", {
|
|
122
|
+
className: "mb-2 flex items-center gap-2",
|
|
123
|
+
children: [/* @__PURE__ */ jsx(AvatarChip, {
|
|
124
|
+
avatarUrl: tweet.authorProfile.avatarUrl,
|
|
125
|
+
hue: tweet.authorProfile.avatarHue,
|
|
126
|
+
name: tweet.name,
|
|
127
|
+
profileId: tweet.authorProfile.id,
|
|
128
|
+
size: "small"
|
|
129
|
+
}), /* @__PURE__ */ jsxs("span", {
|
|
130
|
+
className: "min-w-0",
|
|
131
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
132
|
+
className: "block truncate font-bold",
|
|
133
|
+
children: tweet.name
|
|
134
|
+
}), /* @__PURE__ */ jsxs("span", {
|
|
135
|
+
className: "block truncate text-[12px] text-[var(--ink-soft)]",
|
|
136
|
+
children: [
|
|
137
|
+
"@",
|
|
138
|
+
tweet.author,
|
|
139
|
+
" · ",
|
|
140
|
+
/* @__PURE__ */ jsx(SmartTimestamp, { value: tweet.createdAt })
|
|
141
|
+
]
|
|
142
|
+
})]
|
|
143
|
+
})]
|
|
144
|
+
}),
|
|
145
|
+
/* @__PURE__ */ jsx("span", {
|
|
146
|
+
className: "line-clamp-6 whitespace-pre-wrap [overflow-wrap:anywhere]",
|
|
147
|
+
children: previewText
|
|
148
|
+
}),
|
|
149
|
+
/* @__PURE__ */ jsxs("span", {
|
|
150
|
+
className: "mt-2 flex gap-3 text-[12px] text-[var(--ink-soft)]",
|
|
151
|
+
children: [
|
|
152
|
+
/* @__PURE__ */ jsx("span", { children: tweet.source }),
|
|
153
|
+
tweet.likeCount > 0 ? /* @__PURE__ */ jsxs("span", { children: [formatCompactNumber(tweet.likeCount), " likes"] }) : null,
|
|
154
|
+
tweet.needsReply ? /* @__PURE__ */ jsx("span", { children: "reply open" }) : null
|
|
155
|
+
]
|
|
156
|
+
})
|
|
157
|
+
]
|
|
158
|
+
})
|
|
159
|
+
}) : null]
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
function additionalDirectCitationLinks(references, key) {
|
|
163
|
+
return references.slice(1).flatMap((reference, index) => [index === 0 ? " " : ", ", /* @__PURE__ */ jsx(TweetSourceLink, {
|
|
164
|
+
href: getFallbackTweetUrl(reference),
|
|
165
|
+
children: `source ${String(index + 2)}`
|
|
166
|
+
}, `${key}-direct-source-${String(index + 2)}`)]);
|
|
167
|
+
}
|
|
168
|
+
function splitReadableForSourceLinks(value, count) {
|
|
169
|
+
if (count < 2) return null;
|
|
170
|
+
const separators = Array.from(value.matchAll(/,\s+and\s+|,\s+or\s+|;\s+|,\s+|\s+and\s+|\s+or\s+/g));
|
|
171
|
+
if (separators.length < count - 1) return null;
|
|
172
|
+
const selected = separators.slice(-(count - 1));
|
|
173
|
+
const parts = [];
|
|
174
|
+
let cursor = 0;
|
|
175
|
+
for (const separator of selected) {
|
|
176
|
+
const index = separator.index;
|
|
177
|
+
if (index === void 0) return null;
|
|
178
|
+
const text = value.slice(cursor, index);
|
|
179
|
+
if (!text.trim()) return null;
|
|
180
|
+
parts.push({
|
|
181
|
+
text,
|
|
182
|
+
separatorAfter: separator[0]
|
|
183
|
+
});
|
|
184
|
+
cursor = index + separator[0].length;
|
|
185
|
+
}
|
|
186
|
+
const lastText = value.slice(cursor);
|
|
187
|
+
if (!lastText.trim()) return null;
|
|
188
|
+
parts.push({
|
|
189
|
+
text: lastText,
|
|
190
|
+
separatorAfter: ""
|
|
191
|
+
});
|
|
192
|
+
return parts.length === count ? parts : null;
|
|
193
|
+
}
|
|
194
|
+
function linkedCitationParts(readable, tweets, key) {
|
|
195
|
+
const parts = splitReadableForSourceLinks(readable, tweets.length);
|
|
196
|
+
if (!parts) {
|
|
197
|
+
const tweet = tweets[0];
|
|
198
|
+
if (!tweet) return [];
|
|
199
|
+
return [/* @__PURE__ */ jsx(TweetPreviewToken, {
|
|
200
|
+
tweet,
|
|
201
|
+
children: readable
|
|
202
|
+
}, key), ...additionalCitationLinks(tweets, key)];
|
|
203
|
+
}
|
|
204
|
+
return parts.flatMap((part, index) => {
|
|
205
|
+
const tweet = tweets[index];
|
|
206
|
+
if (!tweet) return [];
|
|
207
|
+
return [/* @__PURE__ */ jsx(TweetPreviewToken, {
|
|
208
|
+
tweet,
|
|
209
|
+
children: part.text
|
|
210
|
+
}, `${key}-part-${String(index)}`), part.separatorAfter];
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
function linkedDirectCitationParts(readable, references, key) {
|
|
214
|
+
const parts = splitReadableForSourceLinks(readable, references.length);
|
|
215
|
+
if (!parts) {
|
|
216
|
+
const reference = references[0];
|
|
217
|
+
if (!reference) return [];
|
|
218
|
+
return [/* @__PURE__ */ jsx(TweetSourceLink, {
|
|
219
|
+
href: getFallbackTweetUrl(reference),
|
|
220
|
+
children: readable
|
|
221
|
+
}, key), ...additionalDirectCitationLinks(references, key)];
|
|
222
|
+
}
|
|
223
|
+
return parts.flatMap((part, index) => {
|
|
224
|
+
const reference = references[index];
|
|
225
|
+
if (!reference) return [];
|
|
226
|
+
return [/* @__PURE__ */ jsx(TweetSourceLink, {
|
|
227
|
+
href: getFallbackTweetUrl(reference),
|
|
228
|
+
children: part.text
|
|
229
|
+
}, `${key}-direct-part-${String(index)}`), part.separatorAfter];
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
function additionalCitationLinks(tweets, key) {
|
|
233
|
+
return tweets.slice(1).flatMap((tweet, index) => [index === 0 ? " " : ", ", /* @__PURE__ */ jsx(TweetPreviewToken, {
|
|
234
|
+
tweet,
|
|
235
|
+
children: `source ${String(index + 2)}`
|
|
236
|
+
}, `${key}-source-${String(index + 2)}`)]);
|
|
237
|
+
}
|
|
238
|
+
function fallbackCitationLinks(tweets, key) {
|
|
239
|
+
return tweets.flatMap((tweet, index) => [index === 0 ? "" : ", ", /* @__PURE__ */ jsx(TweetPreviewToken, {
|
|
240
|
+
tweet,
|
|
241
|
+
children: tweets.length === 1 ? "source" : `source ${String(index + 1)}`
|
|
242
|
+
}, `${key}-fallback-${String(index + 1)}`)]);
|
|
243
|
+
}
|
|
244
|
+
function linkTrailingCitationText(nodes, tweets, key) {
|
|
245
|
+
const tweet = tweets[0];
|
|
246
|
+
if (!tweet) return false;
|
|
247
|
+
const last = nodes.at(-1);
|
|
248
|
+
if (typeof last !== "string") return false;
|
|
249
|
+
const match = /(["“][^"”]+["”])(\s*)$/.exec(last);
|
|
250
|
+
if (match) {
|
|
251
|
+
const quoted = match[1];
|
|
252
|
+
const trailing = match[2] ?? "";
|
|
253
|
+
const before = last.slice(0, match.index);
|
|
254
|
+
nodes[nodes.length - 1] = before;
|
|
255
|
+
nodes.push(/* @__PURE__ */ jsx(TweetPreviewToken, {
|
|
256
|
+
tweet,
|
|
257
|
+
children: quoted
|
|
258
|
+
}, key), ...additionalCitationLinks(tweets, key), /^\s*$/.test(trailing) ? "" : trailing);
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
const bounds = trailingReadableBounds(last, { preferClause: tweets.length === 1 });
|
|
262
|
+
if (!bounds) return false;
|
|
263
|
+
const before = last.slice(0, bounds.start);
|
|
264
|
+
const readable = last.slice(bounds.start, bounds.end);
|
|
265
|
+
const trailing = last.slice(bounds.end);
|
|
266
|
+
nodes[nodes.length - 1] = before;
|
|
267
|
+
nodes.push(...linkedCitationParts(readable, tweets, key), /^\s*$/.test(trailing) ? "" : trailing);
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
function linkTrailingDirectCitationText(nodes, references, key) {
|
|
271
|
+
if (!references[0]) return false;
|
|
272
|
+
const last = nodes.at(-1);
|
|
273
|
+
if (typeof last !== "string") return false;
|
|
274
|
+
const bounds = trailingReadableBounds(last, { preferClause: references.length === 1 });
|
|
275
|
+
if (!bounds) return false;
|
|
276
|
+
const before = last.slice(0, bounds.start);
|
|
277
|
+
const readable = last.slice(bounds.start, bounds.end);
|
|
278
|
+
const trailing = last.slice(bounds.end);
|
|
279
|
+
nodes[nodes.length - 1] = before;
|
|
280
|
+
nodes.push(...linkedDirectCitationParts(readable, references, key), /^\s*$/.test(trailing) ? "" : trailing);
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
function renderInline(text, lookup) {
|
|
284
|
+
const pattern = /(\[[^\]\n]+\]\s*\(https?:\/\/[^\s)]+\)|\*\*[^*]+\*\*|@[A-Za-z0-9_]{1,20}|\((?:\s*(?:tweet_[A-Za-z0-9_:-]+|\d{12,25})\s*,?)+\)|\btweet_[A-Za-z0-9_:-]+\b|\b\d{12,25}\b)/g;
|
|
285
|
+
const nodes = [];
|
|
286
|
+
let cursor = 0;
|
|
287
|
+
let match;
|
|
288
|
+
while (match = pattern.exec(text)) {
|
|
289
|
+
const token = match[0];
|
|
290
|
+
const tokenKey = `${token}-${String(match.index)}`;
|
|
291
|
+
if (match.index > cursor) nodes.push(text.slice(cursor, match.index));
|
|
292
|
+
cursor = match.index + token.length;
|
|
293
|
+
if (token.startsWith("**") && token.endsWith("**")) {
|
|
294
|
+
nodes.push(/* @__PURE__ */ jsx("strong", { children: token.slice(2, -2) }, tokenKey));
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
const markdownLink = /^\[([^\]\n]+)\]\s*\((https?:\/\/[^\s)]+)\)$/.exec(token);
|
|
298
|
+
if (markdownLink) {
|
|
299
|
+
const href = safeHttpUrl(markdownLink[2]);
|
|
300
|
+
nodes.push(href ? /* @__PURE__ */ jsx("a", {
|
|
301
|
+
className: tweetLinkClass,
|
|
302
|
+
href,
|
|
303
|
+
rel: "noreferrer",
|
|
304
|
+
target: "_blank",
|
|
305
|
+
children: markdownLink[1]
|
|
306
|
+
}, tokenKey) : markdownLink[1]);
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
if (token.startsWith("@")) {
|
|
310
|
+
const profile = lookup.profilesByHandle.get(token.slice(1).toLowerCase());
|
|
311
|
+
nodes.push(profile ? /* @__PURE__ */ jsx(ProfilePreview, {
|
|
312
|
+
profile,
|
|
313
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
314
|
+
className: tweetMentionClass,
|
|
315
|
+
children: token
|
|
316
|
+
})
|
|
317
|
+
}, tokenKey) : /* @__PURE__ */ jsx("a", {
|
|
318
|
+
className: tweetMentionClass,
|
|
319
|
+
href: `/profiles/${encodeURIComponent(token.slice(1))}`,
|
|
320
|
+
children: token
|
|
321
|
+
}, tokenKey));
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
const isParenthesizedTweetRef = token.startsWith("(") && token.endsWith(")");
|
|
325
|
+
let references = tweetReferencesFromToken(token);
|
|
326
|
+
if (isParenthesizedTweetRef) {
|
|
327
|
+
const adjacent = adjacentParenthesizedTweetReferences(text, cursor);
|
|
328
|
+
const groupedReferences = [...references, ...adjacent.references];
|
|
329
|
+
if (adjacent.references.length > 0 && groupedReferences.every(isNumericTweetReference)) {
|
|
330
|
+
references = groupedReferences;
|
|
331
|
+
cursor = adjacent.cursor;
|
|
332
|
+
pattern.lastIndex = cursor;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
const resolvedTweets = references.map((reference) => lookup.tweetsById.get(normalizeTweetReference(reference)));
|
|
336
|
+
const allReferencesResolved = references.length > 0 && resolvedTweets.every(Boolean);
|
|
337
|
+
const tweets = resolvedTweets.filter((tweet) => Boolean(tweet));
|
|
338
|
+
const tweet = tweets[0];
|
|
339
|
+
if (isParenthesizedTweetRef && references.length > 1 && !allReferencesResolved) {
|
|
340
|
+
if (references.every(isNumericTweetReference)) {
|
|
341
|
+
const cursorAfterSourceWords = skipRedundantSourceWords(text, cursor);
|
|
342
|
+
if (linkTrailingDirectCitationText(nodes, references, tokenKey)) {
|
|
343
|
+
cursor = cursorAfterSourceWords;
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
nodes.push(...references.flatMap((reference, index) => [index === 0 ? "" : ", ", /* @__PURE__ */ jsx(TweetSourceLink, {
|
|
347
|
+
href: getFallbackTweetUrl(reference),
|
|
348
|
+
children: `source ${String(index + 1)}`
|
|
349
|
+
}, `${tokenKey}-direct-${String(index)}`)]));
|
|
350
|
+
cursor = cursorAfterSourceWords;
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
nodes.push(token);
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
if (tweet && isParenthesizedTweetRef && allReferencesResolved && linkTrailingCitationText(nodes, tweets, tokenKey)) {
|
|
357
|
+
cursor = skipRedundantSourceWords(text, cursor);
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
if (tweet && isParenthesizedTweetRef && allReferencesResolved) {
|
|
361
|
+
nodes.push(...fallbackCitationLinks(tweets, tokenKey));
|
|
362
|
+
cursor = skipRedundantSourceWords(text, cursor);
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
if (tweet) nodes.push(/* @__PURE__ */ jsx(TweetPreviewToken, {
|
|
366
|
+
tweet,
|
|
367
|
+
children: isParenthesizedTweetRef ? "source" : token
|
|
368
|
+
}, tokenKey));
|
|
369
|
+
else if (isParenthesizedTweetRef && references.length === 1 && isNumericTweetReference(references[0] ?? "")) {
|
|
370
|
+
const cursorAfterSourceWords = skipRedundantSourceWords(text, cursor);
|
|
371
|
+
if (linkTrailingDirectCitationText(nodes, references, tokenKey)) {
|
|
372
|
+
cursor = cursorAfterSourceWords;
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
nodes.push(/* @__PURE__ */ jsx(TweetSourceLink, {
|
|
376
|
+
href: getFallbackTweetUrl(references[0]),
|
|
377
|
+
children: "source"
|
|
378
|
+
}, `${tokenKey}-direct`));
|
|
379
|
+
cursor = cursorAfterSourceWords;
|
|
380
|
+
continue;
|
|
381
|
+
} else nodes.push(token);
|
|
382
|
+
}
|
|
383
|
+
if (cursor < text.length) nodes.push(text.slice(cursor));
|
|
384
|
+
return nodes.map((node, index) => /* @__PURE__ */ jsx(Fragment, { children: node }, typeof node === "string" ? `${node}-${String(index)}` : index));
|
|
385
|
+
}
|
|
386
|
+
function isProfileAnalysisContext(context) {
|
|
387
|
+
return "conversations" in context && "profile" in context;
|
|
388
|
+
}
|
|
389
|
+
function addLookupTweet(tweetsById, profilesByHandle, tweet) {
|
|
390
|
+
const normalized = normalizeTweetReference(tweet.id);
|
|
391
|
+
tweetsById.set(normalized, tweet);
|
|
392
|
+
tweetsById.set(`tweet_${normalized}`, tweet);
|
|
393
|
+
profilesByHandle.set(tweet.author.toLowerCase(), tweet.authorProfile);
|
|
394
|
+
}
|
|
395
|
+
function syntheticProfileForConversationTweet(tweet) {
|
|
396
|
+
return {
|
|
397
|
+
id: tweet.profileId,
|
|
398
|
+
handle: tweet.author,
|
|
399
|
+
displayName: tweet.name || tweet.author,
|
|
400
|
+
bio: tweet.bio,
|
|
401
|
+
followersCount: tweet.followersCount,
|
|
402
|
+
avatarHue: 210,
|
|
403
|
+
avatarUrl: tweet.avatarUrl,
|
|
404
|
+
createdAt: tweet.createdAt
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
function profileAnalysisTweetToCitation(tweet, profile) {
|
|
408
|
+
return {
|
|
409
|
+
id: tweet.id,
|
|
410
|
+
url: tweet.url,
|
|
411
|
+
source: "authored",
|
|
412
|
+
author: profile.handle,
|
|
413
|
+
name: profile.displayName,
|
|
414
|
+
authorProfile: profile,
|
|
415
|
+
createdAt: tweet.createdAt,
|
|
416
|
+
text: tweet.text,
|
|
417
|
+
entities: tweet.entities,
|
|
418
|
+
likeCount: tweet.likeCount,
|
|
419
|
+
liked: false,
|
|
420
|
+
bookmarked: false,
|
|
421
|
+
needsReply: false
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
function conversationTweetToCitation(tweet) {
|
|
425
|
+
const authorProfile = syntheticProfileForConversationTweet(tweet);
|
|
426
|
+
return {
|
|
427
|
+
id: tweet.id,
|
|
428
|
+
url: tweet.url,
|
|
429
|
+
source: "mentions",
|
|
430
|
+
author: tweet.author,
|
|
431
|
+
name: tweet.name || tweet.author,
|
|
432
|
+
authorProfile,
|
|
433
|
+
createdAt: tweet.createdAt,
|
|
434
|
+
text: tweet.text,
|
|
435
|
+
entities: tweet.entities,
|
|
436
|
+
likeCount: tweet.likeCount,
|
|
437
|
+
liked: false,
|
|
438
|
+
bookmarked: false,
|
|
439
|
+
needsReply: false
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
function buildLookup(context) {
|
|
443
|
+
const tweetsById = /* @__PURE__ */ new Map();
|
|
444
|
+
const profilesByHandle = /* @__PURE__ */ new Map();
|
|
445
|
+
if (!context) return {
|
|
446
|
+
tweetsById,
|
|
447
|
+
profilesByHandle
|
|
448
|
+
};
|
|
449
|
+
if (isProfileAnalysisContext(context)) {
|
|
450
|
+
profilesByHandle.set(context.profile.handle.toLowerCase(), context.profile);
|
|
451
|
+
for (const profile of context.profiles ?? []) profilesByHandle.set(profile.handle.toLowerCase(), profile);
|
|
452
|
+
for (const tweet of context.tweets) addLookupTweet(tweetsById, profilesByHandle, profileAnalysisTweetToCitation(tweet, context.profile));
|
|
453
|
+
for (const tweet of context.conversations) addLookupTweet(tweetsById, profilesByHandle, conversationTweetToCitation(tweet));
|
|
454
|
+
return {
|
|
455
|
+
tweetsById,
|
|
456
|
+
profilesByHandle
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
for (const tweet of context.tweets) addLookupTweet(tweetsById, profilesByHandle, tweet);
|
|
460
|
+
return {
|
|
461
|
+
tweetsById,
|
|
462
|
+
profilesByHandle
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
//#endregion
|
|
466
|
+
//#region src/components/MarkdownViewer.tsx
|
|
467
|
+
function MarkdownViewer({ markdown, context, className }) {
|
|
468
|
+
const lookup = buildLookup(context);
|
|
469
|
+
const lines = markdown.replace(/\]\s*\r?\n\s*\((https?:\/\/[^\s)]+)\)/g, "]($1)").split(/\r?\n/);
|
|
470
|
+
const nodes = [];
|
|
471
|
+
let listItems = [];
|
|
472
|
+
const flushList = () => {
|
|
473
|
+
if (listItems.length === 0) return;
|
|
474
|
+
nodes.push(/* @__PURE__ */ jsx("ul", {
|
|
475
|
+
className: "my-2.5 flex list-disc flex-col gap-1.5 pl-5 first:mt-0 marker:text-[var(--ink-soft)]",
|
|
476
|
+
children: listItems.map((item, index) => /* @__PURE__ */ jsx("li", { children: item }, String(index)))
|
|
477
|
+
}, `list-${String(nodes.length)}`));
|
|
478
|
+
listItems = [];
|
|
479
|
+
};
|
|
480
|
+
for (const line of lines) {
|
|
481
|
+
const trimmed = line.trim();
|
|
482
|
+
if (!trimmed) {
|
|
483
|
+
flushList();
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
if (trimmed.startsWith("### ")) {
|
|
487
|
+
flushList();
|
|
488
|
+
nodes.push(/* @__PURE__ */ jsx("h3", {
|
|
489
|
+
className: "mt-5 mb-1.5 text-[14px] font-bold uppercase tracking-wide text-[var(--ink-soft)] first:mt-0",
|
|
490
|
+
children: renderInline(trimmed.slice(4), lookup)
|
|
491
|
+
}, `h3-${String(nodes.length)}`));
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
if (trimmed.startsWith("## ")) {
|
|
495
|
+
flushList();
|
|
496
|
+
nodes.push(/* @__PURE__ */ jsx("h2", {
|
|
497
|
+
className: "mt-6 mb-2 text-[18px] font-bold text-[var(--ink)] first:mt-0",
|
|
498
|
+
children: renderInline(trimmed.slice(3), lookup)
|
|
499
|
+
}, `h2-${String(nodes.length)}`));
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
if (trimmed.startsWith("# ")) {
|
|
503
|
+
flushList();
|
|
504
|
+
nodes.push(/* @__PURE__ */ jsx("h1", {
|
|
505
|
+
className: "mt-0 mb-2.5 text-[20px] font-bold text-[var(--ink)]",
|
|
506
|
+
children: renderInline(trimmed.slice(2), lookup)
|
|
507
|
+
}, `h1-${String(nodes.length)}`));
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
if (/^[-*]\s+/.test(trimmed)) {
|
|
511
|
+
listItems.push(renderInline(trimBullet(trimmed), lookup));
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
flushList();
|
|
515
|
+
nodes.push(/* @__PURE__ */ jsx("p", {
|
|
516
|
+
className: "my-2.5 whitespace-pre-wrap first:mt-0 [overflow-wrap:anywhere]",
|
|
517
|
+
children: renderInline(trimmed, lookup)
|
|
518
|
+
}, `p-${String(nodes.length)}`));
|
|
519
|
+
}
|
|
520
|
+
flushList();
|
|
521
|
+
return /* @__PURE__ */ jsx("article", {
|
|
522
|
+
className: cx("max-w-none px-4 py-3 text-[15px] leading-[1.55] text-[var(--ink)]", className),
|
|
523
|
+
children: nodes
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
//#endregion
|
|
527
|
+
//#region src/lib/client-http.ts
|
|
528
|
+
/** Build a useful error from JSON or plain-text non-OK responses. */
|
|
529
|
+
async function responseError(response, { label, statusMessages }) {
|
|
530
|
+
const specialMessage = statusMessages?.[response.status];
|
|
531
|
+
if (specialMessage) return new Error(specialMessage);
|
|
532
|
+
const status = `${String(response.status)}${response.statusText ? ` ${response.statusText}` : ""}`;
|
|
533
|
+
let detail = "";
|
|
534
|
+
try {
|
|
535
|
+
if ((response.headers.get("content-type") ?? "").includes("application/json")) {
|
|
536
|
+
const payload = await response.json();
|
|
537
|
+
if (payload && typeof payload === "object") {
|
|
538
|
+
const record = payload;
|
|
539
|
+
if (typeof record.message === "string") detail = record.message;
|
|
540
|
+
else if (typeof record.error === "string") detail = record.error;
|
|
541
|
+
}
|
|
542
|
+
} else detail = (await response.text()).trim();
|
|
543
|
+
} catch {
|
|
544
|
+
detail = "";
|
|
545
|
+
}
|
|
546
|
+
return /* @__PURE__ */ new Error(detail ? `${label} (${status}): ${detail}` : `${label} (${status})`);
|
|
547
|
+
}
|
|
548
|
+
//#endregion
|
|
549
|
+
//#region src/lib/client-ndjson.ts
|
|
550
|
+
function abortError(signal) {
|
|
551
|
+
return signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted", "AbortError");
|
|
552
|
+
}
|
|
553
|
+
async function readWithAbort(reader, signal) {
|
|
554
|
+
if (!signal) return reader.read();
|
|
555
|
+
if (signal.aborted) throw abortError(signal);
|
|
556
|
+
return new Promise((resolve, reject) => {
|
|
557
|
+
const onAbort = () => {
|
|
558
|
+
reader.cancel(signal.reason).catch(() => void 0);
|
|
559
|
+
reject(abortError(signal));
|
|
560
|
+
};
|
|
561
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
562
|
+
reader.read().then(resolve, reject).finally(() => {
|
|
563
|
+
signal.removeEventListener("abort", onAbort);
|
|
564
|
+
});
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
/** Consume and validate a newline-delimited JSON response body. */
|
|
568
|
+
async function consumeNdjson({ body, schema, onEvent, isTerminal = () => false, signal, prematureEofError = () => /* @__PURE__ */ new Error("Stream closed before a terminal event was received") }) {
|
|
569
|
+
const reader = body.getReader();
|
|
570
|
+
const decoder = new TextDecoder();
|
|
571
|
+
let buffer = "";
|
|
572
|
+
let terminal = false;
|
|
573
|
+
const consumeLine = async (line) => {
|
|
574
|
+
const trimmed = line.trim();
|
|
575
|
+
if (!trimmed) return;
|
|
576
|
+
const event = schema.parse(JSON.parse(trimmed));
|
|
577
|
+
await onEvent(event);
|
|
578
|
+
if (isTerminal(event)) terminal = true;
|
|
579
|
+
};
|
|
580
|
+
try {
|
|
581
|
+
for (;;) {
|
|
582
|
+
const { done, value } = await readWithAbort(reader, signal);
|
|
583
|
+
if (done) break;
|
|
584
|
+
buffer += decoder.decode(value, { stream: true });
|
|
585
|
+
let newline = buffer.indexOf("\n");
|
|
586
|
+
while (newline >= 0) {
|
|
587
|
+
await consumeLine(buffer.slice(0, newline));
|
|
588
|
+
buffer = buffer.slice(newline + 1);
|
|
589
|
+
newline = buffer.indexOf("\n");
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
buffer += decoder.decode();
|
|
593
|
+
await consumeLine(buffer);
|
|
594
|
+
if (!terminal) throw prematureEofError();
|
|
595
|
+
} finally {
|
|
596
|
+
reader.releaseLock();
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
//#endregion
|
|
600
|
+
//#region src/components/useNdjsonRun.ts
|
|
601
|
+
function defaultErrorMessage(cause) {
|
|
602
|
+
return cause instanceof Error ? cause.message : String(cause);
|
|
603
|
+
}
|
|
604
|
+
function useNdjsonRun({ schema, request, onStart, onEvent, onError, isTerminal, errorLabel, emptyBodyMessage, prematureEofError, formatError = defaultErrorMessage, statusMessages }) {
|
|
605
|
+
const [error, setError] = useState(null);
|
|
606
|
+
const [loading, setLoading] = useState(false);
|
|
607
|
+
const abortRef = useRef(null);
|
|
608
|
+
const requestIdRef = useRef(0);
|
|
609
|
+
const run = useCallback((...args) => {
|
|
610
|
+
abortRef.current?.abort();
|
|
611
|
+
const controller = new AbortController();
|
|
612
|
+
const requestId = requestIdRef.current + 1;
|
|
613
|
+
requestIdRef.current = requestId;
|
|
614
|
+
abortRef.current = controller;
|
|
615
|
+
const isActive = () => abortRef.current === controller && requestIdRef.current === requestId && !controller.signal.aborted;
|
|
616
|
+
const context = {
|
|
617
|
+
signal: controller.signal,
|
|
618
|
+
isActive
|
|
619
|
+
};
|
|
620
|
+
setError(null);
|
|
621
|
+
setLoading(true);
|
|
622
|
+
onStart?.(context, ...args);
|
|
623
|
+
request(controller.signal, ...args).then(async (response) => {
|
|
624
|
+
if (!isActive()) return;
|
|
625
|
+
if (!response.ok) throw await responseError(response, {
|
|
626
|
+
label: errorLabel,
|
|
627
|
+
statusMessages
|
|
628
|
+
});
|
|
629
|
+
if (!response.body) throw new Error(emptyBodyMessage);
|
|
630
|
+
await consumeNdjson({
|
|
631
|
+
body: response.body,
|
|
632
|
+
schema,
|
|
633
|
+
signal: controller.signal,
|
|
634
|
+
isTerminal,
|
|
635
|
+
prematureEofError,
|
|
636
|
+
onEvent: async (event) => {
|
|
637
|
+
if (isActive()) await onEvent(event, context);
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
}).catch((cause) => {
|
|
641
|
+
if (isActive()) {
|
|
642
|
+
onError?.(cause);
|
|
643
|
+
setError(formatError(cause));
|
|
644
|
+
}
|
|
645
|
+
}).finally(() => {
|
|
646
|
+
if (isActive()) setLoading(false);
|
|
647
|
+
});
|
|
648
|
+
}, [
|
|
649
|
+
emptyBodyMessage,
|
|
650
|
+
errorLabel,
|
|
651
|
+
formatError,
|
|
652
|
+
isTerminal,
|
|
653
|
+
onEvent,
|
|
654
|
+
onError,
|
|
655
|
+
onStart,
|
|
656
|
+
prematureEofError,
|
|
657
|
+
request,
|
|
658
|
+
schema,
|
|
659
|
+
statusMessages
|
|
660
|
+
]);
|
|
661
|
+
useEffect(() => () => abortRef.current?.abort(), []);
|
|
662
|
+
return {
|
|
663
|
+
error,
|
|
664
|
+
loading,
|
|
665
|
+
run,
|
|
666
|
+
setError
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
//#endregion
|
|
670
|
+
//#region src/lib/client-stream-contracts.ts
|
|
671
|
+
var statusEventSchema = z.object({
|
|
672
|
+
type: z.literal("status"),
|
|
673
|
+
label: z.string(),
|
|
674
|
+
detail: z.string().optional()
|
|
675
|
+
});
|
|
676
|
+
var deltaEventSchema = z.object({
|
|
677
|
+
type: z.literal("delta"),
|
|
678
|
+
delta: z.string()
|
|
679
|
+
});
|
|
680
|
+
var errorEventSchema = z.object({
|
|
681
|
+
type: z.literal("error"),
|
|
682
|
+
error: z.string()
|
|
683
|
+
});
|
|
684
|
+
var periodContextSchema = z.looseObject({
|
|
685
|
+
includeDms: z.boolean(),
|
|
686
|
+
counts: z.looseObject({
|
|
687
|
+
home: z.number(),
|
|
688
|
+
mentions: z.number(),
|
|
689
|
+
links: z.number(),
|
|
690
|
+
dms: z.number()
|
|
691
|
+
}),
|
|
692
|
+
tweets: z.array(z.unknown())
|
|
693
|
+
});
|
|
694
|
+
var discussionContextSchema = z.looseObject({
|
|
695
|
+
includeDms: z.boolean(),
|
|
696
|
+
counts: z.looseObject({
|
|
697
|
+
search: z.number(),
|
|
698
|
+
home: z.number(),
|
|
699
|
+
mentions: z.number(),
|
|
700
|
+
authored: z.number(),
|
|
701
|
+
likes: z.number(),
|
|
702
|
+
bookmarks: z.number(),
|
|
703
|
+
dms: z.number()
|
|
704
|
+
}),
|
|
705
|
+
tweets: z.array(z.unknown())
|
|
706
|
+
});
|
|
707
|
+
var profileContextSchema = z.looseObject({
|
|
708
|
+
handle: z.string(),
|
|
709
|
+
profile: z.looseObject({ handle: z.string() }),
|
|
710
|
+
profiles: z.array(z.unknown()).optional(),
|
|
711
|
+
tweets: z.array(z.unknown()),
|
|
712
|
+
conversations: z.array(z.unknown()),
|
|
713
|
+
counts: z.looseObject({
|
|
714
|
+
tweets: z.number(),
|
|
715
|
+
conversationTweets: z.number(),
|
|
716
|
+
conversationsScanned: z.number()
|
|
717
|
+
})
|
|
718
|
+
});
|
|
719
|
+
var runResultSchema = (context) => z.looseObject({
|
|
720
|
+
context,
|
|
721
|
+
markdown: z.string(),
|
|
722
|
+
cached: z.boolean()
|
|
723
|
+
});
|
|
724
|
+
var periodDigestStreamEventSchema = z.discriminatedUnion("type", [
|
|
725
|
+
statusEventSchema,
|
|
726
|
+
z.object({
|
|
727
|
+
type: z.literal("start"),
|
|
728
|
+
context: periodContextSchema,
|
|
729
|
+
cached: z.boolean()
|
|
730
|
+
}),
|
|
731
|
+
deltaEventSchema,
|
|
732
|
+
z.object({
|
|
733
|
+
type: z.literal("done"),
|
|
734
|
+
result: runResultSchema(periodContextSchema)
|
|
735
|
+
}),
|
|
736
|
+
errorEventSchema
|
|
737
|
+
]);
|
|
738
|
+
var searchDiscussionStreamEventSchema = z.discriminatedUnion("type", [
|
|
739
|
+
z.object({
|
|
740
|
+
type: z.literal("start"),
|
|
741
|
+
context: discussionContextSchema,
|
|
742
|
+
cached: z.boolean()
|
|
743
|
+
}),
|
|
744
|
+
deltaEventSchema,
|
|
745
|
+
z.object({
|
|
746
|
+
type: z.literal("done"),
|
|
747
|
+
result: runResultSchema(discussionContextSchema)
|
|
748
|
+
}),
|
|
749
|
+
errorEventSchema
|
|
750
|
+
]);
|
|
751
|
+
var profileAnalysisStreamEventSchema = z.discriminatedUnion("type", [
|
|
752
|
+
statusEventSchema,
|
|
753
|
+
z.object({
|
|
754
|
+
type: z.literal("start"),
|
|
755
|
+
context: profileContextSchema,
|
|
756
|
+
cached: z.boolean()
|
|
757
|
+
}),
|
|
758
|
+
deltaEventSchema,
|
|
759
|
+
z.object({
|
|
760
|
+
type: z.literal("done"),
|
|
761
|
+
result: runResultSchema(profileContextSchema)
|
|
762
|
+
}),
|
|
763
|
+
errorEventSchema
|
|
764
|
+
]);
|
|
765
|
+
function isTerminalStreamEvent(event) {
|
|
766
|
+
return event.type === "done" || event.type === "error";
|
|
767
|
+
}
|
|
768
|
+
//#endregion
|
|
769
|
+
export { useNdjsonRun as a, searchDiscussionStreamEventSchema as i, periodDigestStreamEventSchema as n, MarkdownViewer as o, profileAnalysisStreamEventSchema as r, isTerminalStreamEvent as t };
|