+
No results found
- We could not find any matches for your query. Try searching for different keywords or checking for spelling errors.
+ We could not find any matches for your query. Try searching for different keywords or click "Ask agent" above.
@@ -199,11 +236,22 @@ import BaseLayout from "@/layouts/BaseLayout.astro";
total: number;
hits: SearchHit[];
semantic: boolean;
+ extraction?: {
+ model: string;
+ source: "clairic" | "fastpath";
+ intent: string;
+ contentType?: string;
+ searchQuery: string;
+ latencyMs: number;
+ };
}
const input = document.getElementById("search-input") as HTMLInputElement | null;
const clearBtn = document.getElementById("search-clear-btn");
const statusCount = document.getElementById("search-count-text");
+ const askAgentBtn = document.getElementById("search-mode-ai-btn");
+ const askAgentQuerySpan = document.getElementById("search-ask-agent-query");
+ const clairicPill = document.getElementById("search-clairic-pill");
const stateReady = document.getElementById("state-ready");
const stateLoading = document.getElementById("state-loading");
@@ -225,6 +273,14 @@ import BaseLayout from "@/layouts/BaseLayout.astro";
if (resultsList) resultsList.classList.toggle("hidden", state !== "results");
}
+ askAgentBtn?.addEventListener("click", () => {
+ const q = input?.value.trim() || "";
+ const win = window as unknown as {
+ openAgentAjDrawer?: (prompt?: string) => void;
+ };
+ win.openAgentAjDrawer?.(q || undefined);
+ });
+
function getBadgeInfo(type: string): { label: string; badgeClass: string; iconSvg: string } {
switch (type) {
case "post":
@@ -295,9 +351,13 @@ import BaseLayout from "@/layouts/BaseLayout.astro";
async function performSearch(query: string, type: string) {
const trimmed = query.trim();
+ if (askAgentQuerySpan) {
+ askAgentQuerySpan.textContent = trimmed ? `: "${trimmed}"` : "";
+ }
if (!trimmed || trimmed.length < 2) {
showState("ready");
if (statusCount) statusCount.textContent = "";
+ if (clairicPill) clairicPill.classList.add("hidden");
return;
}
@@ -321,9 +381,14 @@ import BaseLayout from "@/layouts/BaseLayout.astro";
const data = (await res.json()) as ApiResponse;
const hits = data.hits || [];
+ if (clairicPill && data.extraction) {
+ clairicPill.textContent = `⚡ ${data.extraction.source}: ${data.extraction.intent} (${data.extraction.latencyMs}ms)`;
+ clairicPill.classList.remove("hidden");
+ }
+
if (hits.length === 0) {
if (noResultsText) {
- noResultsText.textContent = `No matches found for "${trimmed}". Try different keywords or broad search terms.`;
+ noResultsText.textContent = `No matches found for "${trimmed}". Click "Ask agent" above to ask Agent AJ in the side drawer.`;
}
if (statusCount) {
statusCount.textContent = "0 results found";
@@ -352,6 +417,9 @@ import BaseLayout from "@/layouts/BaseLayout.astro";
if (clearBtn) {
clearBtn.classList.toggle("hidden", !val);
}
+ if (askAgentQuerySpan) {
+ askAgentQuerySpan.textContent = val.trim() ? `: "${val.trim()}"` : "";
+ }
const url = new URL(window.location.href);
if (val.trim()) {
@@ -408,14 +476,21 @@ import BaseLayout from "@/layouts/BaseLayout.astro";
});
});
- // Check if URL has ?q=
+ // Check if URL has ?q= or ?mode=ai
const initialParams = new URLSearchParams(window.location.search);
const initialQuery = initialParams.get("q");
+ const initialMode = initialParams.get("mode");
if (initialQuery && input) {
input.value = initialQuery;
currentQuery = initialQuery;
if (clearBtn) clearBtn.classList.remove("hidden");
performSearch(initialQuery, selectedType);
+ if (initialMode === "ai") {
+ const win = window as unknown as {
+ openAgentAjDrawer?: (prompt?: string) => void;
+ };
+ win.openAgentAjDrawer?.(initialQuery);
+ }
} else {
showState("ready");
}
diff --git a/apps/site/src/styles/global.css b/apps/site/src/styles/global.css
index c5adcbf5..3cccca5c 100644
--- a/apps/site/src/styles/global.css
+++ b/apps/site/src/styles/global.css
@@ -211,3 +211,13 @@
font-size: 0.875rem;
line-height: 1;
}
+
+/* Desktop Right-Side Resizable Ask Agent Drawer layout shift */
+@media (min-width: 768px) {
+ html.aj-drawer-open body {
+ padding-right: var(--aj-drawer-width, 420px);
+ }
+ html.aj-drawer-open #site-header {
+ right: var(--aj-drawer-width, 420px);
+ }
+}
diff --git a/apps/site/tests/tier1-features.test.mjs b/apps/site/tests/tier1-features.test.mjs
index a04b5cd6..7cf45e02 100644
--- a/apps/site/tests/tier1-features.test.mjs
+++ b/apps/site/tests/tier1-features.test.mjs
@@ -1764,5 +1764,122 @@ describe("Tier 1 - Feature Coverage", () => {
"Must strip client-only modal loading skeleton from Markdown",
);
});
+
+ it("7.20 Agent AJ /api/chat integrates Cloudflare Clairic (@cf/cloudflare/clairic) extraction cascade and fastpath fallback", async () => {
+ const chatModule = loadModule("src/pages/api/chat.ts");
+ assert.equal(chatModule.CLAIRIC_MODEL_ID, "@cf/cloudflare/clairic");
+ assert.equal(typeof chatModule.extractIntentWithClairic, "function");
+
+ // 1. Verify GET /api/chat advertises Clairic extraction model
+ const statusRes = await chatModule.GET({
+ request: new Request("https://codingcat.dev/api/chat"),
+ });
+ assert.equal(statusRes.status, 200);
+ const statusData = await statusRes.json();
+ assert.equal(statusData.extractionModel, "@cf/cloudflare/clairic");
+ assert.equal(statusData.cascadeArchitecture, "extract-route-synthesize");
+
+ // 2. Verify Clairic extraction with mocked Workers AI binding
+ let invokedModel = null;
+ let invokedPayload = null;
+ const mockAiBinding = {
+ async run(modelId, payload) {
+ invokedModel = modelId;
+ invokedPayload = payload;
+ return {
+ answers: {
+ intent: {
+ value: "topic_search",
+ confidence: 0.96,
+ p_null: 0.01,
+ },
+ content_type: {
+ value: "podcast",
+ confidence: 0.92,
+ p_null: 0.02,
+ },
+ search_query: {
+ value: "Sanity CMS",
+ confidence: 0.98,
+ p_null: 0.01,
+ },
+ limit: {
+ value: 4,
+ confidence: 0.89,
+ p_null: 0.05,
+ },
+ },
+ };
+ },
+ };
+
+ const extracted = await chatModule.extractIntentWithClairic(
+ "Find me 4 podcast episodes about Sanity CMS",
+ mockAiBinding,
+ );
+ assert.equal(invokedModel, "@cf/cloudflare/clairic");
+ assert.equal(
+ invokedPayload.state,
+ "Find me 4 podcast episodes about Sanity CMS",
+ );
+ assert.ok(invokedPayload.fields.intent);
+ assert.equal(extracted.source, "clairic");
+ assert.equal(extracted.intent, "topic_search");
+ assert.equal(extracted.contentType, "podcast");
+ assert.equal(extracted.searchQuery, "Sanity CMS");
+ assert.equal(extracted.limit, 4);
+ assert.equal(extracted.confidence, 0.96);
+
+ // 3. Verify deterministic fastpath fallback when AI binding is absent
+ const fastRecent = await chatModule.extractIntentWithClairic(
+ "What is the latest podcast episode?",
+ );
+ assert.equal(fastRecent.source, "fastpath");
+ assert.equal(fastRecent.intent, "recent_content");
+ assert.equal(fastRecent.contentType, "podcast");
+
+ const fastJoke = await chatModule.extractIntentWithClairic(
+ "Tell me a programming dad joke!",
+ );
+ assert.equal(fastJoke.source, "fastpath");
+ assert.equal(fastJoke.intent, "joke_or_kb");
+
+ // 4. Verify Search API and Search/Chat Modals expose Clairic telemetry & Search <-> AI switcher
+ const searchApiSource = fs.readFileSync(
+ path.resolve(SRC_DIR, "pages/api/search.ts"),
+ "utf-8",
+ );
+ assert.ok(searchApiSource.includes("extractIntentWithClairic"));
+ assert.ok(searchApiSource.includes("x-aj-extraction-source"));
+
+ const searchModalSource = fs.readFileSync(
+ path.resolve(SRC_DIR, "components/SearchModal.astro"),
+ "utf-8",
+ );
+ assert.ok(searchModalSource.includes('closedby="any"'));
+ assert.ok(searchModalSource.includes("modal-mode-ai"));
+ assert.ok(searchModalSource.includes("RECENTLY ASKED"));
+ assert.ok(searchModalSource.includes("RECENTLY VIEWED"));
+ assert.ok(searchModalSource.includes("modal-clairic-badge"));
+
+ const chatDrawerSource = fs.readFileSync(
+ path.resolve(SRC_DIR, "components/ChatModal.astro"),
+ "utf-8",
+ );
+ assert.ok(chatDrawerSource.includes("aj-agent-drawer"));
+ assert.ok(chatDrawerSource.includes("aj-drawer-resize-handle"));
+ assert.ok(chatDrawerSource.includes("cc_aj_drawer_open"));
+ assert.ok(
+ !chatDrawerSource.includes('id="ai-chat-open-btn"'),
+ "Must remove floating bottom-right Ask Agent AJ button",
+ );
+
+ const searchPageSource = fs.readFileSync(
+ path.resolve(SRC_DIR, "pages/search.astro"),
+ "utf-8",
+ );
+ assert.ok(searchPageSource.includes("search-mode-ai-btn"));
+ assert.ok(searchPageSource.includes("search-clairic-pill"));
+ });
});
});