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
This commit is contained in:
Giancarmine Salucci
2025-12-21 04:18:38 +01:00
parent 44823c365f
commit 7e4d82de8d
13 changed files with 1890 additions and 310 deletions

View File

@@ -1,306 +1,201 @@
<script lang="ts">
import { page } from '$app/stores';
import type { ProgressEvent } from '$lib/server/extraction';
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>('');
// 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') || '');
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';
function extractUrl(text: string) {
const match = text.match(/(https?:\/\/[^\s]+)/);
return match ? match[0] : null;
}
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');
let targetUrl = $derived(sharedUrl || extractUrl(sharedText));
// 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') || '');
$effect.pre(() => {
loadTandoorConfig();
});
function extractUrl(text: string) {
const match = text.match(/(https?:\/\/[^\s]+)/);
return match ? match[0] : null;
}
// 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'];
}
}
let targetUrl = $derived(sharedUrl || extractUrl(sharedText));
// 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] || '⚙️' : '⚙️';
}
$effect.pre(() => {
loadTandoorConfig();
});
async function process() {
if(!targetUrl) return;
status = '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' }
});
// 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'];
}
}
if (!response.body) {
throw new Error('No response body');
}
// 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] || '⚙️' : '⚙️';
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
async function process() {
if (!targetUrl) return;
status = 'extracting';
thumbnailStatus = 'extracting';
logs = [...logs, '🚀 Starting extraction from: ' + targetUrl];
currentMethod = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
try {
const response = await fetch('/api/extract-stream', {
method: 'POST',
body: JSON.stringify({ url: targetUrl }),
headers: { 'Content-Type': 'application/json' }
});
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n\n');
buffer = lines.pop() || '';
if (!response.body) {
throw new Error('No response body');
}
for (const line of lines) {
if (!line.trim()) continue;
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
const eventMatch = line.match(/^event: (\w+)\ndata: (.+)$/s);
if (!eventMatch) continue;
while (true) {
const { done, value } = await reader.read();
const [, eventType, eventData] = eventMatch;
const event: ProgressEvent = JSON.parse(eventData);
if (done) break;
// 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 (eventType === 'complete' && event.data) {
recipe = event.data.recipe;
bodyText = event.data.recipe?.bodyText || '';
status = 'done';
logs = [...logs, `✅ ${event.message}`];
currentMethod = '';
}
}
}
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n\n');
buffer = lines.pop() || '';
if (status !== 'done') {
status = 'error';
}
} catch(e) {
logs = [...logs, '❌ Network Error: ' + (e instanceof Error ? e.message : 'Unknown')];
status = 'error';
}
}
for (const line of lines) {
if (!line.trim()) continue;
async function retry() {
recipe = null;
bodyText = '';
status = 'idle';
logs = [...logs, 'Retrying extraction...'];
await process();
}
const eventMatch = line.match(/^event: (\w+)\ndata: (.+)$/s);
if (!eventMatch) continue;
async function importToTandoor() {
if (!recipe) return;
tandoorImporting = true;
tandoorError = null;
logs = [...logs, 'Importing recipe to Tandoor...'];
const [, eventType, eventData] = eventMatch;
const event: ProgressEvent = JSON.parse(eventData);
try {
const res = await fetch('/api/tandoor', {
method: 'POST',
body: JSON.stringify({ recipe }),
headers: { 'Content-Type': 'application/json' }
});
// 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 = '';
}
}
}
const data = await res.json();
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';
}
}
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;
}
}
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>
{#snippet urlInputSection()}
{#if targetUrl}
<div class="bg-gray-100 p-2 rounded break-all text-sm border">{targetUrl}</div>
{#if status === 'idle'}
<button onclick={process} class="bg-blue-600 text-white px-4 py-2 rounded shadow hover:bg-blue-700 w-full">
Extract Recipe
</button>
{/if}
{:else}
<p class="text-gray-500">No URL detected. Open this app via Instagram Share Menu.</p>
<div class="text-xs text-gray-400">Debug: Text={sharedText} URL={sharedUrl}</div>
{/if}
{/snippet}
{#snippet progressIndicator()}
{#if status === 'extracting'}
<div class="animate-pulse text-blue-600">Extracting data...</div>
{/if}
{/snippet}
{#snippet extractedTextViewer()}
{#if bodyText}
<details class="border rounded p-2 bg-white text-sm">
<summary class="cursor-pointer font-semibold">📝 View Extracted Text</summary>
<div class="mt-2 pt-2 border-t whitespace-pre-wrap break-word max-h-48 overflow-y-auto text-xs">
{bodyText}
</div>
</details>
{/if}
{/snippet}
{#snippet recipeCard()}
{#if recipe}
<div class="border rounded p-4 bg-green-50 space-y-2">
<h2 class="font-bold text-xl">{recipe.name}</h2>
<p class="text-sm">{recipe.description}</p>
<p class="text-muted"><strong>Servings:</strong> {recipe.servings}</p>
<h3 class="font-bold mt-2">Ingredients</h3>
<ul class="list-disc pl-5 text-sm">
{#each recipe.ingredients as ing}
<li>{ing.amount} {ing.unit} {ing.item}</li>
{/each}
</ul>
<h3 class="font-bold mt-2">Steps</h3>
<ol class="list-decimal pl-5 text-sm">
{#each recipe.steps as step}
<li>{step}</li>
{/each}
</ol>
{#if tandoorEnabled}
<div class="mt-4 pt-4 border-t space-y-2">
<h3 class="font-bold">Tandoor Integration</h3>
{#if tandoorError}
<div class="bg-red-100 text-red-800 p-2 rounded text-sm">
Error: {tandoorError}
</div>
{/if}
<button
onclick={importToTandoor}
disabled={tandoorImporting}
class="bg-orange-600 text-white px-4 py-2 rounded shadow hover:bg-orange-700 w-full disabled:bg-gray-400 disabled:cursor-not-allowed"
>
{tandoorImporting ? 'Importing...' : 'Import to Tandoor'}
</button>
</div>
{/if}
<button
onclick={retry}
class="bg-blue-500 text-white px-4 py-2 rounded shadow hover:bg-blue-600 w-full mt-2"
>
🔄 Retry Extraction
</button>
</div>
{/if}
{/snippet}
{#snippet errorState()}
{#if status === 'error' && bodyText}
<div class="border rounded p-4 bg-yellow-50 space-y-2">
<h3 class="font-bold text-lg">Extraction Error - Raw Text Available</h3>
<details class="border rounded p-2 bg-white text-sm">
<summary class="cursor-pointer font-semibold">📝 View Extracted Text</summary>
<div class="mt-2 pt-2 border-t whitespace-pre-wrap break-word max-h-48 overflow-y-auto text-xs">
{bodyText}
</div>
</details>
<button
onclick={retry}
class="bg-blue-500 text-white px-4 py-2 rounded shadow hover:bg-blue-600 w-full mt-2"
>
🔄 Retry Extraction
</button>
</div>
{/if}
{/snippet}
{#snippet logViewer()}
<div class="bg-slate-900 text-slate-100 p-4 rounded-lg shadow-lg min-h-[120px] max-h-[400px] overflow-y-auto">
<div class="flex items-center justify-between mb-3 pb-2 border-b border-slate-700">
<div class="text-sm font-semibold opacity-70">System Logs</div>
{#if currentMethod}
<div class="text-xs bg-blue-600 px-2 py-1 rounded flex items-center gap-1">
<span class="animate-pulse"></span>
<span>Current: {currentMethod}</span>
</div>
{/if}
</div>
<div class="space-y-1 font-mono text-xs">
{#each logs as log}
<div class="flex items-start gap-2 py-1 {
log.includes('✅') ? 'text-green-400' :
log.includes('❌') ? 'text-red-400' :
log.includes('🔄') ? 'text-yellow-400' :
log.includes('📦') || log.includes('🎯') || log.includes('🔌') || log.includes('📄') ? 'text-blue-300' :
'text-slate-300'
}">
<span class="opacity-50">&gt;</span>
<span class="flex-1">{log}</span>
</div>
{/each}
{#if status === 'extracting'}
<div class="flex items-center gap-2 py-1 text-blue-400 animate-pulse">
<span class="opacity-50">&gt;</span>
<span>Processing...</span>
</div>
{/if}
</div>
</div>
{/snippet}
<div class="p-8 max-w-lg mx-auto space-y-4">
<h1 class="text-2xl font-bold">InstaChef PWA</h1>
{@render urlInputSection()}
{@render progressIndicator()}
{@render extractedTextViewer()}
{@render recipeCard()}
{@render errorState()}
{@render logViewer()}
<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>

View File

@@ -0,0 +1,27 @@
<script lang="ts">
let { status = 'idle', bodyText = '', onRetry } = $props<{
status: string;
bodyText: string;
onRetry: () => void;
}>();
</script>
{#if status === 'error' && bodyText}
<div class="border rounded p-4 bg-yellow-50 space-y-2">
<h3 class="font-bold text-lg">Extraction Error - Raw Text Available</h3>
<details class="border rounded p-2 bg-white text-sm">
<summary class="cursor-pointer font-semibold">📝 View Extracted Text</summary>
<div
class="mt-2 pt-2 border-t whitespace-pre-wrap break-word max-h-48 overflow-y-auto text-xs"
>
{bodyText}
</div>
</details>
<button
onclick={onRetry}
class="bg-blue-500 text-white px-4 py-2 rounded shadow hover:bg-blue-600 w-full mt-2"
>
🔄 Retry Extraction
</button>
</div>
{/if}

View File

@@ -0,0 +1,14 @@
<script lang="ts">
let { bodyText = '' } = $props<{
bodyText: string;
}>();
</script>
{#if bodyText}
<details class="border rounded p-2 bg-white text-sm">
<summary class="cursor-pointer font-semibold">📝 View Extracted Text</summary>
<div class="mt-2 pt-2 border-t whitespace-pre-wrap break-word max-h-48 overflow-y-auto text-xs">
{bodyText}
</div>
</details>
{/if}

View File

@@ -0,0 +1,58 @@
<script lang="ts">
interface HealthState {
status: 'checking' | 'healthy' | 'unhealthy' | 'error';
message: string;
lastChecked: Date | null;
}
let { pollInterval = 30000 } = $props<{
pollInterval?: number;
}>();
let health = $state<HealthState>({
status: 'checking',
message: '',
lastChecked: null
});
async function checkHealth() {
try {
const res = await fetch('/api/llm-health');
const data = await res.json();
health = {
status: data.status === 'healthy' ? 'healthy' : 'unhealthy',
message: data.message,
lastChecked: new Date()
};
} catch (e) {
health = {
status: 'error',
message: e instanceof Error ? e.message : 'Network error',
lastChecked: new Date()
};
}
}
$effect(() => {
checkHealth(); // Initial check
const interval = setInterval(checkHealth, pollInterval);
return () => clearInterval(interval);
});
</script>
<div class="flex items-center gap-2 text-sm">
<div class="flex items-center gap-1">
{#if health.status === 'checking'}
🟡 <span>Checking LLM...</span>
{:else if health.status === 'healthy'}
🟢 <span class="text-green-600">LLM Ready</span>
{:else if health.status === 'unhealthy'}
🔴 <span class="text-red-600">LLM Unavailable</span>
{:else}
🔴 <span class="text-red-600">LLM Error</span>
{/if}
</div>
<div class="text-xs text-gray-500" title={health.message}>
{health.lastChecked ? `Last: ${health.lastChecked.toLocaleTimeString()}` : ''}
</div>
</div>

View File

@@ -0,0 +1,48 @@
<script lang="ts">
let { logs = [], currentMethod = '', status = 'idle' } = $props<{
logs: string[];
currentMethod: string;
status: string;
}>();
</script>
<div
class="bg-slate-900 text-slate-100 p-4 rounded-lg shadow-lg min-h-[120px] max-h-[400px] overflow-y-auto"
>
<div class="flex items-center justify-between mb-3 pb-2 border-b border-slate-700">
<div class="text-sm font-semibold opacity-70">System Logs</div>
{#if currentMethod}
<div class="text-xs bg-blue-600 px-2 py-1 rounded flex items-center gap-1">
<span class="animate-pulse"></span>
<span>Current: {currentMethod}</span>
</div>
{/if}
</div>
<div class="space-y-1 font-mono text-xs">
{#each logs as log}
<div
class="flex items-start gap-2 py-1 {log.includes('✅')
? 'text-green-400'
: log.includes('❌')
? 'text-red-400'
: log.includes('🔄')
? 'text-yellow-400'
: log.includes('📦') ||
log.includes('🎯') ||
log.includes('🔌') ||
log.includes('📄')
? 'text-blue-300'
: 'text-slate-300'}"
>
<span class="opacity-50">&gt;</span>
<span class="flex-1">{log}</span>
</div>
{/each}
{#if status === 'extracting'}
<div class="flex items-center gap-2 py-1 text-blue-400 animate-pulse">
<span class="opacity-50">&gt;</span>
<span>Processing...</span>
</div>
{/if}
</div>
</div>

View File

@@ -0,0 +1,9 @@
<script lang="ts">
let { status = 'idle' } = $props<{
status: string;
}>();
</script>
{#if status === 'extracting'}
<div class="animate-pulse text-blue-600">Extracting data...</div>
{/if}

View File

@@ -0,0 +1,72 @@
<script lang="ts">
interface Recipe {
name: string;
description: string;
servings: number;
ingredients: Array<{ amount: string; unit: string; item: string }>;
steps: string[];
}
let {
recipe = null,
tandoorEnabled = false,
tandoorImporting = false,
tandoorError = null,
onRetry,
onImportToTandoor
} = $props<{
recipe: Recipe | null;
tandoorEnabled: boolean;
tandoorImporting: boolean;
tandoorError: string | null;
onRetry: () => void;
onImportToTandoor: () => void;
}>();
</script>
{#if recipe}
<div class="border rounded p-4 bg-green-50 space-y-2">
<h2 class="font-bold text-xl">{recipe.name}</h2>
<p class="text-sm">{recipe.description}</p>
<p class="text-muted"><strong>Servings:</strong> {recipe.servings}</p>
<h3 class="font-bold mt-2">Ingredients</h3>
<ul class="list-disc pl-5 text-sm">
{#each recipe.ingredients as ing}
<li>{ing.amount} {ing.unit} {ing.item}</li>
{/each}
</ul>
<h3 class="font-bold mt-2">Steps</h3>
<ol class="list-decimal pl-5 text-sm">
{#each recipe.steps as step}
<li>{step}</li>
{/each}
</ol>
{#if tandoorEnabled}
<div class="mt-4 pt-4 border-t space-y-2">
<h3 class="font-bold">Tandoor Integration</h3>
{#if tandoorError}
<div class="bg-red-100 text-red-800 p-2 rounded text-sm">
Error: {tandoorError}
</div>
{/if}
<button
onclick={onImportToTandoor}
disabled={tandoorImporting}
class="bg-orange-600 text-white px-4 py-2 rounded shadow hover:bg-orange-700 w-full disabled:bg-gray-400 disabled:cursor-not-allowed"
>
{tandoorImporting ? 'Importing...' : 'Import to Tandoor'}
</button>
</div>
{/if}
<button
onclick={onRetry}
class="bg-blue-500 text-white px-4 py-2 rounded shadow hover:bg-blue-600 w-full mt-2"
>
🔄 Retry Extraction
</button>
</div>
{/if}

View File

@@ -0,0 +1,32 @@
<script lang="ts">
let { thumbnail = null, status = 'idle' } = $props<{
thumbnail: string | null;
status: 'idle' | 'extracting' | 'success' | 'error';
}>();
</script>
{#if status === 'extracting'}
<div class="border rounded-lg p-4 bg-gray-50">
<div class="flex items-center gap-3 mb-2">
<div class="animate-spin text-blue-600">🎨</div>
<span class="text-sm font-medium text-gray-700">Extracting thumbnail...</span>
</div>
<!-- Loading skeleton -->
<div class="w-full aspect-square bg-gray-200 animate-pulse rounded"></div>
</div>
{:else if status === 'success' && thumbnail}
<div class="border rounded-lg p-4 bg-white shadow-sm">
<div class="flex items-center gap-2 mb-2">
<span class="text-green-600"></span>
<span class="text-sm font-medium text-gray-700">Thumbnail extracted</span>
</div>
<img src={thumbnail} alt="Post thumbnail" class="w-full aspect-square object-cover rounded" />
</div>
{:else if status === 'error'}
<div class="border border-red-200 rounded-lg p-4 bg-red-50">
<div class="flex items-center gap-2">
<span class="text-red-600"></span>
<span class="text-sm font-medium text-red-700">Thumbnail extraction failed</span>
</div>
</div>
{/if}

View File

@@ -0,0 +1,25 @@
<script lang="ts">
let { targetUrl = null, sharedText = '', sharedUrl = '', status = 'idle', onProcess } = $props<{
targetUrl: string | null;
sharedText: string;
sharedUrl: string;
status: string;
onProcess: () => void;
}>();
</script>
{#if targetUrl}
<div class="bg-gray-100 p-2 rounded break-all text-sm border">{targetUrl}</div>
{#if status === 'idle'}
<button
onclick={onProcess}
class="bg-blue-600 text-white px-4 py-2 rounded shadow hover:bg-blue-700 w-full"
>
Extract Recipe
</button>
{/if}
{:else}
<p class="text-gray-500">No URL detected. Open this app via Instagram Share Menu.</p>
<div class="text-xs text-gray-400">Debug: Text={sharedText} URL={sharedUrl}</div>
{/if}