feat: site search with a dialog in the header and a /api/busca endpoint
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d8323c653b
commit
7d0edf13c4
7 files changed
+1115
-5
No files matched your search
@@ -0,0 +1,391 @@
|
||||
import { TOPICOS, normalizar } from "./searchTopics.js";
|
||||
|
||||
/**
|
||||
* Motor da busca inteligente (puro: sem React nem DOM).
|
||||
*
|
||||
* Em vez de só casar texto, ele INTERPRETA a consulta:
|
||||
* "flash s3" → chip = ESP32-S3, assunto = Flash
|
||||
* e então monta respostas diretas: o que a série diz sobre aquele assunto,
|
||||
* quais pinos do diagrama se relacionam a ele (com link já filtrado) e, por
|
||||
* fim, os resultados de texto de sempre (glossário, erros, componentes...).
|
||||
*/
|
||||
|
||||
const STOP = new Set([
|
||||
"de", "do", "da", "dos", "das", "o", "a", "os", "as", "um", "uma", "no", "na", "nos", "nas", "em",
|
||||
"para", "pra", "pro", "com", "sem", "que", "qual", "quais", "como", "e", "ou", "ver", "sobre", "eh",
|
||||
"preciso", "quero", "onde", "fica", "ficam", "esta", "estao", "meu", "minha", "usar", "uso",
|
||||
]);
|
||||
const INTENCAO_PINOS = new Set(["pino", "pinos", "pinagem", "pinout", "pin", "pins", "gpios"]);
|
||||
const INTENCAO_DADOS = new Set([
|
||||
"dados", "especificacao", "especificacoes", "spec", "specs", "informacao", "informacoes", "info",
|
||||
"quanto", "quantos", "quantas", "capacidade", "limite", "limites", "tem", "possui", "suporta",
|
||||
]);
|
||||
|
||||
const EXEMPLOS_TOPICOS = ["flash", "psram", "usb", "adc", "i2c", "deep sleep", "wifi", "strapping"];
|
||||
|
||||
/** Distância de edição com corte: devolve > max assim que passa do limite. */
|
||||
function distancia(a, b, max) {
|
||||
if (Math.abs(a.length - b.length) > max) return max + 1;
|
||||
let anterior = Array.from({ length: b.length + 1 }, (_, i) => i);
|
||||
for (let i = 1; i <= a.length; i++) {
|
||||
const atual = [i];
|
||||
let minimo = i;
|
||||
for (let j = 1; j <= b.length; j++) {
|
||||
const v = Math.min(anterior[j] + 1, atual[j - 1] + 1, anterior[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
||||
atual[j] = v;
|
||||
if (v < minimo) minimo = v;
|
||||
}
|
||||
if (minimo > max) return max + 1;
|
||||
anterior = atual;
|
||||
}
|
||||
return anterior[b.length];
|
||||
}
|
||||
|
||||
const palavrasDe = (texto) => texto.split(/[^a-z0-9]+/).filter((w) => w.length > 1);
|
||||
|
||||
/** Normaliza o payload do servidor uma única vez, na abertura da busca. */
|
||||
export function preparar(dados) {
|
||||
if (!dados) return { itens: [], series: {}, porFuncao: new Map(), aliases: new Map(), chaves: [] };
|
||||
|
||||
const itensRaw = Array.isArray(dados) ? dados : (dados.itens ?? []);
|
||||
const seriesRaw = Array.isArray(dados) ? {} : (dados.series ?? {});
|
||||
|
||||
const itens = itensRaw.map((d) => {
|
||||
const _t = normalizar(d.titulo ?? "");
|
||||
const _d = normalizar(d.descricao ?? "");
|
||||
const _k = normalizar(d.keywords ?? "");
|
||||
return { ...d, _t, _d, _k, _all: `${_t} ${_k} ${_d}`, _w: new Set(palavrasDe(`${_t} ${_k}`)) };
|
||||
});
|
||||
|
||||
const series = {};
|
||||
const porFuncao = new Map(); // "gpio26" | "mtck" -> [{ chave, pino }]
|
||||
const aliases = new Map();
|
||||
|
||||
for (const [chave, s] of Object.entries(seriesRaw)) {
|
||||
const pinos = (s.pinos ?? []).map((p) => {
|
||||
const fs = (p.f ?? []).map(normalizar);
|
||||
const avs = (p.a ?? []).map(normalizar);
|
||||
return {
|
||||
...p,
|
||||
_fs: fs,
|
||||
_fn: fs,
|
||||
_avs: avs,
|
||||
_nome: normalizar(p.nome ?? ""),
|
||||
_blob: normalizar([p.nome ?? "", ...(p.f ?? []), ...(p.c ?? []), ...(p.a ?? [])].join(" ")),
|
||||
};
|
||||
});
|
||||
series[chave] = { ...s, pinos };
|
||||
|
||||
for (const p of pinos) {
|
||||
for (const f of new Set([p._nome, ...p._fs])) {
|
||||
if (!/^[a-z0-9_]{3,}$/.test(f)) continue;
|
||||
if (!porFuncao.has(f)) porFuncao.set(f, []);
|
||||
porFuncao.get(f).push({ chave, pino: p });
|
||||
}
|
||||
}
|
||||
|
||||
const k = normalizar(chave);
|
||||
aliases.set(k, chave);
|
||||
aliases.set(k.replace("-", ""), chave);
|
||||
if (k.startsWith("esp32-")) aliases.set(k.slice(6), chave);
|
||||
}
|
||||
|
||||
return { itens, series, porFuncao, aliases, chaves: Object.keys(series) };
|
||||
}
|
||||
|
||||
/** Tenta casar um termo do dicionário com os tokens livres. Devolve { idx[], qualidade } ou null. */
|
||||
function casarTermo(termo, livres) {
|
||||
const partes = termo.split(" ");
|
||||
if (partes.length > 1) {
|
||||
for (let i = 0; i + partes.length <= livres.length; i++) {
|
||||
if (partes.every((p, j) => livres[i + j].t === p)) {
|
||||
return { idx: partes.map((_, j) => livres[i + j].i), qualidade: 3 + partes.length };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
for (const { t, i } of livres) {
|
||||
if (t === termo) return { idx: [i], qualidade: 3 };
|
||||
if (t === `${termo}s` || `${t}s` === termo) return { idx: [i], qualidade: 2.5 };
|
||||
if (termo.length >= 5 && t.length >= 5 && distancia(t, termo, 1) <= 1) return { idx: [i], qualidade: 1 };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function interpretar(indice, consulta) {
|
||||
const bruto = normalizar(consulta)
|
||||
.replace(/gpio\s*-?\s*(\d+)/g, "gpio$1")
|
||||
.replace(/[^a-z0-9.+\-_ ]/g, " ");
|
||||
const tokens = bruto.split(/\s+/).filter(Boolean);
|
||||
const usados = new Set();
|
||||
|
||||
// 1) Chips
|
||||
const chips = [];
|
||||
tokens.forEach((t, i) => {
|
||||
const chave = indice.aliases.get(t);
|
||||
if (chave && t !== "esp32") {
|
||||
if (!chips.includes(chave)) chips.push(chave);
|
||||
usados.add(i);
|
||||
}
|
||||
});
|
||||
tokens.forEach((t, i) => {
|
||||
if (t !== "esp32") return;
|
||||
usados.add(i);
|
||||
if (!chips.length && indice.series.ESP32) chips.push("ESP32");
|
||||
});
|
||||
|
||||
// 2) Pinos citados (GPIO26) e palavras de intenção / ligação
|
||||
const gpios = [];
|
||||
let querPinos = false;
|
||||
let querDados = false;
|
||||
tokens.forEach((t, i) => {
|
||||
if (/^gpio\d+$/.test(t)) {
|
||||
gpios.push(t);
|
||||
usados.add(i);
|
||||
} else if (INTENCAO_PINOS.has(t)) {
|
||||
querPinos = true;
|
||||
usados.add(i);
|
||||
} else if (INTENCAO_DADOS.has(t)) {
|
||||
querDados = true;
|
||||
usados.add(i);
|
||||
} else if (STOP.has(t)) {
|
||||
usados.add(i);
|
||||
}
|
||||
});
|
||||
|
||||
// 3) Assuntos: os melhores casamentos, sem reaproveitar token
|
||||
const livres = tokens.map((t, i) => ({ t, i })).filter(({ i }) => !usados.has(i));
|
||||
const candidatos = [];
|
||||
for (const topico of TOPICOS) {
|
||||
let melhor = null;
|
||||
for (const termo of topico.termos) {
|
||||
const m = casarTermo(normalizar(termo), livres);
|
||||
if (m && (!melhor || m.qualidade > melhor.qualidade)) melhor = m;
|
||||
}
|
||||
if (melhor) candidatos.push({ topico, ...melhor });
|
||||
}
|
||||
candidatos.sort((a, b) => b.qualidade - a.qualidade);
|
||||
const topicos = [];
|
||||
for (const c of candidatos) {
|
||||
if (topicos.length >= 3) break;
|
||||
if (c.idx.some((i) => usados.has(i))) continue;
|
||||
c.idx.forEach((i) => usados.add(i));
|
||||
topicos.push(c.topico);
|
||||
}
|
||||
|
||||
// 4) Nome/função de pino exato (ex.: "mtck", "xtal_p")
|
||||
const funcoes = [];
|
||||
tokens.forEach((t, i) => {
|
||||
if (usados.has(i) || t.length < 3) return;
|
||||
if (indice.porFuncao.has(t)) {
|
||||
funcoes.push(t);
|
||||
usados.add(i);
|
||||
}
|
||||
});
|
||||
|
||||
const restantes = tokens.filter((_, i) => !usados.has(i));
|
||||
return { tokens, chips, topicos, gpios, funcoes, restantes, querPinos, querDados };
|
||||
}
|
||||
|
||||
const rotuloPino = (p) => {
|
||||
const gpio = p.f.find((f) => /^GPIO\d+$/i.test(f));
|
||||
return gpio && gpio.toLowerCase() !== p.nome.toLowerCase() ? `${gpio}/${p.nome}` : p.nome;
|
||||
};
|
||||
|
||||
/** Pinos do diagrama ligados ao assunto, já separados por nível de certeza. */
|
||||
function pinosDoTopico(serie, topico) {
|
||||
const forte = [];
|
||||
const fraco = [];
|
||||
for (const p of serie.pinos) {
|
||||
let nivel = null;
|
||||
if (topico.pinoNivel) nivel = topico.pinoNivel(p);
|
||||
else if ((topico.pinoCats && p.c.some((c) => topico.pinoCats.includes(c))) || (topico.pinoRe && topico.pinoRe.test(p._blob))) nivel = "forte";
|
||||
if (nivel === "forte") forte.push(p);
|
||||
else if (nivel === "fraco") fraco.push(p);
|
||||
}
|
||||
return { forte, fraco };
|
||||
}
|
||||
|
||||
const hrefPinos = (chave, pinos, motivo) =>
|
||||
`/series/${chave}?pinos=${pinos.map((p) => p.n).join(",")}&motivo=${encodeURIComponent(motivo)}#connections`;
|
||||
|
||||
function respostas(indice, it) {
|
||||
const saida = [];
|
||||
const alvos = it.chips.slice(0, 2);
|
||||
|
||||
for (const chave of alvos) {
|
||||
const serie = indice.series[chave];
|
||||
if (!serie) continue;
|
||||
|
||||
for (const topico of it.topicos.slice(0, 2)) {
|
||||
const specs = serie.specs.filter((s) => s.topicos.includes(topico.id));
|
||||
const cartaoSpec = specs.length
|
||||
? {
|
||||
tipo: "resposta",
|
||||
titulo: `${topico.nome} no ${serie.nome}`,
|
||||
descricao: "Dados da série",
|
||||
linhas: specs.slice(0, 5).map((s) => ({ k: s.label, v: s.valor })),
|
||||
href: `/series/${chave}#${specs[0].secao}`,
|
||||
}
|
||||
: null;
|
||||
|
||||
const { forte, fraco } = pinosDoTopico(serie, topico);
|
||||
const principais = forte.length ? forte : fraco;
|
||||
const cartaoPinos = principais.length
|
||||
? {
|
||||
tipo: "pino",
|
||||
titulo: `Pinos do ${serie.nome} ${topico.pinoRotulo}`,
|
||||
descricao:
|
||||
forte.length && fraco.length
|
||||
? `${forte.length} ocupados + ${fraco.length} que dependem do modo de uso (ex.: SPI de 8 linhas) · abre o diagrama filtrado`
|
||||
: forte.length
|
||||
? `${forte.length} ${forte.length === 1 ? "pino" : "pinos"} · abre o diagrama já filtrado`
|
||||
: `${fraco.length} pinos que só são afetados em certos modos de uso · abre o diagrama filtrado`,
|
||||
chips: forte
|
||||
.slice(0, 12)
|
||||
.map(rotuloPino)
|
||||
.concat(forte.length > 12 ? [`+${forte.length - 12}`] : [])
|
||||
.concat(fraco.length && forte.length ? [`+${fraco.length} condicionais`] : forte.length ? [] : fraco.slice(0, 10).map(rotuloPino)),
|
||||
href: hrefPinos(chave, [...forte, ...fraco], `${topico.nome}: pinos ${topico.pinoRotulo}`),
|
||||
}
|
||||
: null;
|
||||
|
||||
(it.querPinos ? [cartaoPinos, cartaoSpec] : [cartaoSpec, cartaoPinos]).filter(Boolean).forEach((c) => saida.push(c));
|
||||
}
|
||||
|
||||
if (!it.topicos.length && !it.gpios.length && !it.funcoes.length) {
|
||||
if (it.querPinos && serie.pinos.length) {
|
||||
saida.push({
|
||||
tipo: "pino",
|
||||
titulo: `Diagrama de pinos do ${serie.nome}`,
|
||||
descricao: `${serie.pinos.length} pinos com funções, restrições e avisos`,
|
||||
href: `/series/${chave}#connections`,
|
||||
});
|
||||
}
|
||||
if (it.querDados) {
|
||||
saida.push({ tipo: "resposta", titulo: `Especificações do ${serie.nome}`, descricao: "Processador, memória, conectividade, periféricos e mais", href: `/series/${chave}` });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Assunto sem chip: panorama por série ("qual série tem PSRAM?")
|
||||
if (!alvos.length && it.topicos.length) {
|
||||
const topico = it.topicos[0];
|
||||
const linhas = [];
|
||||
for (const chave of indice.chaves) {
|
||||
const serie = indice.series[chave];
|
||||
const spec = serie.specs.find((s) => s.topicos.includes(topico.id));
|
||||
if (spec) linhas.push({ k: serie.nome, v: spec.valor.length > 46 ? `${spec.valor.slice(0, 45)}…` : spec.valor });
|
||||
}
|
||||
if (linhas.length > 1) {
|
||||
saida.push({ tipo: "resposta", titulo: `${topico.nome} em cada série`, descricao: "Comparativo rápido · adicione o chip na busca para detalhar", linhas, href: "/comparacao" });
|
||||
}
|
||||
}
|
||||
|
||||
// GPIO26 e nomes de função: "gpio26 s3", "mtck"
|
||||
const buscas = [...it.gpios, ...it.funcoes];
|
||||
for (const termo of buscas) {
|
||||
const achados = indice.porFuncao.get(termo) ?? [];
|
||||
const filtrados = it.chips.length ? achados.filter((a) => it.chips.includes(a.chave)) : achados;
|
||||
for (const { chave, pino } of filtrados.slice(0, it.chips.length ? 3 : 5)) {
|
||||
const serie = indice.series[chave];
|
||||
saida.push({
|
||||
tipo: "pino",
|
||||
titulo: `${rotuloPino(pino)} no ${serie.nome}`,
|
||||
descricao: [pino.f.join(" · "), pino.a[0]].filter(Boolean).join(" — "),
|
||||
href: hrefPinos(chave, [pino], `Pino ${rotuloPino(pino)}`),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return saida;
|
||||
}
|
||||
|
||||
function pontuarTermo(item, t) {
|
||||
if (item._t === t) return 100;
|
||||
if (item._t.startsWith(t)) return 60;
|
||||
if (item._t.includes(t)) return 40;
|
||||
if (item._k.includes(t)) return 15;
|
||||
if (item._d.includes(t)) return 10;
|
||||
if (t.length >= 4) {
|
||||
const max = t.length >= 8 ? 2 : 1;
|
||||
for (const w of item._w) if (distancia(w, t, max) <= max) return 6;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function casaTopico(item, topico) {
|
||||
let melhor = 0;
|
||||
for (const termo of topico.termos) {
|
||||
const n = normalizar(termo);
|
||||
if (n.includes(" ") ? item._all.includes(n) : item._w.has(n)) {
|
||||
melhor = Math.max(melhor, item._t.includes(n) ? 2 : 1);
|
||||
}
|
||||
}
|
||||
return melhor;
|
||||
}
|
||||
|
||||
const topicoCasou = (item, topicos) => topicos.some((t) => casaTopico(item, t));
|
||||
|
||||
export function buscar(indice, consulta) {
|
||||
const it = interpretar(indice, consulta);
|
||||
const interpretacao = {
|
||||
chips: it.chips.map((c) => ({ chave: c, nome: indice.series[c]?.nome ?? c })),
|
||||
topicos: it.topicos.map((t) => ({ id: t.id, nome: t.nome })),
|
||||
};
|
||||
|
||||
const smart = respostas(indice, it).map((r, i) => ({ id: `r${i}`, keywords: "", ...r }));
|
||||
|
||||
const nomesChips = it.chips.map((c) => new RegExp(`(?<![a-z0-9])${normalizar(c).replace(/[-]/g, "\\-")}(?![a-z0-9]|-[a-z0-9])`));
|
||||
const pontuados = [];
|
||||
for (const item of indice.itens) {
|
||||
let s = 0;
|
||||
let ok = true;
|
||||
for (const t of it.restantes) {
|
||||
const p = pontuarTermo(item, t);
|
||||
if (!p) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
s += p;
|
||||
}
|
||||
if (!ok) continue;
|
||||
|
||||
let acertos = 0;
|
||||
for (const topico of it.topicos) {
|
||||
const m = casaTopico(item, topico);
|
||||
if (m) {
|
||||
s += m === 2 ? 50 : 20;
|
||||
acertos++;
|
||||
}
|
||||
}
|
||||
nomesChips.forEach((nome, i) => {
|
||||
if (item.tipo === "serie" && item.titulo === it.chips[i]) {
|
||||
s += 300;
|
||||
acertos++;
|
||||
} else if (nome.test(item._all)) {
|
||||
s += 25;
|
||||
acertos++;
|
||||
}
|
||||
});
|
||||
|
||||
const ehSerie = item.tipo === "serie" && it.chips.includes(item.titulo);
|
||||
if (!it.restantes.length && !(it.topicos.length ? acertos > (ehSerie ? 0 : 0) && topicoCasou(item, it.topicos) : ehSerie)) continue;
|
||||
if (s > 0) pontuados.push({ item, s });
|
||||
}
|
||||
pontuados.sort((a, b) => b.s - a.s);
|
||||
|
||||
// Quando já respondemos com cartões, a série que repete a resposta fica no fim.
|
||||
const texto = pontuados.slice(0, 24).map((p) => p.item);
|
||||
|
||||
const sugestoes = [];
|
||||
if (it.topicos.length && !it.chips.length) {
|
||||
for (const c of indice.chaves.slice(0, 12)) sugestoes.push({ rotulo: indice.series[c].nome, anexar: c });
|
||||
} else if (it.chips.length && !it.topicos.length && !it.gpios.length) {
|
||||
for (const nome of EXEMPLOS_TOPICOS) sugestoes.push({ rotulo: nome, anexar: nome });
|
||||
}
|
||||
|
||||
return { resultados: [...smart, ...texto], interpretacao, sugestoes, totalRespostas: smart.length };
|
||||
}
|
||||
|
||||
export const EXEMPLOS_DE_BUSCA = ["flash s3", "pinos usb c6", "deep sleep p4", "gpio0 esp32", "secure boot c3", "psram"];
|
||||
@@ -0,0 +1,104 @@
|
||||
import seriesData from "@/public/series.json";
|
||||
import frameworksData from "@/public/frameworks.json";
|
||||
import { categoriasGlossario } from "@/lib/glossarioData";
|
||||
import { errosCatalogo } from "@/lib/diagnosticoData";
|
||||
import { COMPONENTES_REGISTRY } from "@/lib/componentRegistryData";
|
||||
import { montarConhecimento } from "@/lib/searchTopics";
|
||||
import { readFileSync, existsSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
/**
|
||||
* Índice de busca do site, montado no servidor a partir dos mesmos dados que
|
||||
* alimentam as páginas. Uma série, termo ou erro novo entra sozinho na busca.
|
||||
*
|
||||
* Cada entrada: { id, tipo, titulo, descricao, href, keywords }
|
||||
* `keywords` guarda texto extra pesquisável (não exibido).
|
||||
*/
|
||||
|
||||
const PAGINAS = [
|
||||
{ titulo: "Início", descricao: "Página inicial do ESPDocs", href: "/", keywords: "home inicio" },
|
||||
{ titulo: "Todas as Séries", descricao: "Pinouts, especificações e restrições de cada série ESP32", href: "/series", keywords: "chips pinout especificações" },
|
||||
{ titulo: "Comparador de Séries", descricao: "Compare até 4 modelos lado a lado", href: "/comparacao", keywords: "comparar comparação versus" },
|
||||
{ titulo: "Catálogo de Placas", descricao: "Placas de desenvolvimento, conectores e compras", href: "/catalogo", keywords: "placas dev boards comprar devkit" },
|
||||
{ titulo: "Seletor Inteligente", descricao: "Descubra o chip ideal para o seu projeto", href: "/seletor", keywords: "quiz escolher recomendar" },
|
||||
{ titulo: "Diagnóstico de Erros", descricao: "Solucione brownout, bootloop, WDT e falhas", href: "/diagnostico", keywords: "erro problema bug falha" },
|
||||
{ titulo: "Frameworks & SDKs", descricao: "ESP-IDF, Arduino, Rust, RainMaker e mais", href: "/frameworks", keywords: "sdk" },
|
||||
{ titulo: "Component Registry", descricao: "Catálogo de drivers e gerador de idf_component.yml", href: "/componentes", keywords: "bibliotecas drivers yml" },
|
||||
{ titulo: "Glossário Técnico", descricao: "Conceitos essenciais de hardware e software", href: "/glossario", keywords: "termos dicionário significado" },
|
||||
{ titulo: "O Livro", descricao: "Microcontroladores ESP32: conectividade, eficiência e desempenho", href: "/livro", keywords: "livro book capa comprar" },
|
||||
{ titulo: "Sobre o Projeto", descricao: "História, autor e Ciência Embarcada", href: "/sobre", keywords: "autor contato" },
|
||||
];
|
||||
|
||||
const cortar = (texto = "", max = 140) => (texto.length > max ? `${texto.slice(0, max - 1).trimEnd()}…` : texto);
|
||||
|
||||
export function construirIndiceBusca() {
|
||||
const itens = [];
|
||||
|
||||
for (const p of PAGINAS) {
|
||||
itens.push({ tipo: "pagina", ...p, keywords: p.keywords });
|
||||
}
|
||||
|
||||
for (const [chave, s] of Object.entries(seriesData)) {
|
||||
itens.push({
|
||||
tipo: "serie",
|
||||
titulo: chave,
|
||||
descricao: cortar(`${s.nome_completo ?? ""} — ${s.descricao ?? ""}`),
|
||||
href: `/series/${chave}`,
|
||||
keywords: [s.arquitetura, s.wifi, s.bluetooth, s.frequencia, s.nucleos].filter(Boolean).join(" "),
|
||||
});
|
||||
}
|
||||
|
||||
for (const [chave, f] of Object.entries(frameworksData)) {
|
||||
itens.push({
|
||||
tipo: "framework",
|
||||
titulo: f.nome ?? chave,
|
||||
descricao: cortar(`${f.nome_completo ?? ""} — ${f.descricao ?? ""}`),
|
||||
href: `/frameworks/${encodeURIComponent(chave)}`,
|
||||
keywords: [f.linguagem, f.tipo, f.funcao_principal].filter(Boolean).join(" "),
|
||||
});
|
||||
}
|
||||
|
||||
for (const cat of categoriasGlossario) {
|
||||
for (const t of cat.termos) {
|
||||
const slug = t.termo.toLowerCase().replace(/[\s()/²]+/g, "-");
|
||||
itens.push({
|
||||
tipo: "glossario",
|
||||
titulo: t.termo,
|
||||
descricao: cortar(t.definicao),
|
||||
href: `/glossario#${slug}`,
|
||||
keywords: [cat.nome, t.exemplo].filter(Boolean).join(" "),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const e of errosCatalogo) {
|
||||
itens.push({
|
||||
tipo: "erro",
|
||||
titulo: e.titulo,
|
||||
descricao: cortar(e.resumo),
|
||||
href: `/diagnostico#${e.id}`,
|
||||
keywords: [e.logSerial, ...(e.sintomas ?? [])].filter(Boolean).join(" "),
|
||||
});
|
||||
}
|
||||
|
||||
for (const c of COMPONENTES_REGISTRY) {
|
||||
itens.push({
|
||||
tipo: "componente",
|
||||
titulo: c.titulo,
|
||||
descricao: cortar(c.descricao),
|
||||
href: "/componentes",
|
||||
keywords: [c.name, c.namespace, ...(c.tags ?? [])].filter(Boolean).join(" "),
|
||||
});
|
||||
}
|
||||
|
||||
const conexoes = {};
|
||||
for (const chave of Object.keys(seriesData)) {
|
||||
const arquivo = path.join(process.cwd(), "public", "conexoes", `${chave}.json`);
|
||||
if (existsSync(arquivo)) conexoes[chave] = JSON.parse(readFileSync(arquivo, "utf8"));
|
||||
}
|
||||
|
||||
return {
|
||||
itens: itens.map((item, i) => ({ id: i, ...item })),
|
||||
series: montarConhecimento(seriesData, conexoes),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* Conhecimento de domínio da busca inteligente.
|
||||
*
|
||||
* Sem dependências nem alias de import: roda no servidor (montando o índice),
|
||||
* no navegador (interpretando a consulta) e em scripts de teste no Node.
|
||||
*
|
||||
* Todos os textos de `termos` já estão normalizados (minúsculos, sem acento).
|
||||
*/
|
||||
|
||||
export const normalizar = (s = "") =>
|
||||
String(s)
|
||||
.toLowerCase()
|
||||
.normalize("NFD")
|
||||
.replace(/[\u0300-\u036f]/g, "");
|
||||
|
||||
/**
|
||||
* Reconhece pinos ocupados por flash/PSRAM com base no que o diagrama diz.
|
||||
* "forte": o pino é consumido pela memória (ou é a alimentação dela).
|
||||
* "fraco": só é afetado em certos modos (ex.: SPI de 8 linhas/octal).
|
||||
* Avisos genéricos de tensão mínima ("para chips com flash integrada...") não contam.
|
||||
*/
|
||||
const nivelMemoria = (palavra) => (p) => {
|
||||
const citam = p._avs.filter((a) => a.includes(palavra) && !/^para chips (com|sem)/.test(a));
|
||||
if (citam.some((a) => /(utilizad|dedicad|conectad)/.test(a) && !/a menos que/.test(a))) return "forte";
|
||||
if (p._fn.some((f) => /alimentacao/.test(f) && f.includes(palavra))) return "forte";
|
||||
if (citam.length) return "fraco";
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Assuntos que o usuário pode citar. Cada um sabe:
|
||||
* - termos: como as pessoas escrevem (sinônimos, siglas, plurais comuns);
|
||||
* - pinoCats / pinoRe: como reconhecer os pinos relacionados no diagrama;
|
||||
* - pinoRotulo: frase usada no título do resultado de pinos.
|
||||
* Os dados de especificação ligam-se aos assuntos por SPEC_CAMPOS.
|
||||
*/
|
||||
export const TOPICOS = [
|
||||
{ id: "flash", nome: "Flash", termos: ["flash", "memoria flash", "spi flash", "armazenamento", "firmware", "rom externa"], pinoNivel: nivelMemoria("flash"), pinoRotulo: "ligados à memória flash" },
|
||||
{ id: "psram", nome: "PSRAM", termos: ["psram", "ram externa", "spiram"], pinoNivel: nivelMemoria("psram"), pinoRotulo: "ligados à PSRAM" },
|
||||
{ id: "memoria", nome: "Memória", termos: ["memoria", "sram", "heap", "rtc ram", "kb de ram", "memoria interna", "ram"], pinoRotulo: "" },
|
||||
{ id: "wifi", nome: "Wi-Fi", termos: ["wifi", "wi-fi", "wireless", "sem fio", "802.11", "wifi6", "wifi 6", "wi-fi 6"], pinoCats: ["Antena"], pinoRotulo: "de antena e rádio" },
|
||||
{ id: "bluetooth", nome: "Bluetooth", termos: ["bluetooth", "ble", "bt", "bluetooth le", "mesh"], pinoCats: ["Antena"], pinoRotulo: "de antena e rádio" },
|
||||
{ id: "zigbee", nome: "Zigbee/Thread", termos: ["zigbee", "thread", "802.15.4", "ieee 802.15.4"], pinoRotulo: "" },
|
||||
{ id: "matter", nome: "Matter", termos: ["matter", "casa inteligente", "smart home"], pinoRotulo: "" },
|
||||
{ id: "ethernet", nome: "Ethernet", termos: ["ethernet", "rmii", "cabo de rede", "lan"], pinoCats: ["Ethernet"], pinoRotulo: "de Ethernet" },
|
||||
{ id: "can", nome: "CAN / TWAI", termos: ["can", "twai", "can bus", "canbus", "can fd"], pinoRe: /twai|\bcan\b/, pinoRotulo: "de CAN/TWAI" },
|
||||
{ id: "usb", nome: "USB", termos: ["usb", "otg", "usb serial", "usb otg", "hid", "cdc"], pinoCats: ["USB"], pinoRotulo: "de USB" },
|
||||
{ id: "jtag", nome: "JTAG / Depuração", termos: ["jtag", "debug", "depuracao", "depurar", "openocd", "gdb"], pinoCats: ["JTAG"], pinoRotulo: "de JTAG" },
|
||||
{ id: "adc", nome: "ADC", termos: ["adc", "analogico", "entrada analogica", "conversor analogico", "analogread", "saradc"], pinoCats: ["ADC"], pinoRotulo: "com ADC" },
|
||||
{ id: "dac", nome: "DAC", termos: ["dac", "saida analogica", "conversor digital"], pinoCats: ["DAC"], pinoRotulo: "com DAC" },
|
||||
{ id: "touch", nome: "Touch capacitivo", termos: ["touch", "capacitivo", "toque", "touchpad", "sensor de toque"], pinoCats: ["Touch"], pinoRotulo: "de touch capacitivo" },
|
||||
{ id: "uart", nome: "UART", termos: ["uart", "serial", "tx", "rx", "txd", "rxd", "porta serial"], pinoCats: ["UART"], pinoRotulo: "de UART" },
|
||||
{ id: "spi", nome: "SPI", termos: ["spi", "mosi", "miso", "sclk", "barramento spi"], pinoCats: ["SPI"], pinoRotulo: "de SPI" },
|
||||
{ id: "i2c", nome: "I2C", termos: ["i2c", "sda", "scl", "twi", "iic"], pinoCats: ["I2C"], pinoRotulo: "de I2C" },
|
||||
{ id: "i2s", nome: "I2S", termos: ["i2s", "audio", "som", "microfone", "pdm", "codec"], pinoRe: /i2s|\bpdm\b/, pinoRotulo: "de I2S/áudio" },
|
||||
{ id: "pwm", nome: "PWM", termos: ["pwm", "ledc", "mcpwm", "servo", "dimmer"], pinoRe: /ledc|pwm/, pinoRotulo: "de PWM" },
|
||||
{ id: "sd", nome: "Cartão SD / SDIO", termos: ["sd", "sdio", "sd card", "sdcard", "cartao sd", "microsd", "sdmmc", "mmc"], pinoCats: ["SD"], pinoRe: /sdio|sdmmc|\bsd\b/, pinoRotulo: "de cartão SD/SDIO" },
|
||||
{ id: "camera", nome: "Câmera", termos: ["camera", "cam", "dvp", "csi", "mipi csi", "ov2640", "video"], pinoCats: ["CSI"], pinoRe: /\bcam\b|dvp|cam_/, pinoRotulo: "de câmera" },
|
||||
{ id: "lcd", nome: "Display / LCD", termos: ["lcd", "display", "tela", "dsi", "mipi dsi", "oled", "tft", "lvgl", "rgb"], pinoCats: ["DSI"], pinoRe: /lcd/, pinoRotulo: "de display/LCD" },
|
||||
{ id: "strapping", nome: "Boot / Strapping", termos: ["strapping", "boot", "bootstrap", "modo de boot", "download mode", "modo de gravacao", "inicializacao"], pinoCats: ["Strapping"], pinoRotulo: "de strapping (modo de boot)" },
|
||||
{ id: "energia_pinos", nome: "Alimentação", termos: ["alimentacao", "vdd", "3v3", "3.3v", "tensao", "regulador", "ldo", "fonte"], pinoCats: ["Power"], pinoRotulo: "de alimentação" },
|
||||
{ id: "clock", nome: "Clock / Cristal", termos: ["clock", "xtal", "cristal", "oscilador", "32khz", "40mhz"], pinoCats: ["Clock"], pinoRotulo: "de clock/cristal" },
|
||||
{ id: "gpio", nome: "GPIO", termos: ["gpio", "entradas e saidas"], pinoCats: ["GPIO"], pinoRotulo: "de GPIO" },
|
||||
{ id: "baixo_consumo", nome: "Baixo consumo / ULP", termos: ["ulp", "lp core", "lp cpu", "coprocessador", "lp gpio", "low power", "rtc gpio"], pinoCats: ["Low Power"], pinoRotulo: "de baixo consumo (LP/RTC)" },
|
||||
{ id: "antena", nome: "Antena / RF", termos: ["antena", "rf", "radiofrequencia", "ipex"], pinoCats: ["Antena"], pinoRotulo: "de antena/RF" },
|
||||
{ id: "seguranca", nome: "Segurança", termos: ["seguranca", "secure boot", "criptografia", "encriptacao", "aes", "sha", "rsa", "hmac", "ecdsa", "efuse", "tee", "trng", "rng", "hsm", "protecao"], pinoRotulo: "" },
|
||||
{ id: "consumo", nome: "Consumo de energia", termos: ["consumo", "bateria", "deep sleep", "light sleep", "sleep", "modem sleep", "corrente", "miliamperes", "autonomia", "economia de energia", "low power mode"], pinoRotulo: "" },
|
||||
{ id: "temperatura", nome: "Temperatura", termos: ["temperatura", "calor", "graus", "faixa de temperatura", "industrial"], pinoRotulo: "" },
|
||||
{ id: "ia", nome: "IA / Aceleração", termos: ["ia", "ai", "machine learning", "ml", "tinyml", "vetorial", "inteligencia artificial", "rede neural", "esp-nn", "esp-dl"], pinoRotulo: "" },
|
||||
{ id: "processador", nome: "Processador", termos: ["processador", "cpu", "nucleo", "nucleos", "core", "dual core", "single core", "arquitetura", "xtensa", "risc-v", "riscv"], pinoRotulo: "" },
|
||||
{ id: "frequencia", nome: "Frequência", termos: ["frequencia", "mhz", "velocidade", "desempenho", "performance"], pinoRotulo: "" },
|
||||
{ id: "ferramentas", nome: "Ferramentas / SDK", termos: ["esp-idf", "idf", "arduino", "sdk", "toolchain", "versao do idf"], pinoRotulo: "" },
|
||||
{ id: "encapsulamento", nome: "Módulos e encapsulamento", termos: ["modulo", "modulos", "encapsulamento", "package", "qfn", "wroom", "wrover", "mini-1"], pinoRotulo: "" },
|
||||
];
|
||||
|
||||
/**
|
||||
* Campos de public/series.json que respondem a cada assunto.
|
||||
* `campo` aceita caminho com ponto (seguranca.aes). `secao` é o id da âncora
|
||||
* na página /series/[key].
|
||||
*/
|
||||
export const SPEC_CAMPOS = [
|
||||
{ campo: "arquitetura", label: "Arquitetura", secao: "processador", topicos: ["processador"] },
|
||||
{ campo: "nucleos", label: "Núcleos", secao: "processador", topicos: ["processador"] },
|
||||
{ campo: "frequencia", label: "Frequência", secao: "processador", topicos: ["frequencia", "processador"] },
|
||||
{ campo: "coprocessador_ulp", label: "Coprocessador ULP", secao: "processador", topicos: ["baixo_consumo"] },
|
||||
{ campo: "aceleradores_ia", label: "Aceleradores de IA", secao: "processador", topicos: ["ia"] },
|
||||
|
||||
{ campo: "wifi", label: "Wi-Fi", secao: "conectividade", topicos: ["wifi"] },
|
||||
{ campo: "bluetooth", label: "Bluetooth", secao: "conectividade", topicos: ["bluetooth"] },
|
||||
{ campo: "zigbee_thread", label: "Zigbee/Thread", secao: "conectividade", topicos: ["zigbee"] },
|
||||
{ campo: "matter", label: "Matter", secao: "conectividade", topicos: ["matter"] },
|
||||
{ campo: "ethernet", label: "Ethernet", secao: "conectividade", topicos: ["ethernet"] },
|
||||
{ campo: "ethernet_mac", label: "Ethernet MAC", secao: "conectividade", topicos: ["ethernet"] },
|
||||
{ campo: "can", label: "CAN", secao: "conectividade", topicos: ["can"] },
|
||||
|
||||
{ campo: "memoria_sram", label: "SRAM", secao: "memoria", topicos: ["memoria"] },
|
||||
{ campo: "memoria_sram_rtc", label: "SRAM RTC", secao: "memoria", topicos: ["memoria", "baixo_consumo"] },
|
||||
{ campo: "memoria_rom", label: "ROM", secao: "memoria", topicos: ["memoria"] },
|
||||
{ campo: "flash_externa", label: "Flash externa", secao: "memoria", topicos: ["flash", "memoria"] },
|
||||
{ campo: "psram_externa", label: "PSRAM externa", secao: "memoria", topicos: ["psram", "memoria"] },
|
||||
|
||||
{ campo: "gpio", label: "GPIO", secao: "perifericos", topicos: ["gpio"] },
|
||||
{ campo: "adc", label: "ADC", secao: "perifericos", topicos: ["adc"] },
|
||||
{ campo: "dac", label: "DAC", secao: "perifericos", topicos: ["dac"] },
|
||||
{ campo: "touch", label: "Touch", secao: "perifericos", topicos: ["touch"] },
|
||||
{ campo: "uart", label: "UART", secao: "perifericos", topicos: ["uart"] },
|
||||
{ campo: "spi", label: "SPI", secao: "perifericos", topicos: ["spi", "flash"] },
|
||||
{ campo: "i2c", label: "I2C", secao: "perifericos", topicos: ["i2c"] },
|
||||
{ campo: "i2s", label: "I2S", secao: "perifericos", topicos: ["i2s"] },
|
||||
{ campo: "pwm", label: "PWM", secao: "perifericos", topicos: ["pwm"] },
|
||||
{ campo: "usb", label: "USB", secao: "perifericos", topicos: ["usb"] },
|
||||
{ campo: "lcd", label: "LCD", secao: "interfaces", topicos: ["lcd"] },
|
||||
{ campo: "camera", label: "Câmera", secao: "interfaces", topicos: ["camera"] },
|
||||
{ campo: "mipi_csi", label: "MIPI CSI", secao: "interfaces", topicos: ["camera"] },
|
||||
{ campo: "mipi_dsi", label: "MIPI DSI", secao: "interfaces", topicos: ["lcd"] },
|
||||
{ campo: "sdio", label: "SDIO", secao: "interfaces", topicos: ["sd"] },
|
||||
{ campo: "sensor_temperatura", label: "Sensor de temperatura", secao: "interfaces", topicos: ["temperatura"] },
|
||||
|
||||
{ campo: "seguranca.aes", label: "AES", secao: "seguranca", topicos: ["seguranca"] },
|
||||
{ campo: "seguranca.sha", label: "SHA", secao: "seguranca", topicos: ["seguranca"] },
|
||||
{ campo: "seguranca.rsa", label: "RSA", secao: "seguranca", topicos: ["seguranca"] },
|
||||
{ campo: "seguranca.ecc", label: "ECC", secao: "seguranca", topicos: ["seguranca"] },
|
||||
{ campo: "seguranca.ecdsa", label: "ECDSA", secao: "seguranca", topicos: ["seguranca"] },
|
||||
{ campo: "seguranca.hmac", label: "HMAC", secao: "seguranca", topicos: ["seguranca"] },
|
||||
{ campo: "seguranca.assinatura_digital", label: "Assinatura digital", secao: "seguranca", topicos: ["seguranca"] },
|
||||
{ campo: "seguranca.rng", label: "Gerador aleatório", secao: "seguranca", topicos: ["seguranca"] },
|
||||
{ campo: "seguranca.criptografia_flash", label: "Criptografia de flash", secao: "seguranca", topicos: ["seguranca", "flash"] },
|
||||
{ campo: "seguranca.secure_boot", label: "Secure Boot", secao: "seguranca", topicos: ["seguranca", "strapping"] },
|
||||
|
||||
{ campo: "temperatura_operacao", label: "Temperatura de operação", secao: "temperatura", topicos: ["temperatura"] },
|
||||
{ campo: "temperatura_armazenamento", label: "Temperatura de armazenamento", secao: "temperatura", topicos: ["temperatura"] },
|
||||
|
||||
{ campo: "esp_idf_minimo", label: "ESP-IDF mínimo", secao: "ferramentas", topicos: ["ferramentas"] },
|
||||
{ campo: "arduino_core", label: "Core Arduino", secao: "ferramentas", topicos: ["ferramentas"] },
|
||||
{ campo: "depuracao", label: "Depuração", secao: "ferramentas", topicos: ["jtag", "ferramentas"] },
|
||||
{ campo: "encapsulamento", label: "Encapsulamento", secao: "ferramentas", topicos: ["encapsulamento"] },
|
||||
{ campo: "modulos", label: "Módulos", secao: "ferramentas", topicos: ["encapsulamento"] },
|
||||
];
|
||||
|
||||
const ROTULOS_ENERGIA = {
|
||||
active: "Ativo",
|
||||
modem_sleep: "Modem Sleep",
|
||||
light_sleep: "Light Sleep",
|
||||
deep_sleep: "Deep Sleep",
|
||||
hibernation: "Hibernação",
|
||||
};
|
||||
|
||||
const pegar = (obj, caminho) => caminho.split(".").reduce((o, k) => (o == null ? o : o[k]), obj);
|
||||
|
||||
/**
|
||||
* Monta, para cada série, a lista plana de especificações (com os assuntos a
|
||||
* que respondem) e os pinos do diagrama. É o que o navegador baixa.
|
||||
*
|
||||
* @param {Record<string, object>} series conteúdo de public/series.json
|
||||
* @param {Record<string, object[]>} conexoes pinos por série (public/conexoes)
|
||||
*/
|
||||
export function montarConhecimento(series, conexoes) {
|
||||
const saida = {};
|
||||
for (const [chave, s] of Object.entries(series)) {
|
||||
const specs = [];
|
||||
for (const c of SPEC_CAMPOS) {
|
||||
const valor = pegar(s, c.campo);
|
||||
if (valor == null || valor === "") continue;
|
||||
specs.push({ label: c.label, valor: String(valor), secao: c.secao, topicos: c.topicos });
|
||||
}
|
||||
for (const [modo, valor] of Object.entries(s.consumo_energia ?? {})) {
|
||||
const base = modo.replace(/_cpu.*|_wifi.*/, "");
|
||||
const label = ROTULOS_ENERGIA[modo] ?? ROTULOS_ENERGIA[base] ?? modo.replace(/_/g, " ");
|
||||
specs.push({ label: `Consumo: ${label}`, valor: String(valor), secao: "energia", topicos: ["consumo"] });
|
||||
}
|
||||
saida[chave] = {
|
||||
nome: s.nome_completo ?? chave,
|
||||
specs,
|
||||
pinos: (conexoes[chave] ?? []).map((p) => ({
|
||||
n: String(p.numero),
|
||||
nome: p.nome,
|
||||
f: p.funcoes ?? [],
|
||||
c: p.categorias ?? [],
|
||||
a: p.avisos ?? [],
|
||||
})),
|
||||
};
|
||||
}
|
||||
return saida;
|
||||
}
|
||||
Reference in new issue
Block a user