Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 | <script> import { onMount } from "svelte"; import { getInfoEngineItems, getInfoEngineItem } from "../lib/api.js"; let loading = $state(true); let loadingDetail = $state(false); let requestError = $state(""); let items = $state([]); let total = $state(0); let selectedId = $state(null); let selectedItem = $state(null); async function loadItems() { loading = true; requestError = ""; try { const res = await getInfoEngineItems({ limit: 50, offset: 0 }); items = res.items || []; total = res.total || 0; if (items.length > 0) { await selectItem(items[0].id); } else { selectedId = null; selectedItem = null; } } catch (e) { requestError = e?.body?.detail || e?.message || "Failed to load items"; items = []; total = 0; selectedId = null; selectedItem = null; } finally { loading = false; } } async function selectItem(id) { selectedId = id; loadingDetail = true; requestError = ""; try { selectedItem = await getInfoEngineItem(id); } catch (e) { selectedItem = null; requestError = e?.body?.detail || e?.message || "Failed to load item"; } finally { loadingDetail = false; } } function formatDate(value) { if (!value) return "—"; try { return new Date(value).toLocaleString(); } catch { return value; } } onMount(loadItems); </script> <div class="space-y-6"> <div class="flex items-center justify-between"> <div> <h1 class="text-2xl font-bold text-surface-900 dark:text-white tracking-tight">Info Engine</h1> <p class="text-sm text-surface-400 mt-1">Context-aware stream of curated signals</p> </div> <button onclick={loadItems} disabled={loading} class="px-3 py-1.5 text-xs font-medium text-primary-600 bg-primary-50 hover:bg-primary-100 rounded-lg transition-colors disabled:opacity-50 dark:bg-primary-900/30 dark:text-primary-300"> {loading ? "Loading..." : "Refresh"} </button> </div> {#if requestError} <div class="bg-rose-50 dark:bg-rose-900/20 border border-rose-200 dark:border-rose-700 rounded-xl p-4 shadow-sm"> <p class="text-sm font-medium text-rose-700 dark:text-rose-300">{requestError}</p> </div> {/if} <div class="grid grid-cols-1 lg:grid-cols-3 gap-4"> <div class="lg:col-span-1 bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 shadow-sm"> <div class="px-4 py-3 border-b border-surface-200 dark:border-surface-700 text-xs text-surface-400"> Latest items ({total}) </div> {#if loading} <div class="p-4 space-y-2"> {#each Array(6) as _} <div class="h-12 rounded-lg bg-surface-100 dark:bg-surface-700 animate-pulse"></div> {/each} </div> {:else if items.length === 0} <div class="p-4 text-sm text-surface-400">No items yet.</div> {:else} <div class="max-h-[560px] overflow-y-auto p-2"> {#each items as item} <button onclick={() => selectItem(item.id)} class="w-full text-left px-3 py-2 rounded-lg mb-1 transition-colors {selectedId === item.id ? 'bg-primary-50 dark:bg-primary-900/30' : 'hover:bg-surface-100 dark:hover:bg-surface-700'}"> <p class="text-sm font-semibold text-surface-800 dark:text-surface-100 line-clamp-2">{item.title}</p> <p class="text-xs text-surface-400 mt-1">{item.source}</p> </button> {/each} </div> {/if} </div> <div class="lg:col-span-2 bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 p-5 shadow-sm"> {#if loadingDetail} <div class="space-y-3"> <div class="h-7 w-3/4 rounded bg-surface-100 dark:bg-surface-700 animate-pulse"></div> <div class="h-4 w-full rounded bg-surface-100 dark:bg-surface-700 animate-pulse"></div> <div class="h-4 w-5/6 rounded bg-surface-100 dark:bg-surface-700 animate-pulse"></div> <div class="h-24 w-full rounded bg-surface-100 dark:bg-surface-700 animate-pulse"></div> </div> {:else if selectedItem} <h2 class="text-xl font-bold text-surface-900 dark:text-white">{selectedItem.title}</h2> <div class="mt-2 text-xs text-surface-400 space-y-1"> <p>Source: {selectedItem.source}</p> <p>Published: {formatDate(selectedItem.published_at)}</p> <p>Collected: {formatDate(selectedItem.collected_at)}</p> </div> {#if selectedItem.tags?.length} <div class="mt-3 flex flex-wrap gap-2"> {#each selectedItem.tags as tag} <span class="px-2 py-0.5 text-xs rounded-full bg-surface-100 text-surface-600 dark:bg-surface-700 dark:text-surface-300">{tag}</span> {/each} </div> {/if} <p class="mt-4 text-sm text-surface-700 dark:text-surface-300 whitespace-pre-wrap">{selectedItem.summary || selectedItem.content_text || "No summary available."}</p> <a href={selectedItem.url} target="_blank" rel="noopener" class="inline-flex mt-4 px-3 py-1.5 text-xs font-medium rounded-lg bg-primary-600 text-white hover:bg-primary-700"> Open source </a> {:else} <p class="text-sm text-surface-400">Select an item to view details.</p> {/if} </div> </div> </div> |