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.
Files changed (247) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +10 -8
  3. package/bin/birdclaw.mjs +4 -66
  4. package/dist/cli/birdclaw.js +28165 -0
  5. package/dist/client/assets/AvatarChip-DLoqM5xD.js +1 -0
  6. package/dist/client/assets/ProfilePreview-BejAuYsJ.js +1 -0
  7. package/dist/client/assets/SavedTimelineView-BIS7HKpp.js +1 -0
  8. package/dist/client/assets/SmartTimestamp-BR0-9se2.js +1 -0
  9. package/dist/client/assets/TimelineRouteFrame-CyGPj0eM.js +1 -0
  10. package/dist/client/assets/blocks-7UDKQp8H.js +1 -0
  11. package/dist/client/assets/bookmarks-CbBkMvXk.js +1 -0
  12. package/dist/client/assets/chunk-C0phanGV.js +1 -0
  13. package/dist/client/assets/circle-check-C_Vq3ElD.js +1 -0
  14. package/dist/client/assets/client-stream-contracts-ys3XhWG4.js +5 -0
  15. package/dist/client/assets/createLucideIcon-JV8isFFR.js +1 -0
  16. package/dist/client/assets/data-sources-Cm2bpNvg.js +1 -0
  17. package/dist/client/assets/discuss-DTjaOugw.js +1 -0
  18. package/dist/client/assets/dms-CGd6Eogq.js +1 -0
  19. package/dist/client/assets/inbox-BHlGuOy2.js +1 -0
  20. package/dist/client/assets/index-CEIG7S6s.js +31 -0
  21. package/dist/client/assets/index-bpFrQ5yG.css +1 -0
  22. package/dist/client/assets/likes-cLrcAQem.js +1 -0
  23. package/dist/client/assets/links-CM9QO1LC.js +1 -0
  24. package/dist/client/assets/mapbox-BxTlqXt9.js +2 -0
  25. package/dist/client/assets/mapbox-gl-dgf_hX3a.js +3215 -0
  26. package/dist/client/assets/mentions-CxKCgzIf.js +1 -0
  27. package/dist/client/assets/network-map-B5a3whqj.js +2 -0
  28. package/dist/client/assets/profile-analyze-CRtNEwlb.js +1 -0
  29. package/dist/client/assets/profile-hydration-client-YEtJsNTv.js +1 -0
  30. package/dist/client/assets/profiles._handle-C82_HoKX.js +1 -0
  31. package/dist/client/assets/query-client-fYu2rzRC.js +17 -0
  32. package/dist/client/assets/rate-limits-ScaEwdVK.js +1 -0
  33. package/dist/client/assets/react-dom-DHl6Ug7F.js +1 -0
  34. package/dist/client/assets/react-nBylq1U5.js +1 -0
  35. package/dist/client/assets/repeat-2-xCwHM9hD.js +1 -0
  36. package/dist/client/assets/routes-q7ZaW4En.js +1 -0
  37. package/dist/client/assets/schemas-I9r6F6ya.js +64 -0
  38. package/dist/client/assets/search-Dq5kYsxA.js +1 -0
  39. package/dist/client/assets/sparkles-CQVlvHsZ.js +1 -0
  40. package/dist/client/assets/styles-B8s3tt3r.css +2 -0
  41. package/dist/client/assets/today-CeG1O7Py.js +1 -0
  42. package/dist/client/assets/triangle-alert-CBL7s2pf.js +1 -0
  43. package/dist/client/assets/useTimelineRouteData-1HsqgvUi.js +1 -0
  44. package/dist/client/assets/users-CHbKlqUr.js +1 -0
  45. package/dist/server/assets/AvatarChip-zUDAEmjL.js +105 -0
  46. package/dist/server/assets/FeedState-ntW0Q2GB.js +214 -0
  47. package/dist/server/assets/ProfileAnalysisStream-BFvhVdhn.js +198 -0
  48. package/dist/server/assets/ProfilePreview-C6gAor_j.js +752 -0
  49. package/dist/server/assets/SavedTimelineView-BG1jnBp1.js +64 -0
  50. package/dist/server/assets/SmartTimestamp-DfwE1NGf.js +45 -0
  51. package/dist/server/assets/SyncNowButton-BOn_IBXS.js +76 -0
  52. package/dist/server/assets/TimelineRouteFrame-CU7ilM7U.js +85 -0
  53. package/dist/server/assets/TweetRichText-DhCVv1-6.js +74 -0
  54. package/dist/server/assets/_tanstack-start-manifest_v-C-ZRnG_n.js +148 -0
  55. package/dist/server/assets/account-selection-CTdFGWRk.js +44 -0
  56. package/dist/server/assets/blocks-C1mgGTB3.js +299 -0
  57. package/dist/server/assets/blocks-DSgQghqP.js +17 -0
  58. package/dist/server/assets/bookmarks-BJN8tykY.js +14 -0
  59. package/dist/server/assets/client-stream-contracts-C2xbWVjm.js +769 -0
  60. package/dist/server/assets/data-sources-BnA1Q94p.js +180 -0
  61. package/dist/server/assets/discuss-CJ1dau1j.js +15 -0
  62. package/dist/server/assets/discuss-QTo08NCN.js +338 -0
  63. package/dist/server/assets/dms-BuLj4bp8.js +368 -0
  64. package/dist/server/assets/dms-DAyQrVM2.js +240 -0
  65. package/dist/server/assets/empty-plugin-adapters-DzrEkdBF.js +5 -0
  66. package/dist/server/assets/inbox-CkGOaW1q.js +214 -0
  67. package/dist/server/assets/inbox-DpWd1EzC.js +146 -0
  68. package/dist/server/assets/likes-DBOAtsJU.js +14 -0
  69. package/dist/server/assets/links-DAhSjynu.js +240 -0
  70. package/dist/server/assets/links-DmH5lxHO.js +468 -0
  71. package/dist/server/assets/mentions-BH2MzKtB.js +26 -0
  72. package/dist/server/assets/network-map-D3CzRcC4.js +224 -0
  73. package/dist/server/assets/network-map-D5DoLOwR.js +582 -0
  74. package/dist/server/assets/profile-analyze-BP65PED0.js +90 -0
  75. package/dist/server/assets/profile-analyze-C1NoM0tV.js +9 -0
  76. package/dist/server/assets/profile-hydration-client-GJ4kwJS8.js +133 -0
  77. package/dist/server/assets/profiles._handle-DjwlN_ac.js +184 -0
  78. package/dist/server/assets/profiles._handle-DsLNj2Ca.js +12 -0
  79. package/dist/server/assets/query-client-BdauHBgd.js +923 -0
  80. package/dist/server/assets/rate-limits-VAr2qzYD.js +262 -0
  81. package/dist/server/assets/route-search-BUlQ5lGM.js +117 -0
  82. package/dist/server/assets/router-DqkwSPiM.js +17026 -0
  83. package/dist/server/assets/routes-A9c9ZhwN.js +26 -0
  84. package/dist/server/assets/start-FaDOTmto.js +4 -0
  85. package/dist/server/assets/today-B5hVM57Z.js +17 -0
  86. package/dist/server/assets/today-fvGiZ7ud.js +303 -0
  87. package/dist/server/assets/ui-qbmcMSSX.js +137 -0
  88. package/{src/components/useDebouncedValue.ts → dist/server/assets/useDebouncedValue-DOnA6Xcy.js} +4 -4
  89. package/dist/server/assets/useTimelineRouteData-27cxynxa.js +1101 -0
  90. package/dist/server/server.js +5520 -0
  91. package/package.json +27 -34
  92. package/playwright.config.ts +0 -35
  93. package/scripts/browser-perf.mjs +0 -427
  94. package/scripts/build-docs-site.mjs +0 -967
  95. package/scripts/docs-site-assets.mjs +0 -311
  96. package/scripts/run-vitest.mjs +0 -21
  97. package/scripts/sanitize-node-options.mjs +0 -23
  98. package/scripts/start-test-server.mjs +0 -42
  99. package/src/cli-moderation.ts +0 -210
  100. package/src/cli.ts +0 -2336
  101. package/src/components/AccountSwitcher.tsx +0 -186
  102. package/src/components/AppNav.tsx +0 -118
  103. package/src/components/AvatarChip.tsx +0 -58
  104. package/src/components/BrandMark.tsx +0 -67
  105. package/src/components/ConversationThread.tsx +0 -131
  106. package/src/components/DmWorkspace.tsx +0 -286
  107. package/src/components/EmbeddedTweetCard.tsx +0 -55
  108. package/src/components/FeedState.tsx +0 -147
  109. package/src/components/FloatingPreview.tsx +0 -382
  110. package/src/components/InboxCard.tsx +0 -152
  111. package/src/components/LinkPreviewCard.tsx +0 -292
  112. package/src/components/MarkdownViewer.tsx +0 -777
  113. package/src/components/ProfileAnalysisStream.tsx +0 -428
  114. package/src/components/ProfilePreview.tsx +0 -129
  115. package/src/components/SavedTimelineView.tsx +0 -155
  116. package/src/components/SmartTimestamp.tsx +0 -72
  117. package/src/components/SyncNowButton.tsx +0 -140
  118. package/src/components/ThemeSlider.tsx +0 -80
  119. package/src/components/TimelineCard.tsx +0 -533
  120. package/src/components/TimelineRouteFrame.tsx +0 -186
  121. package/src/components/TweetArticleCard.tsx +0 -66
  122. package/src/components/TweetMediaGrid.tsx +0 -185
  123. package/src/components/TweetRichText.tsx +0 -159
  124. package/src/components/account-selection.ts +0 -64
  125. package/src/components/useTimelineRouteData.ts +0 -323
  126. package/src/lib/account-sync-job.ts +0 -666
  127. package/src/lib/actions-transport.ts +0 -300
  128. package/src/lib/api-client.ts +0 -419
  129. package/src/lib/archive-finder.ts +0 -185
  130. package/src/lib/archive-import.ts +0 -2764
  131. package/src/lib/authored-live.ts +0 -1072
  132. package/src/lib/avatar-cache.ts +0 -349
  133. package/src/lib/backup.ts +0 -2741
  134. package/src/lib/bird-actions.ts +0 -294
  135. package/src/lib/bird-command.ts +0 -130
  136. package/src/lib/bird.ts +0 -1140
  137. package/src/lib/blocklist.ts +0 -136
  138. package/src/lib/blocks-write.ts +0 -167
  139. package/src/lib/blocks.ts +0 -389
  140. package/src/lib/bookmark-sync-job.ts +0 -536
  141. package/src/lib/client-cache.ts +0 -109
  142. package/src/lib/config.ts +0 -224
  143. package/src/lib/conversation-surface.ts +0 -205
  144. package/src/lib/data-sources.ts +0 -219
  145. package/src/lib/db.ts +0 -1135
  146. package/src/lib/dms-live.ts +0 -933
  147. package/src/lib/effect-runtime.ts +0 -45
  148. package/src/lib/follow-graph.ts +0 -1097
  149. package/src/lib/geocoding.ts +0 -296
  150. package/src/lib/http-effect.ts +0 -222
  151. package/src/lib/identity-search-index.ts +0 -369
  152. package/src/lib/inbox.ts +0 -241
  153. package/src/lib/link-index.ts +0 -641
  154. package/src/lib/link-insights.ts +0 -860
  155. package/src/lib/link-preview-metadata.ts +0 -754
  156. package/src/lib/location.ts +0 -137
  157. package/src/lib/media-fetch.ts +0 -860
  158. package/src/lib/media-includes.ts +0 -165
  159. package/src/lib/mention-threads-live.ts +0 -917
  160. package/src/lib/mentions-export.ts +0 -147
  161. package/src/lib/mentions-live.ts +0 -1238
  162. package/src/lib/moderation-target.ts +0 -216
  163. package/src/lib/moderation-write.ts +0 -131
  164. package/src/lib/mutes-write.ts +0 -167
  165. package/src/lib/mutes.ts +0 -84
  166. package/src/lib/ndjson-stream.ts +0 -106
  167. package/src/lib/network-map.ts +0 -382
  168. package/src/lib/openai.ts +0 -117
  169. package/src/lib/period-digest.ts +0 -1552
  170. package/src/lib/present.ts +0 -118
  171. package/src/lib/profile-affiliation-hydration.ts +0 -228
  172. package/src/lib/profile-affiliations.ts +0 -262
  173. package/src/lib/profile-analysis.ts +0 -1272
  174. package/src/lib/profile-bio-entities.ts +0 -318
  175. package/src/lib/profile-history.ts +0 -164
  176. package/src/lib/profile-hydration.ts +0 -180
  177. package/src/lib/profile-replies.ts +0 -77
  178. package/src/lib/profile-resolver.ts +0 -650
  179. package/src/lib/queries.ts +0 -2381
  180. package/src/lib/research.ts +0 -652
  181. package/src/lib/search-discussion.ts +0 -1016
  182. package/src/lib/seed.ts +0 -613
  183. package/src/lib/sqlite.ts +0 -147
  184. package/src/lib/sync-cache.ts +0 -65
  185. package/src/lib/theme-transition.ts +0 -113
  186. package/src/lib/theme.tsx +0 -165
  187. package/src/lib/timeline-collections-live.ts +0 -581
  188. package/src/lib/timeline-live.ts +0 -452
  189. package/src/lib/tweet-account-edges.ts +0 -48
  190. package/src/lib/tweet-lookup.ts +0 -46
  191. package/src/lib/tweet-render.ts +0 -430
  192. package/src/lib/tweet-search-live.ts +0 -566
  193. package/src/lib/types.ts +0 -723
  194. package/src/lib/ui.ts +0 -529
  195. package/src/lib/url-expansion-store.ts +0 -110
  196. package/src/lib/url-expansion.ts +0 -343
  197. package/src/lib/url-safety.ts +0 -220
  198. package/src/lib/web-sync.ts +0 -516
  199. package/src/lib/whois.ts +0 -1079
  200. package/src/lib/x-profile.ts +0 -365
  201. package/src/lib/x-web.ts +0 -199
  202. package/src/lib/xurl-rate-limits.ts +0 -267
  203. package/src/lib/xurl.ts +0 -1677
  204. package/src/routeTree.gen.ts +0 -723
  205. package/src/router.tsx +0 -20
  206. package/src/routes/__root.tsx +0 -79
  207. package/src/routes/api/action.tsx +0 -147
  208. package/src/routes/api/avatar.tsx +0 -50
  209. package/src/routes/api/blocks.tsx +0 -35
  210. package/src/routes/api/conversation.tsx +0 -45
  211. package/src/routes/api/data-sources.tsx +0 -24
  212. package/src/routes/api/inbox.tsx +0 -43
  213. package/src/routes/api/link-insights.tsx +0 -85
  214. package/src/routes/api/link-preview.tsx +0 -47
  215. package/src/routes/api/network-map.tsx +0 -55
  216. package/src/routes/api/period-digest.tsx +0 -101
  217. package/src/routes/api/profile-analysis.tsx +0 -97
  218. package/src/routes/api/profile-hydrate.tsx +0 -54
  219. package/src/routes/api/query.tsx +0 -115
  220. package/src/routes/api/search-discussion.tsx +0 -113
  221. package/src/routes/api/status.tsx +0 -28
  222. package/src/routes/api/sync.tsx +0 -105
  223. package/src/routes/api/xurl-rate-limits.tsx +0 -24
  224. package/src/routes/blocks.tsx +0 -377
  225. package/src/routes/bookmarks.tsx +0 -18
  226. package/src/routes/data-sources.tsx +0 -257
  227. package/src/routes/discuss.tsx +0 -419
  228. package/src/routes/dms.tsx +0 -432
  229. package/src/routes/inbox.tsx +0 -226
  230. package/src/routes/index.tsx +0 -32
  231. package/src/routes/likes.tsx +0 -18
  232. package/src/routes/links.tsx +0 -994
  233. package/src/routes/mentions.tsx +0 -32
  234. package/src/routes/network-map.tsx +0 -1035
  235. package/src/routes/profile-analyze.tsx +0 -112
  236. package/src/routes/profiles.$handle.tsx +0 -228
  237. package/src/routes/rate-limits.tsx +0 -309
  238. package/src/routes/today.tsx +0 -511
  239. package/src/styles.css +0 -257
  240. package/tsconfig.json +0 -29
  241. package/vite.config.ts +0 -29
  242. /package/{public → dist/client}/birdclaw-mark.png +0 -0
  243. /package/{public → dist/client}/favicon.ico +0 -0
  244. /package/{public → dist/client}/logo192.png +0 -0
  245. /package/{public → dist/client}/logo512.png +0 -0
  246. /package/{public → dist/client}/manifest.json +0 -0
  247. /package/{public → dist/client}/robots.txt +0 -0
