@johndimm/constellations 1.0.1 → 1.0.2
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/App.tsx +352 -70
- package/FullPageConstellations.tsx +7 -4
- package/components/AppConfirmDialog.tsx +1 -0
- package/components/AppHeader.tsx +69 -29
- package/components/AppNotifications.tsx +1 -0
- package/components/BrowsePeople.tsx +3 -0
- package/components/ControlPanel.tsx +46 -371
- package/components/Graph.tsx +251 -87
- package/components/HelpOverlay.tsx +1 -0
- package/components/NodeContextMenu.tsx +123 -3
- package/components/PeopleBrowserSidebar.tsx +15 -6
- package/components/Sidebar.tsx +46 -19
- package/components/TimelineView.tsx +1 -0
- package/hooks/useExpansion.ts +61 -229
- package/hooks/useGraphActions.ts +1 -0
- package/hooks/useGraphState.ts +75 -40
- package/hooks/useKioskMode.ts +1 -0
- package/hooks/useNodeClickHandler.ts +23 -15
- package/hooks/useSearchHandlers.ts +57 -19
- package/host.ts +1 -1
- package/index.css +17 -3
- package/package.json +2 -1
- package/services/aiService.ts +23 -0
- package/services/aiUtils.ts +216 -207
- package/services/cacheService.ts +1 -0
- package/services/crossrefService.ts +1 -0
- package/services/deepseekService.ts +467 -0
- package/services/geminiService.ts +532 -733
- package/services/graphUtils.ts +128 -18
- package/services/imageService.ts +18 -0
- package/services/openAlexService.ts +1 -0
- package/services/resolveImageForTitle.ts +458 -0
- package/services/wikipediaImage.ts +1 -0
- package/services/wikipediaService.ts +56 -46
- package/types.ts +3 -0
- package/utils/evidenceUtils.ts +1 -0
- package/utils/graphLogicUtils.ts +1 -0
- package/utils/wikiUtils.ts +14 -2
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { jsonFromResponse } from "./aiUtils";
|
|
2
4
|
|
|
3
5
|
type WikiImageCacheEntry = { url: string | null; pageId?: number; pageTitle?: string; misses?: number };
|
|
4
6
|
|
|
@@ -59,7 +61,8 @@ export const fetchWikipediaImage = async (query: string, context?: string): Prom
|
|
|
59
61
|
try {
|
|
60
62
|
const url = `${api}?action=query&format=json&prop=imageinfo&titles=${encodeURIComponent(fileTitle)}&iiprop=url&iiurlwidth=500&origin=*`;
|
|
61
63
|
const res = await fetch(url, { signal });
|
|
62
|
-
const data = await res
|
|
64
|
+
const data = (await jsonFromResponse(res)) as { query?: { pages?: Record<string, unknown> } } | null;
|
|
65
|
+
if (!data) continue;
|
|
63
66
|
const pages = data.query?.pages;
|
|
64
67
|
if (pages) {
|
|
65
68
|
const page = Object.values(pages)[0] as any;
|
|
@@ -78,7 +81,7 @@ export const fetchWikipediaImage = async (query: string, context?: string): Prom
|
|
|
78
81
|
try {
|
|
79
82
|
const wdUrl = `https://www.wikidata.org/w/api.php?action=wbgetentities&format=json&props=claims&ids=${qid}&origin=*`;
|
|
80
83
|
const wdRes = await fetch(wdUrl, { signal });
|
|
81
|
-
const wdData = await wdRes
|
|
84
|
+
const wdData = (await jsonFromResponse(wdRes)) as { entities?: Record<string, { claims?: any }> } | null;
|
|
82
85
|
const claims = wdData?.entities?.[qid]?.claims;
|
|
83
86
|
const p18 = claims?.P18?.[0]?.mainsnak?.datavalue?.value as string | undefined;
|
|
84
87
|
if (!p18) return null;
|
|
@@ -95,8 +98,8 @@ export const fetchWikipediaImage = async (query: string, context?: string): Prom
|
|
|
95
98
|
try {
|
|
96
99
|
const ppUrl = `https://en.wikipedia.org/w/api.php?action=query&format=json&prop=pageprops&titles=${encodeURIComponent(title)}&redirects=1&origin=*`;
|
|
97
100
|
const ppRes = await fetch(ppUrl, { signal });
|
|
98
|
-
const ppData = await ppRes
|
|
99
|
-
const pages = ppData?.query?.pages;
|
|
101
|
+
const ppData = await jsonFromResponse(ppRes);
|
|
102
|
+
const pages = (ppData as { query?: { pages?: unknown } } | null)?.query?.pages;
|
|
100
103
|
const page = pages ? (Object.values(pages)[0] as any) : null;
|
|
101
104
|
const qid = page?.pageprops?.wikibase_item;
|
|
102
105
|
if (!qid || !/^Q\d+$/.test(qid)) return null;
|
|
@@ -112,7 +115,8 @@ export const fetchWikipediaImage = async (query: string, context?: string): Prom
|
|
|
112
115
|
// 1. Get page info, thumbnail, and all images in one go
|
|
113
116
|
const url = `https://en.wikipedia.org/w/api.php?action=query&format=json&prop=pageimages|pageprops|images&titles=${encodeURIComponent(title)}&pithumbsize=500&imlimit=50&redirects=1&origin=*`;
|
|
114
117
|
const res = await fetch(url, { signal });
|
|
115
|
-
const data = await res
|
|
118
|
+
const data = (await jsonFromResponse(res)) as { query?: { pages?: Record<string, unknown> } } | null;
|
|
119
|
+
if (!data) return { url: null };
|
|
116
120
|
|
|
117
121
|
const pages = data.query?.pages;
|
|
118
122
|
if (!pages) return { url: null };
|
|
@@ -146,7 +150,7 @@ export const fetchWikipediaImage = async (query: string, context?: string): Prom
|
|
|
146
150
|
if (candidates.length === 0) return { url: null };
|
|
147
151
|
|
|
148
152
|
const normalized = query.trim().toLowerCase();
|
|
149
|
-
const queryWords = normalized.split(/\s+/).filter(w => w.length > 1);
|
|
153
|
+
const queryWords = normalized.split(/\s+/).filter((w: string) => w.length > 1);
|
|
150
154
|
const isPerson = context?.toLowerCase() === 'person';
|
|
151
155
|
|
|
152
156
|
const scoredCandidates = candidates.map(c => {
|
|
@@ -198,7 +202,7 @@ export const fetchWikipediaImage = async (query: string, context?: string): Prom
|
|
|
198
202
|
if (t.includes('.png')) s -= isPerson ? 20 : 50;
|
|
199
203
|
|
|
200
204
|
// Prefer solo filenames
|
|
201
|
-
const wordCount = t.split(/[^a-z]/).filter(w => w.length > 2).length;
|
|
205
|
+
const wordCount = t.split(/[^a-z]/).filter((w: string) => w.length > 2).length;
|
|
202
206
|
s -= (wordCount * 15); // Stronger penalty for long, descriptive filenames
|
|
203
207
|
|
|
204
208
|
return { ...c, score: s };
|
|
@@ -245,7 +249,8 @@ export const fetchWikipediaImage = async (query: string, context?: string): Prom
|
|
|
245
249
|
const url = `https://www.googleapis.com/books/v1/volumes?q=${encodeURIComponent(q)}&maxResults=1`;
|
|
246
250
|
const res = await fetch(url, { signal });
|
|
247
251
|
if (res.ok) {
|
|
248
|
-
const data = await res
|
|
252
|
+
const data = (await jsonFromResponse(res)) as { items?: { volumeInfo?: { imageLinks?: { thumbnail?: string } } }[] } | null;
|
|
253
|
+
if (!data) return null;
|
|
249
254
|
const img = data.items?.[0]?.volumeInfo?.imageLinks?.thumbnail;
|
|
250
255
|
return img ? img.replace('http://', 'https://') : null;
|
|
251
256
|
}
|
|
@@ -279,10 +284,10 @@ export const fetchWikipediaImage = async (query: string, context?: string): Prom
|
|
|
279
284
|
// console.log(`🔍 [ImageSearch] Attempt 1 (Media-Aware): "${searchQuery}"`);
|
|
280
285
|
const initialSearchUrl = `https://en.wikipedia.org/w/api.php?action=query&format=json&list=search&srsearch=${encodeURIComponent(searchQuery)}&srlimit=5&origin=*`;
|
|
281
286
|
const initialSearchRes = await fetch(initialSearchUrl, { signal: controller.signal });
|
|
282
|
-
const initialSearchData = await initialSearchRes
|
|
287
|
+
const initialSearchData = (await jsonFromResponse(initialSearchRes)) as { query?: { search?: { title: string; snippet?: string }[] } } | null;
|
|
283
288
|
|
|
284
289
|
let bestTitle = query;
|
|
285
|
-
if (initialSearchData
|
|
290
|
+
if (initialSearchData?.query?.search?.length) {
|
|
286
291
|
const results = initialSearchData.query.search;
|
|
287
292
|
const normalized = baseTitle.toLowerCase();
|
|
288
293
|
const avoidMedia = false; // For images, we generally allow media if it's the right title
|
|
@@ -305,7 +310,7 @@ export const fetchWikipediaImage = async (query: string, context?: string): Prom
|
|
|
305
310
|
|
|
306
311
|
// 2. Context matching
|
|
307
312
|
if (context) {
|
|
308
|
-
const words = context.toLowerCase().split(/\s+/).filter(w => w.length > 2);
|
|
313
|
+
const words = context.toLowerCase().split(/\s+/).filter((w: string) => w.length > 2);
|
|
309
314
|
words.forEach(word => {
|
|
310
315
|
if (title.includes(word)) s += 100;
|
|
311
316
|
if (snippet.includes(word)) s += 50;
|
|
@@ -322,7 +327,9 @@ export const fetchWikipediaImage = async (query: string, context?: string): Prom
|
|
|
322
327
|
return s;
|
|
323
328
|
};
|
|
324
329
|
|
|
325
|
-
const scored = results
|
|
330
|
+
const scored = results
|
|
331
|
+
.map((r: any) => ({ r, score: scoreResult(r) }))
|
|
332
|
+
.sort((a: { score: number }, b: { score: number }) => b.score - a.score);
|
|
326
333
|
bestTitle = scored[0]?.r?.title || query;
|
|
327
334
|
// console.log(`✅ [ImageSearch] Chosen result "${bestTitle}" with score ${scored[0]?.score ?? 'n/a'}`);
|
|
328
335
|
}
|
|
@@ -336,9 +343,9 @@ export const fetchWikipediaImage = async (query: string, context?: string): Prom
|
|
|
336
343
|
// console.log(`🔍 [ImageSearch] Attempt 2 (Commons for Person): "${baseTitle}"`);
|
|
337
344
|
const commonsUrl = `https://commons.wikimedia.org/w/api.php?action=query&format=json&list=search&srsearch=${encodeURIComponent(baseTitle)}&srnamespace=6&srlimit=10&origin=*`;
|
|
338
345
|
const commonsRes = await fetch(commonsUrl, { signal: controller.signal });
|
|
339
|
-
const commonsData = await commonsRes
|
|
340
|
-
if (commonsData
|
|
341
|
-
const baseWords = baseTitle.toLowerCase().split(/\s+/).filter(w => w.length > 1);
|
|
346
|
+
const commonsData = (await jsonFromResponse(commonsRes)) as { query?: { search?: any[] } } | null;
|
|
347
|
+
if (commonsData?.query?.search?.length) {
|
|
348
|
+
const baseWords = baseTitle.toLowerCase().split(/\s+/).filter((w: string) => w.length > 1);
|
|
342
349
|
const scoredResults = commonsData.query.search.map((res: any) => {
|
|
343
350
|
const t = res.title.toLowerCase();
|
|
344
351
|
if (excludePatterns.some(p => t.includes(p))) return { res, score: -1000 };
|
|
@@ -356,7 +363,7 @@ export const fetchWikipediaImage = async (query: string, context?: string): Prom
|
|
|
356
363
|
if (t.includes('.png')) s -= 20; // Reduced penalty for Person
|
|
357
364
|
if (t.includes('.svg') || t.includes('.webm') || t.includes('.gif')) s -= 300;
|
|
358
365
|
|
|
359
|
-
const wordCount = t.split(/[^a-z]/).filter(w => w.length > 2).length;
|
|
366
|
+
const wordCount = t.split(/[^a-z]/).filter((w: string) => w.length > 2).length;
|
|
360
367
|
s -= (wordCount * 15);
|
|
361
368
|
|
|
362
369
|
return { res, score: s };
|
|
@@ -386,9 +393,9 @@ export const fetchWikipediaImage = async (query: string, context?: string): Prom
|
|
|
386
393
|
// console.log(`🔍 [ImageSearch] Attempt 4 (Commons): "${baseTitle}"`);
|
|
387
394
|
const commonsUrl = `https://commons.wikimedia.org/w/api.php?action=query&format=json&list=search&srsearch=${encodeURIComponent(baseTitle)}&srnamespace=6&srlimit=10&origin=*`;
|
|
388
395
|
const commonsRes = await fetch(commonsUrl, { signal: controller.signal });
|
|
389
|
-
const commonsData = await commonsRes
|
|
390
|
-
if (commonsData
|
|
391
|
-
const baseWords = baseTitle.toLowerCase().split(/\s+/).filter(w => w.length > 1);
|
|
396
|
+
const commonsData = (await jsonFromResponse(commonsRes)) as { query?: { search?: any[] } } | null;
|
|
397
|
+
if (commonsData?.query?.search?.length) {
|
|
398
|
+
const baseWords = baseTitle.toLowerCase().split(/\s+/).filter((w: string) => w.length > 1);
|
|
392
399
|
const scoredResults = commonsData.query.search.map((res: any) => {
|
|
393
400
|
const t = res.title.toLowerCase();
|
|
394
401
|
if (excludePatterns.some(p => t.includes(p))) return { res, score: -1000 };
|
|
@@ -408,7 +415,7 @@ export const fetchWikipediaImage = async (query: string, context?: string): Prom
|
|
|
408
415
|
if (t.includes('.png')) s -= 50;
|
|
409
416
|
if (t.includes('.svg') || t.includes('.webm') || t.includes('.gif')) s -= 300;
|
|
410
417
|
|
|
411
|
-
const wordCount = t.split(/[^a-z]/).filter(w => w.length > 2).length;
|
|
418
|
+
const wordCount = t.split(/[^a-z]/).filter((w: string) => w.length > 2).length;
|
|
412
419
|
s -= (wordCount * 15);
|
|
413
420
|
|
|
414
421
|
return { res, score: s };
|
|
@@ -426,8 +433,8 @@ export const fetchWikipediaImage = async (query: string, context?: string): Prom
|
|
|
426
433
|
// console.log(`🔍 [ImageSearch] Attempt 5 (Search): "${baseTitle}"`);
|
|
427
434
|
const searchUrl = `https://en.wikipedia.org/w/api.php?action=query&format=json&list=search&srsearch=${encodeURIComponent(baseTitle)}&srlimit=5&origin=*`;
|
|
428
435
|
const searchRes = await fetch(searchUrl, { signal: controller.signal });
|
|
429
|
-
const searchData = await searchRes
|
|
430
|
-
if (searchData
|
|
436
|
+
const searchData = (await jsonFromResponse(searchRes)) as { query?: { search?: { title: string }[] } } | null;
|
|
437
|
+
if (searchData?.query?.search?.length) {
|
|
431
438
|
for (const result of searchData.query.search) {
|
|
432
439
|
const img = await fetchPageImage(result.title, controller.signal);
|
|
433
440
|
if (img.url) return img;
|
|
@@ -498,7 +505,8 @@ export const fetchWikipediaSummary = async (
|
|
|
498
505
|
try {
|
|
499
506
|
const directUrl = `https://en.wikipedia.org/w/api.php?action=query&format=json&prop=extracts|pageprops&exintro&explaintext&titles=${encodeURIComponent(titleToFetch)}&redirects=1&origin=*`;
|
|
500
507
|
const directRes = await fetch(directUrl);
|
|
501
|
-
const directData = await directRes
|
|
508
|
+
const directData = (await jsonFromResponse(directRes)) as { query?: { pages?: unknown; redirects?: unknown } } | null;
|
|
509
|
+
if (!directData) return null;
|
|
502
510
|
const directPages = directData.query?.pages;
|
|
503
511
|
|
|
504
512
|
if (directPages) {
|
|
@@ -541,7 +549,7 @@ export const fetchWikipediaSummary = async (
|
|
|
541
549
|
// for disambiguation (e.g., "Republic (book)" vs "Republic").
|
|
542
550
|
const cleanQuery = query.trim();
|
|
543
551
|
const normalized = cleanQuery.toLowerCase();
|
|
544
|
-
const queryNameParts = normalized.split(/[\s-]+/).filter(w => w.length > 2);
|
|
552
|
+
const queryNameParts = normalized.split(/[\s-]+/).filter((w: string) => w.length > 2);
|
|
545
553
|
const looksLikePersonName = queryNameParts.length >= 2 && !/\d/.test(cleanQuery);
|
|
546
554
|
const queryLastName = looksLikePersonName ? queryNameParts[queryNameParts.length - 1].toLowerCase() : null;
|
|
547
555
|
|
|
@@ -564,7 +572,7 @@ export const fetchWikipediaSummary = async (
|
|
|
564
572
|
const directExact = await tryDirectLookup(cleanQuery);
|
|
565
573
|
if (directExact?.extract) {
|
|
566
574
|
if (queryLastName) {
|
|
567
|
-
const titleParts = String(directExact.title || "").toLowerCase().split(/[\s-]+/).filter(w => w.length > 2);
|
|
575
|
+
const titleParts = String(directExact.title || "").toLowerCase().split(/[\s-]+/).filter((w: string) => w.length > 2);
|
|
568
576
|
// If it's a redirect, we are MUCH more lenient. Napoleon Bonaparte -> Napoleon is a classic case.
|
|
569
577
|
if (!titleParts.includes(queryLastName) && !directExact.redirected) {
|
|
570
578
|
// console.log(`⚠️ [Wiki] Ignoring direct match "${directExact.title}" for "${cleanQuery}" (missing last-name match and no redirect).`);
|
|
@@ -639,10 +647,10 @@ export const fetchWikipediaSummary = async (
|
|
|
639
647
|
const avoidMedia = /\b(project|program|programme|operation|war|battle|campaign|treaty|scandal|scientist)\b/i.test(baseQuery);
|
|
640
648
|
const searchUrl = `https://en.wikipedia.org/w/api.php?action=query&format=json&list=search&srsearch=${encodeURIComponent(searchQuery)}&srlimit=5&origin=*`;
|
|
641
649
|
const searchRes = await fetch(searchUrl);
|
|
642
|
-
const searchData = await searchRes
|
|
650
|
+
const searchData = (await jsonFromResponse(searchRes)) as { query?: { search?: any[] } } | null;
|
|
643
651
|
|
|
644
652
|
let bestTitle = query;
|
|
645
|
-
if (searchData
|
|
653
|
+
if (searchData?.query?.search?.length) {
|
|
646
654
|
const results = searchData.query.search;
|
|
647
655
|
const scoreResult = (r: any, index: number) => {
|
|
648
656
|
const title = r.title.toLowerCase();
|
|
@@ -697,7 +705,7 @@ export const fetchWikipediaSummary = async (
|
|
|
697
705
|
|
|
698
706
|
// 2. Context matching
|
|
699
707
|
if (context) {
|
|
700
|
-
const words = context.toLowerCase().split(/\s+/).filter(w => w.length > 2);
|
|
708
|
+
const words = context.toLowerCase().split(/\s+/).filter((w: string) => w.length > 2);
|
|
701
709
|
words.forEach(word => {
|
|
702
710
|
if (title.includes(word)) s += 100;
|
|
703
711
|
if (snippet.includes(word)) s += 50;
|
|
@@ -754,7 +762,7 @@ export const fetchWikipediaSummary = async (
|
|
|
754
762
|
bestTitle = scored[0]?.r?.title || query;
|
|
755
763
|
|
|
756
764
|
|
|
757
|
-
const titleNameParts = bestTitle.toLowerCase().split(/[\s-]+/).filter(w => w.length > 2);
|
|
765
|
+
const titleNameParts = bestTitle.toLowerCase().split(/[\s-]+/).filter((w: string) => w.length > 2);
|
|
758
766
|
// Require at least one full word match, not just a substring overlap
|
|
759
767
|
const hasFullWordMatch = queryNameParts.some(q => titleNameParts.includes(q));
|
|
760
768
|
const hasOverlap = queryNameParts.some(q => titleNameParts.some(t => t.includes(q) || q.includes(t)));
|
|
@@ -764,7 +772,7 @@ export const fetchWikipediaSummary = async (
|
|
|
764
772
|
|
|
765
773
|
for (const titleToTry of candidates) {
|
|
766
774
|
if (queryNameParts.length > 0) {
|
|
767
|
-
const candidateParts = titleToTry.toLowerCase().split(/[\s-]+/).filter(w => w.length > 2);
|
|
775
|
+
const candidateParts = titleToTry.toLowerCase().split(/[\s-]+/).filter((w: string) => w.length > 2);
|
|
768
776
|
|
|
769
777
|
// STRICT PERSON MATCHING:
|
|
770
778
|
// If we are looking for a person (query has 2+ name parts),
|
|
@@ -789,7 +797,8 @@ export const fetchWikipediaSummary = async (
|
|
|
789
797
|
}
|
|
790
798
|
const summaryUrl = `https://en.wikipedia.org/w/api.php?action=query&format=json&prop=extracts|pageprops&exintro&explaintext&titles=${encodeURIComponent(titleToTry)}&redirects=1&origin=*`;
|
|
791
799
|
const summaryRes = await fetch(summaryUrl);
|
|
792
|
-
const summaryData = await summaryRes
|
|
800
|
+
const summaryData = (await jsonFromResponse(summaryRes)) as { query?: { pages?: unknown } } | null;
|
|
801
|
+
if (!summaryData) continue;
|
|
793
802
|
const pages = summaryData.query?.pages;
|
|
794
803
|
|
|
795
804
|
if (pages) {
|
|
@@ -831,14 +840,14 @@ export const fetchWikipediaSummary = async (
|
|
|
831
840
|
}
|
|
832
841
|
|
|
833
842
|
if (queryNameParts.length >= 2) {
|
|
834
|
-
const pageParts = String(page.title || "").toLowerCase().split(/[\s-]+/).filter(w => w.length > 2);
|
|
843
|
+
const pageParts = String(page.title || "").toLowerCase().split(/[\s-]+/).filter((w: string) => w.length > 2);
|
|
835
844
|
const allMatch = queryNameParts.every(q => pageParts.includes(q));
|
|
836
845
|
if (!allMatch) {
|
|
837
846
|
// console.log(`⚠️ [Wiki] Skipping resolved title "${page.title}" for "${cleanQuery}" (not all name parts match).`);
|
|
838
847
|
continue;
|
|
839
848
|
}
|
|
840
849
|
} else if (queryLastName) {
|
|
841
|
-
const pageParts = String(page.title || "").toLowerCase().split(/[\s-]+/).filter(w => w.length > 2);
|
|
850
|
+
const pageParts = String(page.title || "").toLowerCase().split(/[\s-]+/).filter((w: string) => w.length > 2);
|
|
842
851
|
if (!pageParts.includes(queryLastName)) {
|
|
843
852
|
// console.log(`⚠️ [Wiki] Skipping resolved title "${page.title}" for "${cleanQuery}" (missing last-name match).`);
|
|
844
853
|
continue;
|
|
@@ -936,9 +945,9 @@ export const fetchWikipediaExtract = async (
|
|
|
936
945
|
// when exchars is set (returns fewer chars than the article actually contains). We fetch
|
|
937
946
|
// the full extract and truncate client-side instead.
|
|
938
947
|
const url = `https://en.wikipedia.org/w/api.php?action=query&format=json&prop=extracts|pageprops&explaintext&titles=${encodeURIComponent(title)}&redirects=1&origin=*`;
|
|
939
|
-
|
|
940
|
-
const
|
|
941
|
-
|
|
948
|
+
const res = await fetch(url);
|
|
949
|
+
const data = (await jsonFromResponse(res)) as { query?: { pages?: unknown } } | null;
|
|
950
|
+
if (!data) return { extract: null, pageid: null, title: null };
|
|
942
951
|
const pages = data.query?.pages;
|
|
943
952
|
if (!pages) return { extract: null, pageid: null, title: null };
|
|
944
953
|
const page = Object.values(pages)[0] as any;
|
|
@@ -988,8 +997,8 @@ export const fetchWikidataCastForTitle = async (title: string, limit: number = 1
|
|
|
988
997
|
try {
|
|
989
998
|
const pagepropsUrl = `https://en.wikipedia.org/w/api.php?action=query&format=json&prop=pageprops&titles=${encodeURIComponent(title)}&redirects=1&origin=*`;
|
|
990
999
|
const ppRes = await fetch(pagepropsUrl, { signal });
|
|
991
|
-
const ppData = await ppRes
|
|
992
|
-
const pages = ppData?.query?.pages;
|
|
1000
|
+
const ppData = await jsonFromResponse(ppRes);
|
|
1001
|
+
const pages = (ppData as { query?: { pages?: unknown } } | null)?.query?.pages;
|
|
993
1002
|
if (pages) {
|
|
994
1003
|
const page = Object.values(pages)[0] as any;
|
|
995
1004
|
const candidate = page?.pageprops?.wikibase_item;
|
|
@@ -1008,8 +1017,8 @@ export const fetchWikidataCastForTitle = async (title: string, limit: number = 1
|
|
|
1008
1017
|
|
|
1009
1018
|
const entityUrl = `https://www.wikidata.org/w/api.php?action=wbgetentities&format=json&props=claims&ids=${encodeURIComponent(wikidataId)}&origin=*`;
|
|
1010
1019
|
const entRes = await fetch(entityUrl, { signal });
|
|
1011
|
-
const entData = await entRes
|
|
1012
|
-
const claims = entData?.entities?.[wikidataId]?.claims;
|
|
1020
|
+
const entData = await jsonFromResponse(entRes);
|
|
1021
|
+
const claims = (entData as { entities?: Record<string, { claims?: unknown }> } | null)?.entities?.[wikidataId]?.claims;
|
|
1013
1022
|
if (!claims) return [];
|
|
1014
1023
|
|
|
1015
1024
|
const castIds = extractWikidataItemIds(claims, "P161");
|
|
@@ -1036,7 +1045,8 @@ const fetchWikidataLabels = async (ids: string[], signal: AbortSignal): Promise<
|
|
|
1036
1045
|
try {
|
|
1037
1046
|
const url = `https://www.wikidata.org/w/api.php?action=wbgetentities&format=json&props=labels&languages=en&ids=${encodeURIComponent(chunk.join("|"))}&origin=*`;
|
|
1038
1047
|
const res = await fetch(url, { signal });
|
|
1039
|
-
const data = await res
|
|
1048
|
+
const data = (await jsonFromResponse(res)) as { entities?: Record<string, { labels?: { en?: { value?: string } } }> } | null;
|
|
1049
|
+
if (!data) continue;
|
|
1040
1050
|
const entities = data?.entities || {};
|
|
1041
1051
|
for (const [id, ent] of Object.entries<any>(entities)) {
|
|
1042
1052
|
const label = ent?.labels?.en?.value;
|
|
@@ -1053,7 +1063,7 @@ const resolveWikidataIdBySearch = async (label: string, signal: AbortSignal): Pr
|
|
|
1053
1063
|
try {
|
|
1054
1064
|
const url = `https://www.wikidata.org/w/api.php?action=wbsearchentities&format=json&language=en&limit=8&search=${encodeURIComponent(label)}&origin=*`;
|
|
1055
1065
|
const res = await fetch(url, { signal });
|
|
1056
|
-
const data = await res
|
|
1066
|
+
const data = (await jsonFromResponse(res)) as { search?: any[] } | null;
|
|
1057
1067
|
const results: any[] = data?.search || [];
|
|
1058
1068
|
if (!results.length) return null;
|
|
1059
1069
|
|
|
@@ -1095,8 +1105,8 @@ export const fetchWikidataKeyPeopleForTitle = async (title: string): Promise<Wik
|
|
|
1095
1105
|
try {
|
|
1096
1106
|
const pagepropsUrl = `https://en.wikipedia.org/w/api.php?action=query&format=json&prop=pageprops&titles=${encodeURIComponent(title)}&redirects=1&origin=*`;
|
|
1097
1107
|
const ppRes = await fetch(pagepropsUrl, { signal });
|
|
1098
|
-
const ppData = await ppRes
|
|
1099
|
-
const pages = ppData?.query?.pages;
|
|
1108
|
+
const ppData = await jsonFromResponse(ppRes);
|
|
1109
|
+
const pages = (ppData as { query?: { pages?: unknown } } | null)?.query?.pages;
|
|
1100
1110
|
if (pages) {
|
|
1101
1111
|
const page = Object.values(pages)[0] as any;
|
|
1102
1112
|
const resolvedTitle = String(page?.title || "");
|
|
@@ -1127,7 +1137,7 @@ export const fetchWikidataKeyPeopleForTitle = async (title: string): Promise<Wik
|
|
|
1127
1137
|
// 2) Pull key-people claims.
|
|
1128
1138
|
const entityUrl = `https://www.wikidata.org/w/api.php?action=wbgetentities&format=json&props=claims&ids=${encodeURIComponent(wikidataId)}&origin=*`;
|
|
1129
1139
|
const entRes = await fetch(entityUrl, { signal });
|
|
1130
|
-
const entData = await entRes
|
|
1140
|
+
const entData = (await jsonFromResponse(entRes)) as { entities?: Record<string, { claims?: unknown }> } | null;
|
|
1131
1141
|
const entity = entData?.entities?.[wikidataId];
|
|
1132
1142
|
const claims = entity?.claims;
|
|
1133
1143
|
if (!claims) {
|
package/types.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import { SimulationNodeDatum, SimulationLinkDatum } from 'd3';
|
|
2
3
|
|
|
3
4
|
export interface GraphNode extends SimulationNodeDatum {
|
|
@@ -20,6 +21,8 @@ export interface GraphNode extends SimulationNodeDatum {
|
|
|
20
21
|
atomic_type?: string; // e.g. "Symptom"
|
|
21
22
|
composite_type?: string; // e.g. "Disease"
|
|
22
23
|
mentioningPageTitles?: string[]; // Titles of articles mentioning this entity (for non-article fallback)
|
|
24
|
+
/** Measured card height in timeline view (set by Graph layout). */
|
|
25
|
+
h?: number;
|
|
23
26
|
// D3 Simulation properties explicitly defined to ensure access
|
|
24
27
|
x?: number;
|
|
25
28
|
y?: number;
|
package/utils/evidenceUtils.ts
CHANGED
package/utils/graphLogicUtils.ts
CHANGED
package/utils/wikiUtils.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import { fetchWikipediaExtract } from '../services/wikipediaService';
|
|
2
3
|
|
|
3
4
|
export const buildWikiUrl = (title: string, wikipediaId?: string | number) => {
|
|
@@ -21,10 +22,21 @@ export const looksLikeWikipediaTitle = (t: unknown) => {
|
|
|
21
22
|
return true;
|
|
22
23
|
};
|
|
23
24
|
|
|
24
|
-
const
|
|
25
|
-
|
|
25
|
+
const serverExtractCache = new Map<string, string | null>();
|
|
26
|
+
|
|
27
|
+
function getExtractCacheMap(): Map<string, string | null> {
|
|
28
|
+
if (typeof window === 'undefined') {
|
|
29
|
+
return serverExtractCache;
|
|
30
|
+
}
|
|
31
|
+
const w = window as unknown as { __wikiExtractCache?: Map<string, string | null> };
|
|
32
|
+
if (!w.__wikiExtractCache) {
|
|
33
|
+
w.__wikiExtractCache = new Map();
|
|
34
|
+
}
|
|
35
|
+
return w.__wikiExtractCache;
|
|
36
|
+
}
|
|
26
37
|
|
|
27
38
|
export const getExtractCached = async (title: string) => {
|
|
39
|
+
const extractCache = getExtractCacheMap();
|
|
28
40
|
const key = String(title || '').trim();
|
|
29
41
|
if (!key) return null;
|
|
30
42
|
if (extractCache.has(key)) return extractCache.get(key) || null;
|