feat(app): add design system viewer, nostr feed, stop generation, and content refactor
- Design system browser with grid/detail views for tokens and components - Nostr feed tab with note/article/zap filtering and relay status - Stop generation button to abort AI streaming mid-response - Paste & extract content without sending to AI - Refactor useContentPanel into contentExtraction.ts and contentFiltering.ts - Banner fallback composable for 3-stage image loading - Wikipedia and Google Books as fallback image sources - Loading skeletons with variant-specific shapes - Mobile UX: auto-switch to content, back button, detail flow - Project grid with breadcrumb nav and inline creation - Filesystem Vite plugin for local project browsing - Magazine text cleanup and song grid polish Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
e8fc54cade
commit
00bdc055ba
@@ -207,6 +207,15 @@ export async function handleImgError(
|
||||
}
|
||||
}
|
||||
|
||||
if (img.dataset.fallback !== 'wiki') {
|
||||
const wiki = await fetchWikipediaImage(title, 'film')
|
||||
if (wiki) {
|
||||
img.dataset.fallback = 'wiki'
|
||||
img.src = wiki
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
img.dataset.fallback = 'done'
|
||||
img.src = generatePosterFallback(title, year)
|
||||
}
|
||||
@@ -364,3 +373,132 @@ export async function fetchMusicCover(
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Wikipedia image source (free, no key)
|
||||
// ---------------------------------------------------------------------------
|
||||
const wikiImageCache = new Map<string, string | null>()
|
||||
|
||||
/** Fetch an image from Wikipedia REST API. Free, no API key needed. */
|
||||
export async function fetchWikipediaImage(
|
||||
title: string,
|
||||
disambiguator?: string,
|
||||
): Promise<string | null> {
|
||||
const key = `${title.toLowerCase().trim()}|${(disambiguator ?? '').toLowerCase()}`
|
||||
if (wikiImageCache.has(key)) return wikiImageCache.get(key) ?? null
|
||||
|
||||
const tryTitle = async (t: string): Promise<string | null> => {
|
||||
try {
|
||||
const encoded = encodeURIComponent(t.trim().replace(/\s+/g, '_'))
|
||||
const res = await fetch(`https://en.wikipedia.org/api/rest_v1/page/summary/${encoded}`)
|
||||
if (!res.ok) return null
|
||||
const data = (await res.json()) as {
|
||||
thumbnail?: { source?: string }
|
||||
originalimage?: { source?: string }
|
||||
}
|
||||
return data.originalimage?.source ?? data.thumbnail?.source ?? null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Try exact title first
|
||||
let url = await tryTitle(title)
|
||||
|
||||
// Try with disambiguator suffix if no result
|
||||
if (!url && disambiguator) {
|
||||
url = await tryTitle(`${title} (${disambiguator})`)
|
||||
}
|
||||
|
||||
wikiImageCache.set(key, url)
|
||||
return url
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Google Books image source (free, no key)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Fetch book cover from Google Books API. Free, no API key needed. */
|
||||
export async function fetchGoogleBooksImage(
|
||||
title: string,
|
||||
author?: string,
|
||||
): Promise<string | null> {
|
||||
const key = bookCacheKey(title, author ?? '')
|
||||
const gKey = `gbooks:${key}`
|
||||
if (wikiImageCache.has(gKey)) return wikiImageCache.get(gKey) ?? null
|
||||
|
||||
try {
|
||||
const q = author ? `intitle:${title}+inauthor:${author}` : `intitle:${title}`
|
||||
const res = await fetch(
|
||||
`https://www.googleapis.com/books/v1/volumes?q=${encodeURIComponent(q)}&maxResults=1`,
|
||||
)
|
||||
if (!res.ok) return null
|
||||
const data = (await res.json()) as {
|
||||
items?: { volumeInfo?: { imageLinks?: { thumbnail?: string; smallThumbnail?: string } } }[]
|
||||
}
|
||||
const links = data.items?.[0]?.volumeInfo?.imageLinks
|
||||
let url = links?.thumbnail ?? links?.smallThumbnail ?? null
|
||||
// Google Books returns http URLs and small sizes — upgrade
|
||||
if (url) {
|
||||
url = url.replace(/^http:/, 'https:').replace(/&edge=curl/g, '')
|
||||
// Request larger zoom
|
||||
if (!url.includes('zoom=')) url += '&zoom=2'
|
||||
}
|
||||
wikiImageCache.set(gKey, url)
|
||||
return url
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Chained fetchers — try multiple sources in order
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Film image: TMDB → Wikipedia */
|
||||
export async function fetchFilmImage(
|
||||
title: string,
|
||||
year?: number,
|
||||
): Promise<{ posterUrl: string | null; backdropUrl: string | null }> {
|
||||
// Try TMDB first
|
||||
const tmdb = await fetchTmdbPoster(title, year)
|
||||
if (tmdb.posterUrl || tmdb.backdropUrl) return tmdb
|
||||
|
||||
// Fall back to Wikipedia
|
||||
const wiki = await fetchWikipediaImage(title, 'film')
|
||||
if (wiki) return { posterUrl: wiki, backdropUrl: null }
|
||||
|
||||
return { posterUrl: null, backdropUrl: null }
|
||||
}
|
||||
|
||||
/** TV series image: TMDB → Wikipedia */
|
||||
export async function fetchTVImage(
|
||||
title: string,
|
||||
year?: number,
|
||||
): Promise<{ posterUrl: string | null; backdropUrl: string | null }> {
|
||||
const tmdb = await fetchTmdbTVPoster(title, year)
|
||||
if (tmdb.posterUrl || tmdb.backdropUrl) return tmdb
|
||||
|
||||
const wiki = await fetchWikipediaImage(title, 'TV series')
|
||||
if (wiki) return { posterUrl: wiki, backdropUrl: null }
|
||||
|
||||
return { posterUrl: null, backdropUrl: null }
|
||||
}
|
||||
|
||||
/** Book image: Open Library → Google Books → Wikipedia */
|
||||
export async function fetchBookImage(
|
||||
title: string,
|
||||
author?: string,
|
||||
): Promise<string | null> {
|
||||
// Try Open Library first
|
||||
const ol = await fetchBookCover(title, author ?? '')
|
||||
if (ol) return ol
|
||||
|
||||
// Try Google Books
|
||||
const gb = await fetchGoogleBooksImage(title, author)
|
||||
if (gb) return gb
|
||||
|
||||
// Try Wikipedia
|
||||
const wiki = await fetchWikipediaImage(title, 'novel')
|
||||
return wiki
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user