@@ -1,432 +0,0 @@
1
- import { createFileRoute } from "@tanstack/react-router";
2
- import { Search } from "lucide-react";
3
- import { useEffect, useMemo, useState } from "react";
4
- import { DmWorkspace } from "#/components/DmWorkspace";
5
- import { FeedEmpty, FeedError, FeedLoading } from "#/components/FeedState";
6
- import { SyncNowButton } from "#/components/SyncNowButton";
7
- import { useSelectedAccountId } from "#/components/account-selection";
8
- import {
9
- fetchCachedQueryResponse,
10
- fetchQueryEnvelope,
11
- invalidateCachedQueryResponses,
12
- postAction,
13
- readCachedQueryResponse,
14
- } from "#/lib/api-client";
15
- import type {
16
- DmConversationItem,
17
- DmMessageItem,
18
- QueryEnvelope,
19
- ReplyFilter,
20
- } from "#/lib/types";
21
- import { useDebouncedValue } from "#/components/useDebouncedValue";
22
- import {
23
- cx,
24
- pageHeaderClass,
25
- pageHeaderRowClass,
26
- pageSubtitleClass,
27
- pageTitleClass,
28
- searchFieldIconClass,
29
- searchFieldInputClass,
30
- searchFieldShellClass,
31
- segmentActiveClass,
32
- segmentClass,
33
- segmentedClass,
34
- tabButtonActiveClass,
35
- tabButtonClass,
36
- tabButtonIndicatorClass,
37
- tabStripClass,
38
- timestampClass,
39
- } from "#/lib/ui";
40
-
41
- export const Route = createFileRoute("/dms")({
42
- component: DmsRoute,
43
- });
44
-
45
- const TABS: Array<{ value: ReplyFilter; label: string }> = [
46
- { value: "all", label: "All" },
47
- { value: "unreplied", label: "Unreplied" },
48
- { value: "replied", label: "Replied" },
49
- ];
50
-
51
- const SORTS: Array<{ value: "recent" | "followers"; label: string }> = [
52
- { value: "recent", label: "Newest" },
53
- { value: "followers", label: "Followers" },
54
- ];
55
-
56
- type DmInboxFilter = "all" | "accepted" | "requests";
57
-
58
- const INBOX_FILTERS: Array<{ value: DmInboxFilter; label: string }> = [
59
- { value: "all", label: "All" },
60
- { value: "accepted", label: "Accepted" },
61
- { value: "requests", label: "Requests" },
62
- ];
63
-
64
- const filterNumberFieldClass =
65
- "flex h-[46px] shrink-0 items-center gap-2 rounded-md border border-[var(--line)] bg-[var(--bg)] px-3 py-0 text-[14px] text-[var(--ink)] outline-none transition-colors duration-150 focus-within:border-[var(--accent)] focus-within:shadow-[0_0_0_1px_var(--accent)]";
66
-
67
- function DmsRoute() {
68
- const [meta, setMeta] = useState<QueryEnvelope | null>(null);
69
- const [items, setItems] = useState<DmConversationItem[]>([]);
70
- const [messages, setMessages] = useState<DmMessageItem[]>([]);
71
- const [loadedConversationId, setLoadedConversationId] = useState<
72
- string | undefined
73
- >();
74
- const [selectedConversationId, setSelectedConversationId] = useState<
75
- string | undefined
76
- >();
77
- const [inboxFilter, setInboxFilter] = useState<DmInboxFilter>("all");
78
- const [replyFilter, setReplyFilter] = useState<ReplyFilter>("unreplied");
79
- const [minFollowers, setMinFollowers] = useState("");
80
- const [minInfluenceScore, setMinInfluenceScore] = useState("");
81
- const [sort, setSort] = useState<"recent" | "followers">("recent");
82
- const [search, setSearch] = useState("");
83
- const [replyDraft, setReplyDraft] = useState("");
84
- const [refreshTick, setRefreshTick] = useState(0);
85
- const [loading, setLoading] = useState(true);
86
- const [error, setError] = useState<string | null>(null);
87
- const [replyError, setReplyError] = useState<string | null>(null);
88
- const selectedAccountId = useSelectedAccountId(meta?.accounts);
89
- const debouncedSearch = useDebouncedValue(search, 180);
90
-
91
- async function loadStatus(force = false) {
92
- setMeta(await fetchQueryEnvelope(undefined, { force }));
93
- }
94
-
95
- useEffect(() => {
96
- void loadStatus();
97
- }, []);
98
-
99
- useEffect(() => {
100
- const controller = new AbortController();
101
- let active = true;
102
- const url = new URL("/api/query", window.location.origin);
103
- url.searchParams.set("resource", "dms");
104
- url.searchParams.set("inbox", inboxFilter);
105
- url.searchParams.set("replyFilter", replyFilter);
106
- url.searchParams.set("refresh", String(refreshTick));
107
- url.searchParams.set("sort", sort);
108
- if (minFollowers.trim()) {
109
- url.searchParams.set("minFollowers", minFollowers.trim());
110
- }
111
- if (minInfluenceScore.trim()) {
112
- url.searchParams.set("minInfluenceScore", minInfluenceScore.trim());
113
- }
114
- if (selectedAccountId && inboxFilter !== "requests") {
115
- url.searchParams.set("account", selectedAccountId);
116
- }
117
- if (selectedConversationId) {
118
- url.searchParams.set("conversationId", selectedConversationId);
119
- }
120
- if (debouncedSearch.trim()) {
121
- url.searchParams.set("search", debouncedSearch.trim());
122
- }
123
-
124
- const applyResponse = (
125
- data: Awaited<ReturnType<typeof fetchCachedQueryResponse>>,
126
- ) => {
127
- const conversations = data.items as DmConversationItem[];
128
- const nextSelected =
129
- data.selectedConversation?.conversation.id ?? conversations[0]?.id;
130
- setLoadedConversationId(data.selectedConversation?.conversation.id);
131
- setItems(conversations);
132
- setSelectedConversationId((current) => {
133
- if (!current) return nextSelected;
134
- return conversations.some((conversation) => conversation.id === current)
135
- ? current
136
- : nextSelected;
137
- });
138
- setMessages(data.selectedConversation?.messages ?? []);
139
- };
140
- setError(null);
141
- const cached = readCachedQueryResponse(url);
142
- if (cached) {
143
- applyResponse(cached);
144
- setLoading(false);
145
- return () => {
146
- active = false;
147
- controller.abort();
148
- };
149
- }
150
- setLoading(true);
151
- fetchCachedQueryResponse(url, { signal: controller.signal })
152
- .then((data) => {
153
- if (!active) return;
154
- applyResponse(data);
155
- })
156
- .catch((fetchError: unknown) => {
157
- if (
158
- fetchError instanceof DOMException &&
159
- fetchError.name === "AbortError"
160
- ) {
161
- return;
162
- }
163
- if (!active) return;
164
- setError(
165
- fetchError instanceof Error
166
- ? fetchError.message
167
- : "Messages unavailable",
168
- );
169
- setItems([]);
170
- setMessages([]);
171
- setLoadedConversationId(undefined);
172
- })
173
- .finally(() => {
174
- if (active) {
175
- setLoading(false);
176
- }
177
- });
178
-
179
- return () => {
180
- active = false;
181
- controller.abort();
182
- };
183
- }, [
184
- minFollowers,
185
- minInfluenceScore,
186
- inboxFilter,
187
- refreshTick,
188
- replyFilter,
189
- debouncedSearch,
190
- selectedConversationId,
191
- selectedAccountId,
192
- sort,
193
- ]);
194
-
195
- const selectedConversation =
196
- items.find((item) => item.id === selectedConversationId) ?? null;
197
- const switchingConversation =
198
- loading &&
199
- Boolean(
200
- selectedConversationId &&
201
- loadedConversationId &&
202
- selectedConversationId !== loadedConversationId,
203
- );
204
-
205
- const subtitle = useMemo(() => {
206
- if (!meta) return "Loading direct messages...";
207
- return `${String(meta.stats.dms)} conversations cached locally`;
208
- }, [meta]);
209
-
210
- async function replyToConversation(conversationId: string) {
211
- const text = replyDraft.trim();
212
- if (!text || !selectedConversation) return;
213
-
214
- const now = new Date().toISOString();
215
- const accountRecord = meta?.accounts.find(
216
- (account) => account.id === selectedConversation.accountId,
217
- );
218
- const senderHandle = (
219
- accountRecord?.handle ?? selectedConversation.accountHandle
220
- ).replace(/^@/, "");
221
- const optimisticMessage: DmMessageItem = {
222
- id: `optimistic-${now}`,
223
- conversationId,
224
- text,
225
- createdAt: now,
226
- direction: "outbound",
227
- isReplied: true,
228
- mediaCount: 0,
229
- sender: {
230
- id: `local-${selectedConversation.accountId}`,
231
- handle: senderHandle,
232
- displayName: accountRecord?.name ?? senderHandle,
233
- bio: "",
234
- followersCount: 0,
235
- avatarHue: 18,
236
- createdAt: now,
237
- },
238
- };
239
- const previousMessages = messages;
240
- const previousItems = items;
241
-
242
- setReplyError(null);
243
- setReplyDraft("");
244
- setMessages((current) => [...current, optimisticMessage]);
245
- setItems((current) =>
246
- current.map((item) =>
247
- item.id === conversationId
248
- ? {
249
- ...item,
250
- lastMessageAt: now,
251
- lastMessagePreview: text,
252
- needsReply: false,
253
- unreadCount: 0,
254
- }
255
- : item,
256
- ),
257
- );
258
-
259
- try {
260
- await postAction({
261
- kind: "replyDm",
262
- conversationId,
263
- text,
264
- });
265
-
266
- invalidateCachedQueryResponses();
267
- setSelectedConversationId(conversationId);
268
- setRefreshTick((value) => value + 1);
269
- } catch (error) {
270
- setReplyDraft(text);
271
- setMessages(previousMessages);
272
- setItems(previousItems);
273
- setReplyError(error instanceof Error ? error.message : "Reply failed");
274
- }
275
- }
276
-
277
- function refreshLocalView() {
278
- invalidateCachedQueryResponses();
279
- setRefreshTick((value) => value + 1);
280
- void loadStatus(true);
281
- }
282
-
283
- return (
284
- <>
285
- <header className={pageHeaderClass}>
286
- <div className={pageHeaderRowClass}>
287
- <div className="flex min-w-0 flex-col">
288
- <h1 className={pageTitleClass}>Messages</h1>
289
- <p className={pageSubtitleClass}>{subtitle}</p>
290
- </div>
291
- <SyncNowButton
292
- accounts={meta?.accounts}
293
- kind="dms"
294
- label="Sync DMs"
295
- onSynced={refreshLocalView}
296
- syncOptions={{
297
- inbox: inboxFilter,
298
- limit: inboxFilter === "requests" ? 200 : 50,
299
- maxPages: inboxFilter === "requests" ? 3 : 1,
300
- }}
301
- />
302
- </div>
303
- <div className="flex flex-wrap items-center gap-2 px-4 pb-3">
304
- <div className={segmentedClass} aria-label="DM inbox">
305
- {INBOX_FILTERS.map((filter) => (
306
- <button
307
- key={filter.value}
308
- aria-pressed={inboxFilter === filter.value}
309
- className={cx(
310
- segmentClass,
311
- inboxFilter === filter.value && segmentActiveClass,
312
- )}
313
- onClick={() => setInboxFilter(filter.value)}
314
- type="button"
315
- >
316
- {filter.label}
317
- </button>
318
- ))}
319
- </div>
320
- <label className={cx(searchFieldShellClass, "flex-1 min-w-[200px]")}>
321
- <Search className={searchFieldIconClass} strokeWidth={2} />
322
- <input
323
- className={searchFieldInputClass}
324
- onChange={(event) => setSearch(event.target.value)}
325
- placeholder="Search DMs"
326
- value={search}
327
- />
328
- </label>
329
- <label className={cx(filterNumberFieldClass, "w-[156px]")}>
330
- <span className="shrink-0 text-[12px] font-semibold text-[var(--ink-soft)]">
331
- Followers
332
- </span>
333
- <input
334
- className="min-w-0 flex-1 border-0 bg-transparent text-right text-[14px] text-[var(--ink)] outline-none placeholder:text-[var(--ink-soft)]"
335
- inputMode="numeric"
336
- onChange={(event) => setMinFollowers(event.target.value)}
337
- placeholder="Any"
338
- value={minFollowers}
339
- />
340
- </label>
341
- <label className={cx(filterNumberFieldClass, "w-[132px]")}>
342
- <span className="shrink-0 text-[12px] font-semibold text-[var(--ink-soft)]">
343
- Score
344
- </span>
345
- <input
346
- className="min-w-0 flex-1 border-0 bg-transparent text-right text-[14px] text-[var(--ink)] outline-none placeholder:text-[var(--ink-soft)]"
347
- inputMode="numeric"
348
- onChange={(event) => setMinInfluenceScore(event.target.value)}
349
- placeholder="Any"
350
- value={minInfluenceScore}
351
- />
352
- </label>
353
- <div className={segmentedClass}>
354
- {SORTS.map((option) => (
355
- <button
356
- key={option.value}
357
- className={cx(
358
- segmentClass,
359
- option.value === sort && segmentActiveClass,
360
- )}
361
- onClick={() => setSort(option.value)}
362
- type="button"
363
- >
364
- {option.label}
365
- </button>
366
- ))}
367
- </div>
368
- </div>
369
- <div className={tabStripClass} aria-label="DM reply filter">
370
- {TABS.map((tab) => {
371
- const active = replyFilter === tab.value;
372
- return (
373
- <button
374
- key={tab.value}
375
- type="button"
376
- aria-pressed={active}
377
- className={cx(tabButtonClass, active && tabButtonActiveClass)}
378
- onClick={() => setReplyFilter(tab.value)}
379
- >
380
- <span className="relative inline-flex flex-col items-center justify-center py-1">
381
- {tab.value}
382
- {active ? <span className={tabButtonIndicatorClass} /> : null}
383
- </span>
384
- </button>
385
- );
386
- })}
387
- </div>
388
- </header>
389
- {replyError ? (
390
- <p className={cx(timestampClass, "px-4 py-2 text-red-500")}>
391
- {replyError}
392
- </p>
393
- ) : null}
394
-
395
- {loading && (items.length === 0 || switchingConversation) ? (
396
- <FeedLoading
397
- detail="Reading local conversations and reply state"
398
- label="Loading messages"
399
- />
400
- ) : error ? (
401
- <FeedError
402
- action={
403
- <button
404
- className="rounded-full bg-[var(--accent)] px-4 py-1.5 text-[14px] font-bold text-white"
405
- onClick={() => setRefreshTick((value) => value + 1)}
406
- type="button"
407
- >
408
- Retry
409
- </button>
410
- }
411
- message={error}
412
- title="Could not load messages"
413
- />
414
- ) : items.length === 0 ? (
415
- <FeedEmpty
416
- detail="Sync DMs or broaden the filters to find a conversation."
417
- label="No conversations in this view"
418
- />
419
- ) : (
420
- <DmWorkspace
421
- conversations={items}
422
- onReplyDraftChange={setReplyDraft}
423
- onReplySend={replyToConversation}
424
- onSelectConversation={setSelectedConversationId}
425
- replyDraft={replyDraft}
426
- selectedConversation={selectedConversation}
427
- selectedMessages={messages}
428
- />
429
- )}
430
- </>
431
- );
432
- }
@@ -1,226 +0,0 @@
1
- import { createFileRoute } from "@tanstack/react-router";
2
- import { Sparkles } from "lucide-react";
3
- import { useEffect, useMemo, useState } from "react";
4
- import { useSelectedAccountId } from "#/components/account-selection";
5
- import { InboxCard } from "#/components/InboxCard";
6
- import { postAction } from "#/lib/api-client";
7
- import type {
8
- InboxItem,
9
- InboxKind,
10
- InboxResponse,
11
- QueryEnvelope,
12
- } from "#/lib/types";
13
- import {
14
- cx,
15
- emptyStateClass,
16
- feedClass,
17
- pageHeaderClass,
18
- pageHeaderRowClass,
19
- pageSubtitleClass,
20
- pageTitleClass,
21
- primaryButtonClass,
22
- secondaryButtonClass,
23
- tabButtonActiveClass,
24
- tabButtonClass,
25
- tabButtonIndicatorClass,
26
- tabStripClass,
27
- textFieldClass,
28
- textFieldShortClass,
29
- timestampClass,
30
- } from "#/lib/ui";
31
-
32
- export const Route = createFileRoute("/inbox")({
33
- component: InboxRoute,
34
- });
35
-
36
- const TABS: Array<{ value: InboxKind; label: string }> = [
37
- { value: "mixed", label: "Mixed" },
38
- { value: "mentions", label: "Mentions" },
39
- { value: "dms", label: "DMs" },
40
- ];
41
-
42
- function InboxRoute() {
43
- const [meta, setMeta] = useState<QueryEnvelope | null>(null);
44
- const [items, setItems] = useState<InboxItem[]>([]);
45
- const [kind, setKind] = useState<InboxKind>("mixed");
46
- const [minScore, setMinScore] = useState("40");
47
- const [hideLowSignal, setHideLowSignal] = useState(true);
48
- const [refreshTick, setRefreshTick] = useState(0);
49
- const [isScoring, setIsScoring] = useState(false);
50
- const [activeReplyId, setActiveReplyId] = useState<string | null>(null);
51
- const [replyDraft, setReplyDraft] = useState("");
52
- const [isSendingReply, setIsSendingReply] = useState(false);
53
- const [replyError, setReplyError] = useState<string | null>(null);
54
- const [stats, setStats] = useState<InboxResponse["stats"] | null>(null);
55
- const selectedAccountId = useSelectedAccountId(meta?.accounts);
56
-
57
- useEffect(() => {
58
- fetch("/api/status")
59
- .then((response) => response.json())
60
- .then((data: QueryEnvelope) => setMeta(data));
61
- }, []);
62
-
63
- useEffect(() => {
64
- const url = new URL("/api/inbox", window.location.origin);
65
- url.searchParams.set("kind", kind);
66
- url.searchParams.set("minScore", minScore);
67
- url.searchParams.set("refresh", String(refreshTick));
68
- if (selectedAccountId) {
69
- url.searchParams.set("account", selectedAccountId);
70
- }
71
- if (hideLowSignal) {
72
- url.searchParams.set("hideLowSignal", "1");
73
- }
74
-
75
- fetch(url)
76
- .then((response) => response.json())
77
- .then((data: InboxResponse) => {
78
- setItems(data.items);
79
- setStats(data.stats);
80
- });
81
- }, [hideLowSignal, kind, minScore, refreshTick, selectedAccountId]);
82
-
83
- const subtitle = useMemo(() => {
84
- if (!meta || !stats) return "Ranking unreplied mentions and DMs...";
85
- return `${String(stats.total)} in queue · ${String(stats.openai)} OpenAI scored · ${meta.transport.statusText}`;
86
- }, [meta, stats]);
87
-
88
- async function scoreNow() {
89
- setIsScoring(true);
90
- try {
91
- await fetch("/api/action", {
92
- method: "POST",
93
- headers: { "content-type": "application/json" },
94
- body: JSON.stringify({
95
- kind: "scoreInbox",
96
- scoreKind: kind,
97
- account: selectedAccountId,
98
- limit: 8,
99
- }),
100
- });
101
- setRefreshTick((value) => value + 1);
102
- } finally {
103
- setIsScoring(false);
104
- }
105
- }
106
-
107
- async function sendReply(item: InboxItem) {
108
- if (!replyDraft.trim()) return;
109
- setIsSendingReply(true);
110
- setReplyError(null);
111
- try {
112
- await postAction(
113
- item.entityKind === "dm"
114
- ? {
115
- kind: "replyDm",
116
- conversationId: item.entityId,
117
- text: replyDraft,
118
- }
119
- : {
120
- kind: "replyTweet",
121
- accountId: item.accountId,
122
- tweetId: item.entityId,
123
- text: replyDraft,
124
- },
125
- );
126
- setReplyDraft("");
127
- setActiveReplyId(null);
128
- setRefreshTick((value) => value + 1);
129
- } catch (error) {
130
- setReplyError(error instanceof Error ? error.message : "Reply failed");
131
- } finally {
132
- setIsSendingReply(false);
133
- }
134
- }
135
-
136
- return (
137
- <>
138
- <header className={pageHeaderClass}>
139
- <div className={pageHeaderRowClass}>
140
- <div className="flex min-w-0 flex-col">
141
- <h1 className={pageTitleClass}>Inbox</h1>
142
- <p className={pageSubtitleClass}>{subtitle}</p>
143
- </div>
144
- <button
145
- className={primaryButtonClass}
146
- disabled={isScoring}
147
- onClick={() => void scoreNow()}
148
- type="button"
149
- >
150
- <Sparkles className="size-4" strokeWidth={2.2} />
151
- {isScoring ? "Scoring..." : "Score with OpenAI"}
152
- </button>
153
- </div>
154
- <div className="flex flex-wrap items-center gap-2 px-4 pb-3">
155
- <input
156
- className={cx(textFieldClass, textFieldShortClass)}
157
- inputMode="numeric"
158
- onChange={(event) => setMinScore(event.target.value)}
159
- placeholder="Min AI score"
160
- value={minScore}
161
- />
162
- <button
163
- className={secondaryButtonClass}
164
- onClick={() => setHideLowSignal((value) => !value)}
165
- type="button"
166
- aria-pressed={hideLowSignal}
167
- >
168
- {hideLowSignal ? "Hide low-signal" : "Show all"}
169
- </button>
170
- </div>
171
- <div className={tabStripClass}>
172
- {TABS.map((tab) => {
173
- const active = kind === tab.value;
174
- return (
175
- <button
176
- key={tab.value}
177
- type="button"
178
- aria-pressed={active}
179
- className={cx(tabButtonClass, active && tabButtonActiveClass)}
180
- onClick={() => setKind(tab.value)}
181
- >
182
- <span className="relative inline-flex flex-col items-center justify-center py-1">
183
- {tab.value}
184
- {active ? <span className={tabButtonIndicatorClass} /> : null}
185
- </span>
186
- </button>
187
- );
188
- })}
189
- </div>
190
- </header>
191
- {replyError ? (
192
- <p className={cx(timestampClass, "px-4 py-2 text-red-500")}>
193
- {replyError}
194
- </p>
195
- ) : null}
196
- <section className={feedClass}>
197
- {items.length === 0 ? (
198
- <div className={emptyStateClass}>Inbox clear.</div>
199
- ) : null}
200
- {items.map((item) => (
201
- <InboxCard
202
- key={item.id}
203
- isReplying={activeReplyId === item.id}
204
- item={item}
205
- onReplyChange={setReplyDraft}
206
- onReplySend={() => void sendReply(item)}
207
- onReplyToggle={() => {
208
- setReplyError(null);
209
- if (activeReplyId === item.id) {
210
- setActiveReplyId(null);
211
- setReplyDraft("");
212
- return;
213
- }
214
- setActiveReplyId(item.id);
215
- setReplyDraft("");
216
- }}
217
- replyDraft={activeReplyId === item.id ? replyDraft : ""}
218
- />
219
- ))}
220
- </section>
221
- {isSendingReply ? (
222
- <p className={cx(timestampClass, "px-4 py-2")}>Sending reply...</p>
223
- ) : null}
224
- </>
225
- );
226
- }
@@ -1,32 +0,0 @@
1
- import { createFileRoute } from "@tanstack/react-router";
2
- import { TimelineRouteFrame } from "#/components/TimelineRouteFrame";
3
- import type { QueryEnvelope } from "#/lib/types";
4
-
5
- export const Route = createFileRoute("/")({
6
- component: HomeRoute,
7
- });
8
-
9
- function homeSubtitle(meta: QueryEnvelope | null) {
10
- if (!meta) return "Loading local context...";
11
- return `${String(meta.stats.home)} items · ${String(meta.stats.needsReply)} waiting · ${meta.transport.statusText}`;
12
- }
13
-
14
- function HomeRoute() {
15
- return (
16
- <TimelineRouteFrame
17
- emptyDetail="Try a different filter or sync the timeline again."
18
- emptyLabel="No posts in this view"
19
- errorFallback="Timeline unavailable"
20
- errorTitle="Could not load posts"
21
- initialReplyFilter="all"
22
- loadingDetail="Reading the local timeline store"
23
- loadingLabel="Loading posts"
24
- resource="home"
25
- searchPlaceholder="Search local timeline"
26
- subtitle={homeSubtitle}
27
- syncKind="timeline"
28
- syncLabel="Sync timeline"
29
- title="Home"
30
- />
31
- );
32
- }
@@ -1,18 +0,0 @@
1
- import { createFileRoute } from "@tanstack/react-router";
2
- import { SavedTimelineView } from "#/components/SavedTimelineView";
3
-
4
- export const Route = createFileRoute("/likes")({
5
- component: LikesRoute,
6
- });
7
-
8
- function LikesRoute() {
9
- return (
10
- <SavedTimelineView
11
- eyebrow="liked posts"
12
- filter="liked"
13
- loadingLabel="Loading liked posts..."
14
- searchPlaceholder="Search likes"
15
- title="Things worth keeping in the orbit."
16
- />
17
- );
18
- }