FAQ
Troubleshooting decision tree
One question at a time until the answer appears
One question at a time until the answer appears
Build a guided troubleshooting widget with Tailwind CSS. Use a max-w-xl bordered card with a heading reading "Let us narrow it down", a muted subline, and a breadcrumb row of answered steps rendered as small pills with the chosen answers and a reset link on the right. The body shows one question at a time: a question in medium weight and two or three full-width answer buttons as bordered rows with a chevron. The final state replaces the question with a resolution panel — a tinted box with a title, two paragraphs, a monospaced command where relevant, and two buttons for marking it solved or contacting support. Then add JavaScript holding the tree as a nested object, rendering the current node, appending each answer to the breadcrumb, allowing a click on any breadcrumb pill to rewind to that point, and resetting the whole flow from the reset link. <section class="mx-auto max-w-xl px-6 py-14">
<div class="rounded-2xl border border-neutral-200 bg-white p-7">
<h2 class="text-xl font-semibold tracking-tight text-neutral-900">Let us narrow it down</h2>
<p class="mt-1.5 text-sm text-neutral-500">Three questions at most. Nothing is sent anywhere until you ask for support.</p>
<div class="mt-5 flex flex-wrap items-center gap-2">
<div id="treeCrumbs" class="flex flex-wrap gap-1.5"></div>
<button id="treeReset" class="ml-auto text-xs text-neutral-500 underline-offset-4 hover:text-neutral-900 hover:underline">Start over</button>
</div>
<div id="treeBody" class="mt-6"></div>
</div>
</section> var TREE = {
start: {
question: "What is going wrong?",
answers: [
["A sync is failing", "sync"],
["Numbers look wrong", "numbers"],
["I cannot sign in", "signin"],
],
},
sync: {
question: "What does the sync status say?",
answers: [
["Permission denied", "perm"],
["Timed out", "timeout"],
["Succeeded, but no rows", "empty"],
],
},
numbers: {
question: "Are they wrong everywhere, or in one report?",
answers: [
["One report only", "onereport"],
["Everywhere", "everywhere"],
],
},
signin: {
result: {
title: "Try the account recovery flow",
body: "Five failed attempts locks an account for fifteen minutes. If your workspace uses SSO, the password form will always reject you — use the SSO button instead.",
code: "",
},
},
perm: {
result: {
title: "The warehouse role lost a grant",
body: "This almost always follows a schema change. Re-run the grant statement below as an admin, then retry the sync from the source page.",
code: "GRANT SELECT ON ALL TABLES IN SCHEMA analytics TO northwind_ro;",
},
},
timeout: {
result: {
title: "The query is exceeding the window",
body: "Syncs are cancelled after 15 minutes. Add an incremental cursor on an indexed timestamp column so each run reads only what changed.",
code: "cursor: updated_at, mode: incremental",
},
},
empty: {
result: {
title: "The filter is excluding everything",
body: "A sync that succeeds with zero rows usually has a where clause referencing a column that has since been renamed. Check the source filter, then run a preview.",
code: "",
},
},
onereport: {
result: {
title: "Check the report cache and its filters",
body: "Reports cache for five minutes by default. A pinned date range is the other usual suspect — open the report settings and look at the range selector.",
code: "",
},
},
everywhere: {
result: {
title: "Timezone mismatch between source and workspace",
body: "If every figure is shifted rather than wrong, the source is reporting in UTC while the workspace renders in local time. Set the workspace timezone to match the warehouse.",
code: "",
},
},
};
var body = document.getElementById("treeBody");
var crumbs = document.getElementById("treeCrumbs");
var path = [];
function renderCrumbs() {
crumbs.innerHTML = path
.map(function (step, i) {
return (
'<button data-step="' + i + '" class="crumb rounded-full bg-neutral-100 px-2.5 py-1 text-xs text-neutral-600 transition hover:bg-neutral-200">' +
step.label +
"</button>"
);
})
.join("");
Array.prototype.slice.call(crumbs.querySelectorAll(".crumb")).forEach(function (pill) {
pill.addEventListener("click", function () {
// Rewinding drops everything after the pill that was clicked.
var index = Number(pill.dataset.step);
var node = path[index].from;
path = path.slice(0, index);
render(node);
});
});
}
function render(key) {
var node = TREE[key];
renderCrumbs();
if (node.result) {
body.innerHTML =
'<div class="rounded-xl border border-green-200 bg-green-50 p-5">' +
'<p class="text-sm font-semibold text-green-900">' + node.result.title + "</p>" +
'<p class="mt-2 text-sm leading-relaxed text-green-800">' + node.result.body + "</p>" +
(node.result.code
? '<pre class="mt-3 overflow-x-auto rounded-lg bg-neutral-900 p-3 font-mono text-xs text-neutral-200">' + node.result.code + "</pre>"
: "") +
"</div>" +
'<div class="mt-4 flex flex-wrap gap-2">' +
'<button class="rounded-xl bg-neutral-900 px-4 py-2.5 text-sm font-medium text-white">That fixed it</button>' +
'<button class="rounded-xl border border-neutral-200 px-4 py-2.5 text-sm font-medium text-neutral-700">Contact support</button>' +
"</div>";
return;
}
body.innerHTML =
'<p class="text-sm font-medium text-neutral-900">' + node.question + "</p>" +
'<div class="mt-3 space-y-2">' +
node.answers
.map(function (answer) {
return (
'<button data-next="' + answer[1] + '" data-label="' + answer[0] + '" class="answer flex w-full items-center justify-between gap-3 rounded-xl border border-neutral-200 px-4 py-3 text-left text-sm text-neutral-700 transition hover:border-neutral-400 hover:bg-neutral-50">' +
"<span>" + answer[0] + "</span><span class=\"text-neutral-300\">›</span>" +
"</button>"
);
})
.join("") +
"</div>";
Array.prototype.slice.call(body.querySelectorAll(".answer")).forEach(function (button) {
button.addEventListener("click", function () {
path.push({ label: button.dataset.label, from: key });
render(button.dataset.next);
});
});
}
document.getElementById("treeReset").addEventListener("click", function () {
path = [];
render("start");
});
render("start");