app
Chip Fetcher (cf)
Public Made by Adomby adom
Crawls the web on your behalf to gather everything about the chips you care about: datasheets, symbols, footprints, 3D models, stock and pricing, straight from the manufacturer first, into your EDA tool. Say "cf" to your AI.
← Commit history
1.3.10: newbie panel (say strip + LEDs + getting started) is one closable block with a Run-a-demo-fetch button; empty libraries auto-seed 3 sample wiki bundles; detail pane auto-selects a sample; cf sweep (README, page title, sites pack); sites skills get Parent-skill lines + rich triggers; dashboard-flow playbook is pup-always
7 files changed
+179−20
Cargo.lock+1−1@@ -4,7 +4,7 @@ version = 4 [[package]] name = "adom-chip-fetcher"-version = "1.3.9"+version = "1.3.10" dependencies = [ "anyhow", "clap",
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-chip-fetcher"-version = "1.3.9"+version = "1.3.10" edition = "2021" description = "Crawls the web on your behalf to gather everything about the chips you care about (datasheet, symbol, footprint, 3D, stock) into one library, by driving Chrome via pup."
src/cli.rs+110@@ -196,6 +196,10 @@ enum Cmd { #[arg(long, default_value_t = 8786)] port: u16, },+ /// Seed the sample parts (published Adom Wiki bundles) into an empty library.+ SeedSamples,+ /// The newbie panel's demo: re-fetch the demo part from the wiki with live heartbeats.+ DemoFetch, /// The phrases users say to their AI to drive this app (from dockbar.json). Say, /// Stage a phrase into this app's own AI thread (copy fallback).@@ -558,6 +562,12 @@ pub fn run() -> Result<()> { Cmd::Logins { import } => cmd_logins(import), Cmd::Health { port } => health_cmd(port), Cmd::Doctor { fix, port } => doctor_cmd(fix, port),+ Cmd::SeedSamples => {+ let n = seed_samples(true);+ println!("OK: {n} sample(s) seeded");+ Ok(())+ }+ Cmd::DemoFetch => demo_fetch_cmd(), Cmd::Say => { for p in crate::dock::say_phrases() { println!("{p}");@@ -1921,6 +1931,7 @@ fn auto_render_thumbnails(mpn: &str) { /// if one exists. Falls through (prints a clear message) when there's no match, /// so it doubles as the fall-through test without touching the vendor ladder. fn wiki_fetch_cmd(mpn: &str) -> Result<()> {+ let existed = library::root().join(mpn).exists(); library::ensure_dir(mpn)?; match crate::sources::wiki::try_fetch(mpn)? { Some(pull) => {@@ -1932,6 +1943,10 @@ fn wiki_fetch_cmd(mpn: &str) -> Result<()> { println!("\nReused from the Adom Wiki, no external fetch needed."); } None => {+ // Do not leave an empty stub behind a lookup that pulled nothing.+ if !existed {+ let _ = std::fs::remove_dir(library::root().join(mpn));+ } println!("Adom Wiki: no published bundle for {mpn}."); hints::print_hint(&format!("not on the wiki, run `adom-chip-fetcher fetch {mpn}` to walk the vendor ladder (manufacturer → SnapMagic → …).")); }@@ -1939,6 +1954,101 @@ fn wiki_fetch_cmd(mpn: &str) -> Result<()> { Ok(()) } +/// The parts a fresh library is seeded with: published Adom Wiki component+/// bundles (symbol + footprint + 3D + datasheet), pulled with ZERO scraping+/// and no desktop, so a brand-new user sees real cards, not an empty grid.+/// Deliberately varied: a BLE MCU, an IMU, and an 0402 passive.+pub const SAMPLE_MPNS: &[&str] = &["nRF52840-CKAA", "BMI270", "0402B102J500CT"];+/// The part the newbie panel's Demo button fetches live (kept OUT of the+/// seed list so the demo always visibly adds a card).+pub const DEMO_MPN: &str = "0603WAF100JT5E";++fn mark_sample(mpn: &str) {+ let p = library::root().join(mpn).join("info.json");+ let mut v: serde_json::Value = std::fs::read_to_string(&p)+ .ok()+ .and_then(|s| serde_json::from_str(&s).ok())+ .unwrap_or_else(|| serde_json::json!({}));+ if let Some(o) = v.as_object_mut() {+ o.insert("sample".into(), serde_json::json!(true));+ o.insert("sample_note".into(), serde_json::json!("Seeded from the Adom Wiki as a sample; delete the folder any time."));+ }+ let _ = std::fs::write(&p, serde_json::to_string_pretty(&v).unwrap_or_default());+}++/// Seed the sample parts (skipping any already present). Quiet per-part+/// failures: a wiki hiccup must never break first run.+pub fn seed_samples(announce: bool) -> usize {+ let mut added = 0;+ for mpn in SAMPLE_MPNS {+ if library::root().join(mpn).exists() {+ continue;+ }+ let _ = hints::heartbeat_quiet(mpn, "seeding sample from the Adom Wiki");+ match crate::sources::wiki::try_fetch(mpn) {+ Ok(Some(_)) => {+ mark_sample(mpn);+ let _ = thumbnails::generate_for(mpn, true);+ hints::heartbeat_quiet(mpn, "imported (Adom Wiki sample)");+ if announce {+ println!("OK: seeded sample {mpn} from the Adom Wiki");+ }+ added += 1;+ }+ _ => {+ let _ = std::fs::remove_dir_all(library::root().join(mpn));+ if announce {+ println!("WARN: could not seed {mpn} (wiki unreachable?); skipped");+ }+ }+ }+ }+ added+}++/// The Demo button: re-fetch the demo part from the wiki with live+/// heartbeats, so the user watches a card arrive end to end.+fn demo_fetch_cmd() -> Result<()> {+ let mpn = DEMO_MPN;+ // NEVER delete real user data: the demo only clears its own previous run+ // (info.json carries sample:true). A genuinely fetched part with the same+ // MPN is left alone and the demo simply re-imports over nothing.+ let dir = library::root().join(mpn);+ if dir.exists() {+ let is_sample = std::fs::read_to_string(dir.join("info.json"))+ .ok()+ .and_then(|t| serde_json::from_str::<serde_json::Value>(&t).ok())+ .and_then(|v| v.get("sample").and_then(|b| b.as_bool()))+ .unwrap_or(false);+ if is_sample {+ let _ = std::fs::remove_dir_all(&dir);+ } else {+ println!("OK: {mpn} is already in your library from a real fetch; the demo will not touch it. Select its card to explore, or delete the folder to re-run the demo.");+ let _ = hints::heartbeat(mpn, "imported");+ return Ok(());+ }+ }+ println!("Demo: fetching {mpn} the same way a real cf lookup ends, from a published Adom Wiki bundle (no desktop needed).");+ let _ = hints::heartbeat(mpn, "starting demo fetch");+ library::ensure_dir(mpn)?;+ let _ = hints::heartbeat(mpn, "probing the Adom Wiki for a published bundle");+ match crate::sources::wiki::try_fetch(mpn)? {+ Some(pull) => {+ let _ = hints::heartbeat(mpn, "downloading the bundle");+ for f in &pull.files_added {+ println!(" + {f}");+ }+ mark_sample(mpn);+ let _ = hints::heartbeat(mpn, "importing");+ let _ = thumbnails::generate_for(mpn, true);+ let _ = hints::heartbeat(mpn, "imported");+ println!("OK: {mpn} is on the dashboard: datasheet, symbol, footprint and 3D. A real lookup does the same via the manufacturer's site when the wiki has no bundle.");+ Ok(())+ }+ None => anyhow::bail!("the wiki bundle for {mpn} is unavailable right now; try again in a minute"),+ }+}+ fn fetch(mpn: &str) -> Result<()> { library::ensure_dir(mpn)?; // The whole lookup, start to finish, shows an indeterminate taskbar bar on
src/jobs.rs+1@@ -40,6 +40,7 @@ pub fn command_for(action: &str, body: &Value) -> Option<(String, Vec<String>)> "install_tools" => sh("adom-chip-fetcher doctor --fix 2>&1"), "install_skill" => sh("adom-chip-fetcher install 2>&1"), "refresh_status" => sh("adom-chip-fetcher doctor 2>&1; true"),+ "demo_fetch" => sh("adom-chip-fetcher demo-fetch 2>&1"), "import_logins" => { let hosts = body .get("hostFilter")
src/serve.rs+15@@ -78,6 +78,21 @@ pub fn run(port: u16, print_url: bool, detached_child: bool) -> Result<()> { } dock::claim_dock_webview(port); LIBRARY_VERSION.lock().map(|mut v| *v = library_signature()).ok();+ // First run: an empty library teaches nothing. Seed the sample bundles+ // from the Adom Wiki in the background (no scraping, no desktop) so the+ // first thing a new user sees is real cards. Gated by `seed_samples`.+ if crate::settings::get_bool("seed_samples") {+ let empty = std::fs::read_dir(crate::library::root()).map(|rd| rd.flatten().filter(|e| e.path().is_dir()).count() == 0).unwrap_or(true);+ if empty {+ std::thread::spawn(|| {+ let n = crate::cli::seed_samples(false);+ if n > 0 {+ crate::dock::toast_push(&format!("Seeded {n} sample parts from the Adom Wiki so you can explore. Delete them any time; the Demo button fetches one live."), "info");+ crate::dock::console_push("info", &format!("seeded {n} sample parts from the Adom Wiki"), "serve");+ }+ });+ }+ } for mut request in server.incoming_requests() { let raw_url = request.url().to_string();
src/settings.rs+2−1@@ -21,7 +21,8 @@ pub const SCHEMA: &[(&str, &str, &[&str], &str)] = &[ ("auto_zoom", "false", &[], "Auto-zoom the active card"), ("auto_select_active", "true", &[], "Follow the active lookup"), ("show_stock_badges", "true", &[], "Stock and lifecycle badges on cards"),- ("show_welcome", "true", &[], "Getting-started card on the dashboard"),+ ("show_welcome", "true", &[], "Newbie panel on the dashboard"),+ ("seed_samples", "true", &[], "Seed sample parts into an empty library"), ("activity_stale_seconds", "90", &[], "Heartbeat considered stale after (seconds)"), ];
src/ui/index.html+49−17@@ -136,6 +136,9 @@ header{flex:0 0 44px;height:44px;display:flex;align-items:center;gap:12px;paddin .led.warn{background:var(--yellow)} .led.fail{background:var(--red)} +/* ---- The newbie panel: say strip + LEDs + getting started, one closable block ---- */+.newbie{position:relative}+.newbie-close{position:absolute;top:10px;right:10px;z-index:2} /* ---- Welcome (first run) ---- */ .welcome{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:18px 20px;margin-bottom:16px;max-width:760px} .welcome h2{font:600 18px var(--brand-heading);color:var(--text)}@@ -532,7 +535,7 @@ dialog.settings{width:min(680px,92vw);overflow:hidden} <button class="iconbtn" id="btn-sources" aria-pressed="false" aria-label="Sourcing order" data-tooltip="Sourcing order: the exact priority chip fetcher uses to search the internet for electronics data. Manufacturer site first, then the Adom data tools (parts-search, Mouser, DigiKey, JLCPCB), then the manual CAD-crawl skills (SnapMagic, CSE, Ultra Librarian), with LCSC last. Click again to return to the cards."><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6h16M4 12h16M4 18h10"/></svg></button> <button class="iconbtn" id="btn-arch" aria-label="Architecture map" data-tooltip="Architecture map: every tool chip fetcher depends on. Local CLIs that run in your container, native Rust libraries, remote service containers called over HTTPS, and the sub-apps you may also need to modify. Open this before editing chip fetcher."><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M9 3L3 6v15l6-3 6 3 6-3V3l-6 3-6-3z"/><path d="M9 3v15M15 6v15"/></svg></button> <button class="iconbtn" id="btn-reveal-lib" aria-label="Library folder" data-tooltip="Reveal the chip fetcher library folder in the VS Code Explorer sidebar. Opens ~/project/chip-fetcher/library/ so you can see every chip directory."><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7.5a2 2 0 0 1 2-2h4l2 2.5h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7.5z"/></svg></button>- <button class="iconbtn" id="btn-help" aria-pressed="false" aria-label="Getting started" data-tooltip="Show the getting-started card: the three steps, the example cf phrase, and where the component lights live."><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M9.5 9a2.5 2.5 0 1 1 3.4 2.33c-.7.28-1.4.84-1.4 1.67v.5"/><path d="M12 17h.01"/></svg></button>+ <button class="iconbtn" id="btn-help" aria-pressed="false" aria-label="Getting started" data-tooltip="Show or hide the newbie panel: the say-it phrases, the component lights, and getting started with its demo fetch."><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M9.5 9a2.5 2.5 0 1 1 3.4 2.33c-.7.28-1.4.84-1.4 1.67v.5"/><path d="M12 17h.01"/></svg></button> <button class="iconbtn" id="btn-settings" aria-label="Settings" data-tooltip="Settings: components and their install buttons, the taskbar progress mode, dashboard behaviour, and vendor logins."><!-- the shared Adom cog (assets/icons/settings-cog.svg), inlined so it follows currentColor --><svg viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M12 15.5A3.5 3.5 0 0 1 8.5 12A3.5 3.5 0 0 1 12 8.5a3.5 3.5 0 0 1 3.5 3.5a3.5 3.5 0 0 1-3.5 3.5m7.43-2.53c.04-.32.07-.64.07-.97s-.03-.66-.07-1l2.11-1.63c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.31-.61-.22l-2.49 1c-.52-.39-1.06-.73-1.69-.98l-.37-2.65A.506.506 0 0 0 14 2h-4c-.25 0-.46.18-.5.42l-.37 2.65c-.63.25-1.17.59-1.69.98l-2.49-1c-.22-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64L4.57 11c-.04.34-.07.67-.07 1s.03.65.07.97l-2.11 1.66c-.19.15-.25.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1.01c.52.4 1.06.74 1.69.99l.37 2.65c.04.24.25.42.5.42h4c.25 0 .46-.18.5-.42l.37-2.65c.63-.26 1.17-.59 1.69-.99l2.49 1.01c.22.08.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64z"/></svg></button> </div> <div class="hover-popover" id="app-info" role="dialog" aria-label="App information">@@ -563,6 +566,8 @@ dialog.settings{width:min(680px,92vw);overflow:hidden} <div class="app-body" id="appBody"> <div class="content" id="content">+ <section class="newbie" id="newbie">+ <button class="iconbtn newbie-close" id="btn-newbie-hide" aria-label="Close the newbie panel" data-tooltip="Tuck the whole panel away (phrases, component lights, getting started). The ? in the header brings it back."><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M18 6L6 18M6 6l12 12"/></svg></button> <section class="say" id="say" aria-labelledby="sayTitle"> <div class="say-head"> <h2 id="sayTitle">Say it to your AI</h2>@@ -571,18 +576,19 @@ dialog.settings{width:min(680px,92vw);overflow:hidden} <div class="say-list" id="sayList"></div> </section> <div class="leds" id="leds" aria-label="Component status"></div>- <section class="welcome" id="welcome" hidden>+ <section class="welcome" id="welcome"> <div class="welcome-head"> <h2>Getting started</h2>- <button class="iconbtn" id="btn-welcome-hide" aria-label="Tuck this away" data-tooltip="Tuck the getting-started card away. Bring it back any time with the ? button in the header."><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M18 6L6 18M6 6l12 12"/></svg></button> </div>- <p>Three steps get the first card on screen. Once cf feels familiar, tuck this card away with the X; the ? in the header brings it back.</p>+ <p>Once cf feels familiar, close this whole panel with the X in its corner; the ? in the header brings it back. The library starts with a few sample parts seeded from the Adom Wiki so the cards below are real; delete them any time.</p> <ol> <li><span class="num">1</span><div>Check the component lights above and fix anything that is not green. The Components card in Settings has an Install button for each one.<br><button class="btn" id="btn-welcome-settings">Open Settings to Components</button></div></li> <li><span class="num">2</span><div>Ask your AI for a chip by part number, using the word <b>cf</b>. Try it right now with a serious one, the STM32H743 (a 480 MHz Cortex-M7):<br><button class="btn btn-primary" id="btn-welcome-example">Copy: "fetch STM32H743VIT6 with cf"</button></div></li>- <li><span class="num">3</span><div>Cards appear here as parts are fetched. Each card carries the datasheet, the KiCad symbol, the footprint and the 3D model, with one-click open and send-to-EDA buttons.</div></li>+ <li><span class="num">3</span><div>Or watch one happen right now: the demo pulls a published part off the Adom Wiki the same way a real lookup ends, and its card appears below within seconds.<br><button class="btn btn-primary" id="btn-demo">Run a demo fetch</button> <span class="card-status" id="demoStatus"></span></div></li>+ <li><span class="num">4</span><div>Cards appear here as parts are fetched. Each card carries the datasheet, the KiCad symbol, the footprint and the 3D model, with one-click open and send-to-EDA buttons.</div></li> </ol> </section>+ </section> <div id="grid-view"><div class="cards" id="cards"><div class="empty-state">Loading the library</div></div></div> <div id="validation-view" class="view" hidden></div> <div id="sources-view" class="view" hidden></div>@@ -957,7 +963,13 @@ async function loadLibrary() { const vendors = [...new Set(LIB.chips.map(c => c.vendor).filter(v => v && v !== '?' && v !== DASH))].sort(); vsel.innerHTML = '<option value="">All vendors</option>' + vendors.map(v => `<option value="${escapeHtml(v)}">${escapeHtml(v)}</option>`).join(''); vsel.value = keep;- $id('welcome').hidden = !(WELCOME_PINNED || (LIB.chips.length === 0 && SETTINGS.show_welcome !== false));+ applyNewbiePanel();+ // The detail pane teaches by example: with nothing selected yet, open the+ // first sample card (or the first card) instead of an empty pane.+ if (!SELECTED && LIB.chips.length && !isPhone()) {+ const sample = LIB.chips.find(c => c.info && c.info.sample) || LIB.chips[0];+ select(sample.mpn);+ } updateSubject(); render(); renderProjectTabs(); if (firstLoad) loadProjects(); if (ACTIVE_MPN) { scrollActiveCardIntoView(); autoZoomIfEnabled(ACTIVE_MPN); }@@ -1774,12 +1786,33 @@ function openSettings(cardId) { } $id('btn-settings').onclick = () => openSettings(); $id('btn-welcome-settings').onclick = () => openSettings('card-components');-let WELCOME_PINNED = false; // the ? button shows the card even with a full library-$id('btn-welcome-hide').onclick = async () => {- WELCOME_PINNED = false;- $id('welcome').hidden = true;- try { await postJson('/api/settings', { show_welcome: false }); SETTINGS.show_welcome = false; } catch (e) {}- cfToast('Tucked away. The ? button in the header brings it back.', { type: 'ok' });+function applyNewbiePanel() {+ $id('newbie').hidden = SETTINGS.show_welcome === false;+ $id('btn-help').setAttribute('aria-pressed', String(SETTINGS.show_welcome !== false));+}+$id('btn-newbie-hide').onclick = async () => {+ SETTINGS.show_welcome = false; applyNewbiePanel();+ try { await postJson('/api/settings', { show_welcome: false }); } catch (e) {}+ cfToast('Newbie panel tucked away. The ? button in the header brings it back.', { type: 'ok' });+};+$id('btn-demo').onclick = async () => {+ const btn = $id('btn-demo'), st = $id('demoStatus');+ btn.disabled = true; st.textContent = 'starting';+ try {+ const r = await (await postJson('/api/jobs', { action: 'demo_fetch' })).json();+ if (!r.ok) throw new Error(r.error || 'could not start');+ const poll = setInterval(async () => {+ try {+ const j = await api('/api/jobs/' + r.id);+ st.textContent = j.state === 'running' ? 'fetching from the Adom Wiki' : j.state;+ if (j.state !== 'running') {+ clearInterval(poll); btn.disabled = false;+ if (j.state === 'done') { cfToast('Demo done: the card below just arrived from the Adom Wiki.', { type: 'ok' }); st.textContent = 'done'; }+ else cfToast('Demo failed; see the console (adom-chip-fetcher ui console).', { type: 'err' });+ }+ } catch (e) { clearInterval(poll); btn.disabled = false; }+ }, 800);+ } catch (e) { btn.disabled = false; st.textContent = ''; cfToast('Demo could not start: ' + e.message, { type: 'err' }); } }; $id('btn-welcome-example').onclick = async () => { const p = 'fetch STM32H743VIT6 with cf';@@ -1787,11 +1820,10 @@ $id('btn-welcome-example').onclick = async () => { else cfToast('Could not copy; type it into your AI instead.', { type: 'err' }); }; $id('btn-help').onclick = async () => {- WELCOME_PINNED = !WELCOME_PINNED;- if (WELCOME_PINNED && SETTINGS.show_welcome === false) { try { await postJson('/api/settings', { show_welcome: true }); SETTINGS.show_welcome = true; } catch (e) {} }- $id('welcome').hidden = !WELCOME_PINNED && !(LIB && LIB.chips.length === 0 && SETTINGS.show_welcome !== false);- if (!$id('welcome').hidden) $id('welcome').scrollIntoView({ block: 'nearest' });- $id('btn-help').setAttribute('aria-pressed', String(WELCOME_PINNED));+ const show = SETTINGS.show_welcome === false;+ SETTINGS.show_welcome = show; applyNewbiePanel();+ try { await postJson('/api/settings', { show_welcome: show }); } catch (e) {}+ if (show) $id('newbie').scrollIntoView({ block: 'start' }); }; $id('btnCloseSettings').onclick = () => dlg.close(); dlg.addEventListener('close', reportUi);