birdclaw 0.8.1 → 0.8.3
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 +25 -0
- package/package.json +2 -4
- package/scripts/browser-perf.mjs +27 -0
- package/src/cli/command-context.ts +17 -0
- package/src/cli/register-analysis.ts +500 -0
- package/src/cli/register-compose.ts +40 -0
- package/src/cli/register-graph.ts +132 -0
- package/src/cli/register-inbox.ts +41 -0
- package/src/cli/register-storage.ts +106 -0
- package/src/cli.ts +30 -750
- package/src/components/AccountSwitcher.tsx +7 -15
- package/src/components/AvatarChip.tsx +1 -1
- package/src/components/AvatarPreload.ts +149 -0
- package/src/components/ConversationThread.tsx +4 -0
- package/src/components/EmbeddedTweetCard.tsx +4 -0
- package/src/components/FloatingPreview.tsx +382 -0
- package/src/components/MarkdownCitations.tsx +680 -0
- package/src/components/MarkdownViewer.tsx +7 -715
- package/src/components/ProfileAnalysisClient.ts +191 -0
- package/src/components/ProfileAnalysisStream.tsx +16 -185
- package/src/components/ProfilePreview.tsx +41 -81
- package/src/components/TimelineCard.tsx +18 -2
- package/src/components/TweetArticleCard.tsx +66 -0
- package/src/components/TweetRichText.tsx +33 -2
- package/src/components/links-controller.ts +162 -0
- package/src/components/links-model.ts +198 -0
- package/src/components/network-map-controller.ts +84 -0
- package/src/components/network-map-model.ts +255 -0
- package/src/components/useDebouncedValue.ts +12 -0
- package/src/components/useTimelineRouteData.ts +142 -170
- package/src/lib/analysis-runtime.ts +238 -0
- package/src/lib/api-client.ts +16 -100
- package/src/lib/api-contracts.ts +328 -0
- package/src/lib/archive-finder.ts +38 -0
- package/src/lib/archive-import-plan.ts +102 -0
- package/src/lib/archive-import.ts +170 -239
- package/src/lib/authored-live.ts +77 -182
- package/src/lib/backup.ts +335 -424
- package/src/lib/bird.ts +32 -1
- package/src/lib/blocks-write.ts +30 -26
- package/src/lib/blocks.ts +18 -20
- package/src/lib/database-metrics.ts +88 -0
- package/src/lib/database-migrations.ts +34 -0
- package/src/lib/database-schema.ts +312 -0
- package/src/lib/database-writer.ts +69 -0
- package/src/lib/db.ts +141 -334
- package/src/lib/dm-read-model.ts +533 -0
- package/src/lib/dms-live.ts +34 -97
- package/src/lib/follow-graph.ts +17 -27
- package/src/lib/import-repository.ts +138 -0
- package/src/lib/inbox.ts +2 -1
- package/src/lib/live-sync-engine.ts +209 -0
- package/src/lib/live-transport-gateway.ts +128 -0
- package/src/lib/mention-threads-live.ts +90 -176
- package/src/lib/mentions-export.ts +1 -1
- package/src/lib/mentions-live.ts +57 -225
- package/src/lib/moderation-target.ts +15 -4
- package/src/lib/moderation-write.ts +1 -1
- package/src/lib/mutes-write.ts +30 -26
- package/src/lib/openai-response-runtime.ts +251 -0
- package/src/lib/paginated-sync.ts +93 -0
- package/src/lib/period-digest.ts +116 -304
- package/src/lib/profile-analysis.ts +37 -111
- package/src/lib/queries.ts +6 -2380
- package/src/lib/query-actions.ts +437 -0
- package/src/lib/query-client.tsx +47 -0
- package/src/lib/query-read-model-shared.ts +52 -0
- package/src/lib/query-read-models.ts +5 -0
- package/src/lib/query-resource.ts +41 -0
- package/src/lib/query-status.ts +164 -0
- package/src/lib/research.ts +1 -1
- package/src/lib/runtime-services.ts +20 -0
- package/src/lib/search-discussion.ts +75 -279
- package/src/lib/server-runtime-services.ts +30 -0
- package/src/lib/sqlite.ts +53 -14
- package/src/lib/streaming-ingestion.ts +240 -0
- package/src/lib/sync-cache.ts +6 -1
- package/src/lib/sync-plan.ts +175 -0
- package/src/lib/timeline-collections-live.ts +83 -256
- package/src/lib/timeline-live.ts +86 -235
- package/src/lib/timeline-read-model.ts +1191 -0
- package/src/lib/tweet-render.ts +78 -0
- package/src/lib/tweet-repository.ts +156 -0
- package/src/lib/tweet-search-live.ts +63 -166
- package/src/lib/types.ts +8 -0
- package/src/lib/ui.ts +1 -1
- package/src/lib/web-sync.ts +67 -50
- package/src/lib/whois.ts +2 -1
- package/src/router.tsx +1 -1
- package/src/routes/__root.tsx +11 -21
- package/src/routes/api/action.tsx +1 -1
- package/src/routes/api/conversation.tsx +1 -1
- package/src/routes/api/query.tsx +32 -26
- package/src/routes/api/status.tsx +6 -2
- package/src/routes/api/sync.tsx +5 -2
- package/src/routes/blocks.tsx +97 -131
- package/src/routes/data-sources.tsx +17 -25
- package/src/routes/dms.tsx +168 -167
- package/src/routes/inbox.tsx +63 -57
- package/src/routes/links.tsx +31 -329
- package/src/routes/network-map.tsx +41 -344
- package/src/routes/rate-limits.tsx +17 -21
- package/vite.config.ts +0 -2
package/src/routes/dms.tsx
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
import {
|
|
3
|
+
keepPreviousData,
|
|
4
|
+
useMutation,
|
|
5
|
+
useQuery,
|
|
6
|
+
useQueryClient,
|
|
7
|
+
} from "@tanstack/react-query";
|
|
2
8
|
import { Search } from "lucide-react";
|
|
3
9
|
import { useEffect, useMemo, useState } from "react";
|
|
4
10
|
import { DmWorkspace } from "#/components/DmWorkspace";
|
|
@@ -10,12 +16,14 @@ import {
|
|
|
10
16
|
fetchQueryResponse,
|
|
11
17
|
postAction,
|
|
12
18
|
} from "#/lib/api-client";
|
|
19
|
+
import { queryKeys } from "#/lib/query-client";
|
|
13
20
|
import type {
|
|
14
21
|
DmConversationItem,
|
|
15
22
|
DmMessageItem,
|
|
16
|
-
|
|
23
|
+
QueryResponse,
|
|
17
24
|
ReplyFilter,
|
|
18
25
|
} from "#/lib/types";
|
|
26
|
+
import { useDebouncedValue } from "#/components/useDebouncedValue";
|
|
19
27
|
import {
|
|
20
28
|
cx,
|
|
21
29
|
pageHeaderClass,
|
|
@@ -62,12 +70,7 @@ const filterNumberFieldClass =
|
|
|
62
70
|
"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)]";
|
|
63
71
|
|
|
64
72
|
function DmsRoute() {
|
|
65
|
-
const
|
|
66
|
-
const [items, setItems] = useState<DmConversationItem[]>([]);
|
|
67
|
-
const [messages, setMessages] = useState<DmMessageItem[]>([]);
|
|
68
|
-
const [loadedConversationId, setLoadedConversationId] = useState<
|
|
69
|
-
string | undefined
|
|
70
|
-
>();
|
|
73
|
+
const queryClient = useQueryClient();
|
|
71
74
|
const [selectedConversationId, setSelectedConversationId] = useState<
|
|
72
75
|
string | undefined
|
|
73
76
|
>();
|
|
@@ -78,189 +81,187 @@ function DmsRoute() {
|
|
|
78
81
|
const [sort, setSort] = useState<"recent" | "followers">("recent");
|
|
79
82
|
const [search, setSearch] = useState("");
|
|
80
83
|
const [replyDraft, setReplyDraft] = useState("");
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
84
|
+
const statusQuery = useQuery({
|
|
85
|
+
queryKey: queryKeys.status,
|
|
86
|
+
queryFn: ({ signal }) => fetchQueryEnvelope({ signal }),
|
|
87
|
+
});
|
|
88
|
+
const meta = statusQuery.data ?? null;
|
|
85
89
|
const selectedAccountId = useSelectedAccountId(meta?.accounts);
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
+
const debouncedSearch = useDebouncedValue(search, 180);
|
|
91
|
+
const dmsQueryKey = [
|
|
92
|
+
...queryKeys.dms,
|
|
93
|
+
{
|
|
94
|
+
inboxFilter,
|
|
95
|
+
replyFilter,
|
|
96
|
+
minFollowers,
|
|
97
|
+
minInfluenceScore,
|
|
98
|
+
sort,
|
|
99
|
+
search: debouncedSearch,
|
|
100
|
+
selectedAccountId: selectedAccountId ?? null,
|
|
101
|
+
selectedConversationId: selectedConversationId ?? null,
|
|
102
|
+
},
|
|
103
|
+
] as const;
|
|
104
|
+
const dmsQuery = useQuery({
|
|
105
|
+
queryKey: dmsQueryKey,
|
|
106
|
+
queryFn: ({ signal }) => {
|
|
107
|
+
const url = new URL("/api/query", window.location.origin);
|
|
108
|
+
url.searchParams.set("resource", "dms");
|
|
109
|
+
url.searchParams.set("inbox", inboxFilter);
|
|
110
|
+
url.searchParams.set("replyFilter", replyFilter);
|
|
111
|
+
url.searchParams.set("sort", sort);
|
|
112
|
+
if (minFollowers.trim()) {
|
|
113
|
+
url.searchParams.set("minFollowers", minFollowers.trim());
|
|
114
|
+
}
|
|
115
|
+
if (minInfluenceScore.trim()) {
|
|
116
|
+
url.searchParams.set("minInfluenceScore", minInfluenceScore.trim());
|
|
117
|
+
}
|
|
118
|
+
if (selectedAccountId && inboxFilter !== "requests") {
|
|
119
|
+
url.searchParams.set("account", selectedAccountId);
|
|
120
|
+
}
|
|
121
|
+
if (selectedConversationId) {
|
|
122
|
+
url.searchParams.set("conversationId", selectedConversationId);
|
|
123
|
+
}
|
|
124
|
+
if (debouncedSearch.trim()) {
|
|
125
|
+
url.searchParams.set("search", debouncedSearch.trim());
|
|
126
|
+
}
|
|
127
|
+
return fetchQueryResponse(url, { signal });
|
|
128
|
+
},
|
|
129
|
+
placeholderData: keepPreviousData,
|
|
130
|
+
staleTime: 5 * 60_000,
|
|
131
|
+
});
|
|
132
|
+
const items = (dmsQuery.data?.items ?? []) as DmConversationItem[];
|
|
133
|
+
const messages = dmsQuery.data?.selectedConversation?.messages ?? [];
|
|
134
|
+
const loadedConversationId =
|
|
135
|
+
dmsQuery.data?.selectedConversation?.conversation.id;
|
|
90
136
|
|
|
91
137
|
useEffect(() => {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
url.searchParams.set("replyFilter", replyFilter);
|
|
102
|
-
url.searchParams.set("refresh", String(refreshTick));
|
|
103
|
-
url.searchParams.set("sort", sort);
|
|
104
|
-
if (minFollowers.trim()) {
|
|
105
|
-
url.searchParams.set("minFollowers", minFollowers.trim());
|
|
106
|
-
}
|
|
107
|
-
if (minInfluenceScore.trim()) {
|
|
108
|
-
url.searchParams.set("minInfluenceScore", minInfluenceScore.trim());
|
|
109
|
-
}
|
|
110
|
-
if (selectedAccountId && inboxFilter !== "requests") {
|
|
111
|
-
url.searchParams.set("account", selectedAccountId);
|
|
112
|
-
}
|
|
113
|
-
if (selectedConversationId) {
|
|
114
|
-
url.searchParams.set("conversationId", selectedConversationId);
|
|
115
|
-
}
|
|
116
|
-
if (search.trim()) {
|
|
117
|
-
url.searchParams.set("search", search.trim());
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
setError(null);
|
|
121
|
-
setLoading(true);
|
|
122
|
-
fetchQueryResponse(url, { signal: controller.signal })
|
|
123
|
-
.then((data) => {
|
|
124
|
-
if (!active) return;
|
|
125
|
-
const conversations = data.items as DmConversationItem[];
|
|
126
|
-
const nextSelected =
|
|
127
|
-
data.selectedConversation?.conversation.id ?? conversations[0]?.id;
|
|
128
|
-
setLoadedConversationId(data.selectedConversation?.conversation.id);
|
|
129
|
-
setItems(conversations);
|
|
130
|
-
setSelectedConversationId((current) => {
|
|
131
|
-
if (!current) return nextSelected;
|
|
132
|
-
return conversations.some(
|
|
133
|
-
(conversation) => conversation.id === current,
|
|
134
|
-
)
|
|
135
|
-
? current
|
|
136
|
-
: nextSelected;
|
|
137
|
-
});
|
|
138
|
-
setMessages(data.selectedConversation?.messages ?? []);
|
|
139
|
-
})
|
|
140
|
-
.catch((fetchError: unknown) => {
|
|
141
|
-
if (
|
|
142
|
-
fetchError instanceof DOMException &&
|
|
143
|
-
fetchError.name === "AbortError"
|
|
144
|
-
) {
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
if (!active) return;
|
|
148
|
-
setError(
|
|
149
|
-
fetchError instanceof Error
|
|
150
|
-
? fetchError.message
|
|
151
|
-
: "Messages unavailable",
|
|
152
|
-
);
|
|
153
|
-
setItems([]);
|
|
154
|
-
setMessages([]);
|
|
155
|
-
setLoadedConversationId(undefined);
|
|
156
|
-
})
|
|
157
|
-
.finally(() => {
|
|
158
|
-
if (active) {
|
|
159
|
-
setLoading(false);
|
|
160
|
-
}
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
return () => {
|
|
164
|
-
active = false;
|
|
165
|
-
controller.abort();
|
|
166
|
-
};
|
|
167
|
-
}, [
|
|
168
|
-
minFollowers,
|
|
169
|
-
minInfluenceScore,
|
|
170
|
-
inboxFilter,
|
|
171
|
-
refreshTick,
|
|
172
|
-
replyFilter,
|
|
173
|
-
search,
|
|
174
|
-
selectedConversationId,
|
|
175
|
-
selectedAccountId,
|
|
176
|
-
sort,
|
|
177
|
-
]);
|
|
138
|
+
if (!dmsQuery.data) return;
|
|
139
|
+
const nextSelected = loadedConversationId ?? items[0]?.id;
|
|
140
|
+
setSelectedConversationId((current) => {
|
|
141
|
+
if (!current) return nextSelected;
|
|
142
|
+
return items.some((conversation) => conversation.id === current)
|
|
143
|
+
? current
|
|
144
|
+
: nextSelected;
|
|
145
|
+
});
|
|
146
|
+
}, [dmsQuery.data, items, loadedConversationId]);
|
|
178
147
|
|
|
179
148
|
const selectedConversation =
|
|
180
149
|
items.find((item) => item.id === selectedConversationId) ?? null;
|
|
181
|
-
const switchingConversation =
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
);
|
|
150
|
+
const switchingConversation = Boolean(
|
|
151
|
+
!dmsQuery.isError &&
|
|
152
|
+
selectedConversationId &&
|
|
153
|
+
loadedConversationId &&
|
|
154
|
+
selectedConversationId !== loadedConversationId,
|
|
155
|
+
);
|
|
188
156
|
|
|
189
157
|
const subtitle = useMemo(() => {
|
|
190
158
|
if (!meta) return "Loading direct messages...";
|
|
191
159
|
return `${String(meta.stats.dms)} conversations cached locally`;
|
|
192
160
|
}, [meta]);
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const text = replyDraft.trim();
|
|
196
|
-
if (!text || !selectedConversation) return;
|
|
197
|
-
|
|
198
|
-
const now = new Date().toISOString();
|
|
199
|
-
const accountRecord = meta?.accounts.find(
|
|
200
|
-
(account) => account.id === selectedConversation.accountId,
|
|
201
|
-
);
|
|
202
|
-
const senderHandle = (
|
|
203
|
-
accountRecord?.handle ?? selectedConversation.accountHandle
|
|
204
|
-
).replace(/^@/, "");
|
|
205
|
-
const optimisticMessage: DmMessageItem = {
|
|
206
|
-
id: `optimistic-${now}`,
|
|
161
|
+
const replyMutation = useMutation({
|
|
162
|
+
mutationFn: ({
|
|
207
163
|
conversationId,
|
|
208
164
|
text,
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
165
|
+
}: {
|
|
166
|
+
conversationId: string;
|
|
167
|
+
text: string;
|
|
168
|
+
}) => postAction({ kind: "replyDm", conversationId, text }),
|
|
169
|
+
onMutate: async ({ conversationId, text }) => {
|
|
170
|
+
await queryClient.cancelQueries({ queryKey: dmsQueryKey });
|
|
171
|
+
const previous = queryClient.getQueryData<QueryResponse>(dmsQueryKey);
|
|
172
|
+
if (!previous || !selectedConversation) return { previous };
|
|
173
|
+
const now = new Date().toISOString();
|
|
174
|
+
const accountRecord = meta?.accounts.find(
|
|
175
|
+
(account) => account.id === selectedConversation.accountId,
|
|
176
|
+
);
|
|
177
|
+
const senderHandle = (
|
|
178
|
+
accountRecord?.handle ?? selectedConversation.accountHandle
|
|
179
|
+
).replace(/^@/, "");
|
|
180
|
+
const optimisticMessage: DmMessageItem = {
|
|
181
|
+
id: `optimistic-${now}`,
|
|
182
|
+
conversationId,
|
|
183
|
+
text,
|
|
220
184
|
createdAt: now,
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
185
|
+
direction: "outbound",
|
|
186
|
+
isReplied: true,
|
|
187
|
+
mediaCount: 0,
|
|
188
|
+
sender: {
|
|
189
|
+
id: `local-${selectedConversation.accountId}`,
|
|
190
|
+
handle: senderHandle,
|
|
191
|
+
displayName: accountRecord?.name ?? senderHandle,
|
|
192
|
+
bio: "",
|
|
193
|
+
followersCount: 0,
|
|
194
|
+
avatarHue: 18,
|
|
195
|
+
createdAt: now,
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
const previousItems = previous.items as DmConversationItem[];
|
|
199
|
+
queryClient.setQueryData<QueryResponse>(dmsQueryKey, {
|
|
200
|
+
...previous,
|
|
201
|
+
items: previousItems.map((item) =>
|
|
202
|
+
item.id === conversationId
|
|
203
|
+
? {
|
|
204
|
+
...item,
|
|
205
|
+
lastMessageAt: now,
|
|
206
|
+
lastMessagePreview: text,
|
|
207
|
+
needsReply: false,
|
|
208
|
+
unreadCount: 0,
|
|
209
|
+
}
|
|
210
|
+
: item,
|
|
211
|
+
),
|
|
212
|
+
selectedConversation: previous.selectedConversation
|
|
232
213
|
? {
|
|
233
|
-
...
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
214
|
+
...previous.selectedConversation,
|
|
215
|
+
messages: [
|
|
216
|
+
...previous.selectedConversation.messages,
|
|
217
|
+
optimisticMessage,
|
|
218
|
+
],
|
|
238
219
|
}
|
|
239
|
-
:
|
|
240
|
-
),
|
|
241
|
-
);
|
|
242
|
-
|
|
243
|
-
try {
|
|
244
|
-
await postAction({
|
|
245
|
-
kind: "replyDm",
|
|
246
|
-
conversationId,
|
|
247
|
-
text,
|
|
220
|
+
: previous.selectedConversation,
|
|
248
221
|
});
|
|
222
|
+
return { previous };
|
|
223
|
+
},
|
|
224
|
+
onError: (_error, _variables, context) => {
|
|
225
|
+
if (context?.previous) {
|
|
226
|
+
queryClient.setQueryData(dmsQueryKey, context.previous);
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
onSettled: () =>
|
|
230
|
+
Promise.all([
|
|
231
|
+
queryClient.invalidateQueries({ queryKey: queryKeys.dms }),
|
|
232
|
+
queryClient.invalidateQueries({ queryKey: queryKeys.status }),
|
|
233
|
+
]),
|
|
234
|
+
});
|
|
249
235
|
|
|
236
|
+
async function replyToConversation(conversationId: string) {
|
|
237
|
+
const text = replyDraft.trim();
|
|
238
|
+
if (!text || !selectedConversation) return;
|
|
239
|
+
setReplyDraft("");
|
|
240
|
+
try {
|
|
241
|
+
await replyMutation.mutateAsync({ conversationId, text });
|
|
250
242
|
setSelectedConversationId(conversationId);
|
|
251
|
-
|
|
252
|
-
} catch (error) {
|
|
243
|
+
} catch {
|
|
253
244
|
setReplyDraft(text);
|
|
254
|
-
setMessages(previousMessages);
|
|
255
|
-
setItems(previousItems);
|
|
256
|
-
setReplyError(error instanceof Error ? error.message : "Reply failed");
|
|
257
245
|
}
|
|
258
246
|
}
|
|
259
247
|
|
|
260
248
|
function refreshLocalView() {
|
|
261
|
-
|
|
262
|
-
|
|
249
|
+
void Promise.all([
|
|
250
|
+
queryClient.invalidateQueries({ queryKey: queryKeys.dms }),
|
|
251
|
+
queryClient.invalidateQueries({ queryKey: queryKeys.status }),
|
|
252
|
+
]);
|
|
263
253
|
}
|
|
254
|
+
const loading = dmsQuery.isPending;
|
|
255
|
+
const error = dmsQuery.error
|
|
256
|
+
? dmsQuery.error instanceof Error
|
|
257
|
+
? dmsQuery.error.message
|
|
258
|
+
: "Messages unavailable"
|
|
259
|
+
: null;
|
|
260
|
+
const replyError = replyMutation.error
|
|
261
|
+
? replyMutation.error instanceof Error
|
|
262
|
+
? replyMutation.error.message
|
|
263
|
+
: "Reply failed"
|
|
264
|
+
: null;
|
|
264
265
|
|
|
265
266
|
return (
|
|
266
267
|
<>
|
|
@@ -374,7 +375,7 @@ function DmsRoute() {
|
|
|
374
375
|
</p>
|
|
375
376
|
) : null}
|
|
376
377
|
|
|
377
|
-
{loading &&
|
|
378
|
+
{(loading && items.length === 0) || switchingConversation ? (
|
|
378
379
|
<FeedLoading
|
|
379
380
|
detail="Reading local conversations and reply state"
|
|
380
381
|
label="Loading messages"
|
|
@@ -384,7 +385,7 @@ function DmsRoute() {
|
|
|
384
385
|
action={
|
|
385
386
|
<button
|
|
386
387
|
className="rounded-full bg-[var(--accent)] px-4 py-1.5 text-[14px] font-bold text-white"
|
|
387
|
-
onClick={() =>
|
|
388
|
+
onClick={() => void dmsQuery.refetch()}
|
|
388
389
|
type="button"
|
|
389
390
|
>
|
|
390
391
|
Retry
|
package/src/routes/inbox.tsx
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
import {
|
|
3
|
+
keepPreviousData,
|
|
4
|
+
useMutation,
|
|
5
|
+
useQuery,
|
|
6
|
+
useQueryClient,
|
|
7
|
+
} from "@tanstack/react-query";
|
|
2
8
|
import { Sparkles } from "lucide-react";
|
|
3
|
-
import {
|
|
9
|
+
import { useMemo, useState } from "react";
|
|
4
10
|
import { useSelectedAccountId } from "#/components/account-selection";
|
|
5
11
|
import { InboxCard } from "#/components/InboxCard";
|
|
6
|
-
import { postAction } from "#/lib/api-client";
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
InboxKind,
|
|
10
|
-
InboxResponse,
|
|
11
|
-
QueryEnvelope,
|
|
12
|
-
} from "#/lib/types";
|
|
12
|
+
import { fetchQueryEnvelope, postAction } from "#/lib/api-client";
|
|
13
|
+
import { queryKeys } from "#/lib/query-client";
|
|
14
|
+
import type { InboxItem, InboxKind, InboxResponse } from "#/lib/types";
|
|
13
15
|
import {
|
|
14
16
|
cx,
|
|
15
17
|
emptyStateClass,
|
|
@@ -40,45 +42,64 @@ const TABS: Array<{ value: InboxKind; label: string }> = [
|
|
|
40
42
|
];
|
|
41
43
|
|
|
42
44
|
function InboxRoute() {
|
|
43
|
-
const
|
|
44
|
-
const [items, setItems] = useState<InboxItem[]>([]);
|
|
45
|
+
const queryClient = useQueryClient();
|
|
45
46
|
const [kind, setKind] = useState<InboxKind>("mixed");
|
|
46
47
|
const [minScore, setMinScore] = useState("40");
|
|
47
48
|
const [hideLowSignal, setHideLowSignal] = useState(true);
|
|
48
|
-
const [refreshTick, setRefreshTick] = useState(0);
|
|
49
|
-
const [isScoring, setIsScoring] = useState(false);
|
|
50
49
|
const [activeReplyId, setActiveReplyId] = useState<string | null>(null);
|
|
51
50
|
const [replyDraft, setReplyDraft] = useState("");
|
|
52
51
|
const [isSendingReply, setIsSendingReply] = useState(false);
|
|
53
52
|
const [replyError, setReplyError] = useState<string | null>(null);
|
|
54
|
-
const
|
|
53
|
+
const statusQuery = useQuery({
|
|
54
|
+
queryKey: queryKeys.status,
|
|
55
|
+
queryFn: ({ signal }) => fetchQueryEnvelope({ signal }),
|
|
56
|
+
});
|
|
57
|
+
const meta = statusQuery.data ?? null;
|
|
55
58
|
const selectedAccountId = useSelectedAccountId(meta?.accounts);
|
|
59
|
+
const inboxQueryKey = [
|
|
60
|
+
...queryKeys.inbox,
|
|
61
|
+
{
|
|
62
|
+
hideLowSignal,
|
|
63
|
+
kind,
|
|
64
|
+
minScore,
|
|
65
|
+
selectedAccountId: selectedAccountId ?? null,
|
|
66
|
+
},
|
|
67
|
+
] as const;
|
|
68
|
+
const inboxQuery = useQuery({
|
|
69
|
+
queryKey: inboxQueryKey,
|
|
70
|
+
queryFn: async ({ signal }) => {
|
|
71
|
+
const url = new URL("/api/inbox", window.location.origin);
|
|
72
|
+
url.searchParams.set("kind", kind);
|
|
73
|
+
url.searchParams.set("minScore", minScore);
|
|
74
|
+
if (selectedAccountId) {
|
|
75
|
+
url.searchParams.set("account", selectedAccountId);
|
|
76
|
+
}
|
|
77
|
+
if (hideLowSignal) {
|
|
78
|
+
url.searchParams.set("hideLowSignal", "1");
|
|
79
|
+
}
|
|
56
80
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
.
|
|
78
|
-
|
|
79
|
-
setStats(data.stats);
|
|
80
|
-
});
|
|
81
|
-
}, [hideLowSignal, kind, minScore, refreshTick, selectedAccountId]);
|
|
81
|
+
const response = await fetch(url, { signal });
|
|
82
|
+
if (!response.ok) {
|
|
83
|
+
throw new Error(`Inbox request failed (${String(response.status)})`);
|
|
84
|
+
}
|
|
85
|
+
return (await response.json()) as InboxResponse;
|
|
86
|
+
},
|
|
87
|
+
placeholderData: keepPreviousData,
|
|
88
|
+
staleTime: 5 * 60_000,
|
|
89
|
+
});
|
|
90
|
+
const items = inboxQuery.data?.items ?? [];
|
|
91
|
+
const stats = inboxQuery.data?.stats ?? null;
|
|
92
|
+
const scoreMutation = useMutation({
|
|
93
|
+
mutationFn: () =>
|
|
94
|
+
postAction({
|
|
95
|
+
kind: "scoreInbox",
|
|
96
|
+
scoreKind: kind,
|
|
97
|
+
account: selectedAccountId,
|
|
98
|
+
limit: 8,
|
|
99
|
+
}),
|
|
100
|
+
onSuccess: () =>
|
|
101
|
+
queryClient.invalidateQueries({ queryKey: queryKeys.inbox }),
|
|
102
|
+
});
|
|
82
103
|
|
|
83
104
|
const subtitle = useMemo(() => {
|
|
84
105
|
if (!meta || !stats) return "Ranking unreplied mentions and DMs...";
|
|
@@ -86,22 +107,7 @@ function InboxRoute() {
|
|
|
86
107
|
}, [meta, stats]);
|
|
87
108
|
|
|
88
109
|
async function scoreNow() {
|
|
89
|
-
|
|
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
|
-
}
|
|
110
|
+
await scoreMutation.mutateAsync();
|
|
105
111
|
}
|
|
106
112
|
|
|
107
113
|
async function sendReply(item: InboxItem) {
|
|
@@ -125,7 +131,7 @@ function InboxRoute() {
|
|
|
125
131
|
);
|
|
126
132
|
setReplyDraft("");
|
|
127
133
|
setActiveReplyId(null);
|
|
128
|
-
|
|
134
|
+
await queryClient.invalidateQueries({ queryKey: queryKeys.inbox });
|
|
129
135
|
} catch (error) {
|
|
130
136
|
setReplyError(error instanceof Error ? error.message : "Reply failed");
|
|
131
137
|
} finally {
|
|
@@ -143,12 +149,12 @@ function InboxRoute() {
|
|
|
143
149
|
</div>
|
|
144
150
|
<button
|
|
145
151
|
className={primaryButtonClass}
|
|
146
|
-
disabled={
|
|
152
|
+
disabled={scoreMutation.isPending}
|
|
147
153
|
onClick={() => void scoreNow()}
|
|
148
154
|
type="button"
|
|
149
155
|
>
|
|
150
156
|
<Sparkles className="size-4" strokeWidth={2.2} />
|
|
151
|
-
{
|
|
157
|
+
{scoreMutation.isPending ? "Scoring..." : "Score with OpenAI"}
|
|
152
158
|
</button>
|
|
153
159
|
</div>
|
|
154
160
|
<div className="flex flex-wrap items-center gap-2 px-4 pb-3">
|