birdclaw 0.8.2 → 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 +16 -0
- package/package.json +2 -1
- 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/MarkdownCitations.tsx +680 -0
- package/src/components/MarkdownViewer.tsx +8 -674
- package/src/components/ProfileAnalysisClient.ts +191 -0
- package/src/components/ProfileAnalysisStream.tsx +16 -185
- package/src/components/ProfilePreview.tsx +2 -0
- 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/useTimelineRouteData.ts +105 -235
- package/src/lib/analysis-runtime.ts +238 -0
- package/src/lib/api-client.ts +16 -215
- package/src/lib/api-contracts.ts +328 -0
- package/src/lib/archive-import-plan.ts +102 -0
- package/src/lib/archive-import.ts +170 -239
- package/src/lib/authored-live.ts +75 -120
- package/src/lib/backup.ts +335 -424
- 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 +84 -330
- 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 -177
- package/src/lib/mentions-export.ts +1 -1
- package/src/lib/mentions-live.ts +57 -181
- 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 +36 -110
- package/src/lib/queries.ts +6 -2381
- 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 +48 -12
- 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 -257
- package/src/lib/timeline-live.ts +86 -236
- package/src/lib/timeline-read-model.ts +1191 -0
- package/src/lib/tweet-repository.ts +156 -0
- package/src/lib/tweet-search-live.ts +63 -167
- package/src/lib/web-sync.ts +67 -50
- package/src/lib/whois.ts +2 -1
- package/src/routes/__root.tsx +11 -8
- 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 -4
- 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 +167 -184
- package/src/routes/inbox.tsx +63 -57
- package/src/routes/links.tsx +31 -394
- package/src/routes/network-map.tsx +41 -344
- package/src/routes/rate-limits.tsx +17 -21
- package/src/lib/client-cache.ts +0 -109
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";
|
|
@@ -6,16 +12,15 @@ import { FeedEmpty, FeedError, FeedLoading } from "#/components/FeedState";
|
|
|
6
12
|
import { SyncNowButton } from "#/components/SyncNowButton";
|
|
7
13
|
import { useSelectedAccountId } from "#/components/account-selection";
|
|
8
14
|
import {
|
|
9
|
-
fetchCachedQueryResponse,
|
|
10
15
|
fetchQueryEnvelope,
|
|
11
|
-
|
|
16
|
+
fetchQueryResponse,
|
|
12
17
|
postAction,
|
|
13
|
-
readCachedQueryResponse,
|
|
14
18
|
} from "#/lib/api-client";
|
|
19
|
+
import { queryKeys } from "#/lib/query-client";
|
|
15
20
|
import type {
|
|
16
21
|
DmConversationItem,
|
|
17
22
|
DmMessageItem,
|
|
18
|
-
|
|
23
|
+
QueryResponse,
|
|
19
24
|
ReplyFilter,
|
|
20
25
|
} from "#/lib/types";
|
|
21
26
|
import { useDebouncedValue } from "#/components/useDebouncedValue";
|
|
@@ -65,12 +70,7 @@ const filterNumberFieldClass =
|
|
|
65
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)]";
|
|
66
71
|
|
|
67
72
|
function DmsRoute() {
|
|
68
|
-
const
|
|
69
|
-
const [items, setItems] = useState<DmConversationItem[]>([]);
|
|
70
|
-
const [messages, setMessages] = useState<DmMessageItem[]>([]);
|
|
71
|
-
const [loadedConversationId, setLoadedConversationId] = useState<
|
|
72
|
-
string | undefined
|
|
73
|
-
>();
|
|
73
|
+
const queryClient = useQueryClient();
|
|
74
74
|
const [selectedConversationId, setSelectedConversationId] = useState<
|
|
75
75
|
string | undefined
|
|
76
76
|
>();
|
|
@@ -81,204 +81,187 @@ function DmsRoute() {
|
|
|
81
81
|
const [sort, setSort] = useState<"recent" | "followers">("recent");
|
|
82
82
|
const [search, setSearch] = useState("");
|
|
83
83
|
const [replyDraft, setReplyDraft] = useState("");
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
84
|
+
const statusQuery = useQuery({
|
|
85
|
+
queryKey: queryKeys.status,
|
|
86
|
+
queryFn: ({ signal }) => fetchQueryEnvelope({ signal }),
|
|
87
|
+
});
|
|
88
|
+
const meta = statusQuery.data ?? null;
|
|
88
89
|
const selectedAccountId = useSelectedAccountId(meta?.accounts);
|
|
89
90
|
const debouncedSearch = useDebouncedValue(search, 180);
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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;
|
|
94
136
|
|
|
95
137
|
useEffect(() => {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
]);
|
|
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]);
|
|
194
147
|
|
|
195
148
|
const selectedConversation =
|
|
196
149
|
items.find((item) => item.id === selectedConversationId) ?? null;
|
|
197
|
-
const switchingConversation =
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
);
|
|
150
|
+
const switchingConversation = Boolean(
|
|
151
|
+
!dmsQuery.isError &&
|
|
152
|
+
selectedConversationId &&
|
|
153
|
+
loadedConversationId &&
|
|
154
|
+
selectedConversationId !== loadedConversationId,
|
|
155
|
+
);
|
|
204
156
|
|
|
205
157
|
const subtitle = useMemo(() => {
|
|
206
158
|
if (!meta) return "Loading direct messages...";
|
|
207
159
|
return `${String(meta.stats.dms)} conversations cached locally`;
|
|
208
160
|
}, [meta]);
|
|
209
|
-
|
|
210
|
-
|
|
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}`,
|
|
161
|
+
const replyMutation = useMutation({
|
|
162
|
+
mutationFn: ({
|
|
223
163
|
conversationId,
|
|
224
164
|
text,
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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,
|
|
236
184
|
createdAt: now,
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
|
248
213
|
? {
|
|
249
|
-
...
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
214
|
+
...previous.selectedConversation,
|
|
215
|
+
messages: [
|
|
216
|
+
...previous.selectedConversation.messages,
|
|
217
|
+
optimisticMessage,
|
|
218
|
+
],
|
|
254
219
|
}
|
|
255
|
-
:
|
|
256
|
-
),
|
|
257
|
-
);
|
|
258
|
-
|
|
259
|
-
try {
|
|
260
|
-
await postAction({
|
|
261
|
-
kind: "replyDm",
|
|
262
|
-
conversationId,
|
|
263
|
-
text,
|
|
220
|
+
: previous.selectedConversation,
|
|
264
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
|
+
});
|
|
265
235
|
|
|
266
|
-
|
|
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 });
|
|
267
242
|
setSelectedConversationId(conversationId);
|
|
268
|
-
|
|
269
|
-
} catch (error) {
|
|
243
|
+
} catch {
|
|
270
244
|
setReplyDraft(text);
|
|
271
|
-
setMessages(previousMessages);
|
|
272
|
-
setItems(previousItems);
|
|
273
|
-
setReplyError(error instanceof Error ? error.message : "Reply failed");
|
|
274
245
|
}
|
|
275
246
|
}
|
|
276
247
|
|
|
277
248
|
function refreshLocalView() {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
249
|
+
void Promise.all([
|
|
250
|
+
queryClient.invalidateQueries({ queryKey: queryKeys.dms }),
|
|
251
|
+
queryClient.invalidateQueries({ queryKey: queryKeys.status }),
|
|
252
|
+
]);
|
|
281
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;
|
|
282
265
|
|
|
283
266
|
return (
|
|
284
267
|
<>
|
|
@@ -392,7 +375,7 @@ function DmsRoute() {
|
|
|
392
375
|
</p>
|
|
393
376
|
) : null}
|
|
394
377
|
|
|
395
|
-
{loading &&
|
|
378
|
+
{(loading && items.length === 0) || switchingConversation ? (
|
|
396
379
|
<FeedLoading
|
|
397
380
|
detail="Reading local conversations and reply state"
|
|
398
381
|
label="Loading messages"
|
|
@@ -402,7 +385,7 @@ function DmsRoute() {
|
|
|
402
385
|
action={
|
|
403
386
|
<button
|
|
404
387
|
className="rounded-full bg-[var(--accent)] px-4 py-1.5 text-[14px] font-bold text-white"
|
|
405
|
-
onClick={() =>
|
|
388
|
+
onClick={() => void dmsQuery.refetch()}
|
|
406
389
|
type="button"
|
|
407
390
|
>
|
|
408
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">
|