Files
insta-recipe/src/routes/share/+page.svelte
Giancarmine Salucci 7e4d82de8d feat(share): refactor page and enhance thumbnail extraction
- Extract 8 reusable components from monolithic share page
- Add LLM health indicator with 30s polling
- Implement stealth thumbnail extraction with 4-method cascade
- Integrate real-time thumbnail preview component
- Reduce share page from 306 to ~140 lines
- Add comprehensive outcome documentation

Components:
- UrlInputSection: URL input and extraction trigger
- ProgressIndicator: Loading state display
- ExtractedTextViewer: Collapsible text preview
- RecipeCard: Recipe display with Tandoor integration
- ErrorState: Error handling UI
- LogViewer: System logs with color coding
- LlmHealthIndicator: LLM status with polling
- ThumbnailPreview: Real-time thumbnail display

Thumbnail Methods:
1. Meta tag extraction (og:image, twitter:image)
2. Video poster attribute
3. Instagram embedded JSON data
4. Screenshot fallback

Stories Completed:
- Story 1: Component extraction and refactoring
- Story 2: LLM health status indicator
- Story 3: Enhanced stealth thumbnail extraction
- Story 4: Thumbnail preview integration

Closes: RefactorSharePageAndEnhanceThumbnails
2025-12-21 04:18:38 +01:00

201 lines
6.0 KiB
Svelte
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts">
import { page } from '$app/stores';
import type { ProgressEvent } from '$lib/server/extraction';
import UrlInputSection from './components/UrlInputSection.svelte';
import ProgressIndicator from './components/ProgressIndicator.svelte';
import ExtractedTextViewer from './components/ExtractedTextViewer.svelte';
import RecipeCard from './components/RecipeCard.svelte';
import ErrorState from './components/ErrorState.svelte';
import LogViewer from './components/LogViewer.svelte';
import LlmHealthIndicator from './components/LlmHealthIndicator.svelte';
import ThumbnailPreview from './components/ThumbnailPreview.svelte';
let status = $state('idle');
let logs = $state<string[]>([]);
let recipe = $state<any>(null);
let bodyText = $state<string>('');
let tandoorEnabled = $state(false);
let tandoorImporting = $state(false);
let tandoorError = $state<string | null>(null);
let currentMethod = $state<string>('');
let thumbnail = $state<string | null>(null);
let thumbnailStatus = $state<'idle' | 'extracting' | 'success' | 'error'>('idle');
// URL param parsing for Share Target
// Instagram typically shares text that contains the URL, so we might need to parse it out
let sharedText = $derived($page.url.searchParams.get('text') || '');
let sharedUrl = $derived($page.url.searchParams.get('url') || '');
function extractUrl(text: string) {
const match = text.match(/(https?:\/\/[^\s]+)/);
return match ? match[0] : null;
}
let targetUrl = $derived(sharedUrl || extractUrl(sharedText));
$effect.pre(() => {
loadTandoorConfig();
});
// Load Tandoor config on mount
async function loadTandoorConfig() {
try {
const res = await fetch('/api/tandoor-config');
const config = await res.json();
tandoorEnabled = config.enabled;
logs = [...logs, `Tandoor integration ${config.enabled ? 'enabled' : 'disabled'}`];
} catch (e) {
logs = [...logs, 'Failed to load Tandoor config'];
}
}
// Map method names to icons
function getMethodIcon(method?: string): string {
const icons: Record<string, string> = {
'embedded-json': '📦',
'dom-selector': '🎯',
'graphql-api': '🔌',
legacy: '📄'
};
return method ? icons[method] || '⚙️' : '⚙️';
}
async function process() {
if (!targetUrl) return;
status = 'extracting';
thumbnailStatus = 'extracting';
logs = [...logs, '🚀 Starting extraction from: ' + targetUrl];
currentMethod = '';
try {
const response = await fetch('/api/extract-stream', {
method: 'POST',
body: JSON.stringify({ url: targetUrl }),
headers: { 'Content-Type': 'application/json' }
});
if (!response.body) {
throw new Error('No response body');
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (!line.trim()) continue;
const eventMatch = line.match(/^event: (\w+)\ndata: (.+)$/s);
if (!eventMatch) continue;
const [, eventType, eventData] = eventMatch;
const event: ProgressEvent = JSON.parse(eventData);
// Update UI based on event type
if (event.type === 'method') {
currentMethod = event.method || '';
logs = [...logs, `${getMethodIcon(event.method)} ${event.message}`];
} else if (event.type === 'status') {
logs = [...logs, ` ${event.message}`];
} else if (event.type === 'retry') {
logs = [...logs, `🔄 ${event.message}`];
} else if (event.type === 'error') {
logs = [...logs, `❌ ${event.message}`];
} else if (event.type === 'thumbnail') {
thumbnail = event.data?.thumbnail || null;
thumbnailStatus = thumbnail ? 'success' : 'error';
logs = [...logs, `🎨 ${event.message}`];
} else if (eventType === 'complete' && event.data) {
recipe = event.data.recipe;
bodyText = event.data.recipe?.bodyText || '';
status = 'done';
logs = [...logs, `✅ ${event.message}`];
currentMethod = '';
}
}
}
if (status !== 'done') {
status = 'error';
if (thumbnailStatus === 'extracting') {
thumbnailStatus = 'error';
}
}
} catch (e) {
logs = [...logs, '❌ Network Error: ' + (e instanceof Error ? e.message : 'Unknown')];
status = 'error';
thumbnailStatus = 'error';
}
}
async function retry() {
recipe = null;
bodyText = '';
status = 'idle';
logs = [...logs, 'Retrying extraction...'];
await process();
}
async function importToTandoor() {
if (!recipe) return;
tandoorImporting = true;
tandoorError = null;
logs = [...logs, 'Importing recipe to Tandoor...'];
try {
const res = await fetch('/api/tandoor', {
method: 'POST',
body: JSON.stringify({ recipe }),
headers: { 'Content-Type': 'application/json' }
});
const data = await res.json();
if (data.success) {
logs = [...logs, `✓ Recipe imported successfully (ID: ${data.recipeId})`];
tandoorError = null;
} else {
logs = [...logs, `✗ Import failed: ${data.error}`];
tandoorError = data.error;
}
} catch (e) {
const errorMsg = e instanceof Error ? e.message : 'Unknown error';
logs = [...logs, `✗ Network error: ${errorMsg}`];
tandoorError = errorMsg;
} finally {
tandoorImporting = false;
}
}
</script>
<div class="p-8 max-w-lg mx-auto space-y-4">
<div class="flex items-center justify-between">
<h1 class="text-2xl font-bold">InstaChef PWA</h1>
<LlmHealthIndicator />
</div>
<UrlInputSection {targetUrl} {sharedText} {sharedUrl} {status} onProcess={process} />
<ProgressIndicator {status} />
<ThumbnailPreview {thumbnail} status={thumbnailStatus} />
<ExtractedTextViewer {bodyText} />
<RecipeCard
{recipe}
{tandoorEnabled}
{tandoorImporting}
{tandoorError}
onRetry={retry}
onImportToTandoor={importToTandoor}
/>
<ErrorState {status} {bodyText} onRetry={retry} />
<LogViewer {logs} {currentMethod} {status} />
</div>