Skip to content

Commit aeeac77

Browse files
committed
feat(site): add Clairic extraction & Search <-> Ask AI switcher to Search and Agent AJ
1 parent 0dd43b8 commit aeeac77

6 files changed

Lines changed: 943 additions & 154 deletions

File tree

‎apps/site/src/components/ChatModal.astro‎

Lines changed: 85 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -37,17 +37,31 @@
3737
<p class="text-[11px] text-muted-foreground">Grounded in Sanity Content Lake • Clairic & Workers AI</p>
3838
</div>
3939
</div>
40-
<button
41-
type="button"
42-
id="ai-chat-close-btn"
43-
class="rounded-md p-1.5 text-muted-foreground hover:bg-accent hover:text-foreground transition-colors cursor-pointer"
44-
aria-label="Close dialog"
45-
>
46-
<svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
47-
<line x1="18" y1="6" x2="6" y2="18"></line>
48-
<line x1="6" y1="6" x2="18" y2="18"></line>
49-
</svg>
50-
</button>
40+
<div class="flex items-center gap-2">
41+
<button
42+
type="button"
43+
id="ai-chat-switch-search-btn"
44+
class="inline-flex items-center gap-1.5 rounded-lg border border-border bg-card px-2.5 py-1 text-xs font-medium text-muted-foreground hover:bg-accent hover:text-foreground transition-colors cursor-pointer"
45+
title="Switch to Search"
46+
>
47+
<svg class="size-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
48+
<circle cx="11" cy="11" r="8"></circle>
49+
<path d="m21 21-4.3-4.3"></path>
50+
</svg>
51+
<span>Search</span>
52+
</button>
53+
<button
54+
type="button"
55+
id="ai-chat-close-btn"
56+
class="rounded-md p-1.5 text-muted-foreground hover:bg-accent hover:text-foreground transition-colors cursor-pointer"
57+
aria-label="Close dialog"
58+
>
59+
<svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
60+
<line x1="18" y1="6" x2="6" y2="18"></line>
61+
<line x1="6" y1="6" x2="18" y2="18"></line>
62+
</svg>
63+
</button>
64+
</div>
5165
</header>
5266

5367
<!-- Messages Container -->
@@ -191,6 +205,7 @@
191205
<script>
192206
const openBtn = document.getElementById("ai-chat-open-btn");
193207
const closeBtn = document.getElementById("ai-chat-close-btn");
208+
const switchSearchBtn = document.getElementById("ai-chat-switch-search-btn");
194209
const modal = document.getElementById("ai-chat-modal");
195210
const form = document.getElementById("ai-chat-form") as HTMLFormElement | null;
196211
const input = document.getElementById("ai-chat-input") as HTMLInputElement | null;
@@ -200,10 +215,15 @@
200215

201216
let isStreaming = false;
202217

203-
function openModal() {
218+
function openModal(initialPrompt?: string) {
204219
if (modal) {
205220
modal.classList.remove("hidden");
206-
input?.focus();
221+
if (initialPrompt && input) {
222+
input.value = initialPrompt;
223+
form?.dispatchEvent(new Event("submit", { cancelable: true }));
224+
} else {
225+
input?.focus();
226+
}
207227
}
208228
}
209229

@@ -213,8 +233,28 @@
213233
}
214234
}
215235

216-
openBtn?.addEventListener("click", openModal);
236+
(window as unknown as { openAgentAjModal?: (prompt?: string) => void }).openAgentAjModal = openModal;
237+
238+
openBtn?.addEventListener("click", () => openModal());
217239
closeBtn?.addEventListener("click", closeModal);
240+
switchSearchBtn?.addEventListener("click", () => {
241+
const draft = input?.value.trim() || "";
242+
closeModal();
243+
const searchDialog = document.getElementById("search-modal") as HTMLDialogElement | null;
244+
const searchInput = document.getElementById("modal-search-input") as HTMLInputElement | null;
245+
if (searchDialog) {
246+
if (!searchDialog.open) {
247+
searchDialog.showModal();
248+
}
249+
if (searchInput) {
250+
if (draft) {
251+
searchInput.value = draft;
252+
searchInput.dispatchEvent(new Event("input", { bubbles: true }));
253+
}
254+
searchInput.focus();
255+
}
256+
}
257+
});
218258

219259
modal?.addEventListener("click", (e) => {
220260
if (e.target === modal) {
@@ -498,7 +538,10 @@
498538
container.innerHTML = html + cardsHtml;
499539
}
500540

501-
function appendMessage(role: "user" | "assistant", initialText = ""): HTMLElement {
541+
function appendMessage(
542+
role: "user" | "assistant",
543+
initialText = "",
544+
): { contentEl: HTMLElement; telemetryEl?: HTMLElement } {
502545
if (welcomeContainer) {
503546
welcomeContainer.classList.add("hidden");
504547
}
@@ -510,11 +553,22 @@
510553
: "mr-auto bg-muted/60 text-foreground rounded-2xl rounded-tl-xs px-4 py-2.5 border border-border"
511554
}`;
512555

556+
let telemetryEl: HTMLElement | undefined;
557+
513558
if (role === "assistant") {
559+
const badgeRow = document.createElement("div");
560+
badgeRow.className = "flex flex-wrap items-center justify-between gap-2 mb-0.5";
561+
514562
const badge = document.createElement("div");
515-
badge.className = "flex items-center gap-1.5 text-[11px] font-semibold text-primary mb-0.5";
563+
badge.className = "flex items-center gap-1.5 text-[11px] font-semibold text-primary";
516564
badge.innerHTML = `<img src="/icon.svg" alt="" class="size-3.5 object-contain" width="14" height="14" /><span>Agent AJ</span>`;
517-
msgDiv.appendChild(badge);
565+
566+
telemetryEl = document.createElement("span");
567+
telemetryEl.className = "hidden rounded-full border border-primary/30 bg-primary/10 px-2 py-0.5 font-mono text-[10px] font-medium text-primary";
568+
569+
badgeRow.appendChild(badge);
570+
badgeRow.appendChild(telemetryEl);
571+
msgDiv.appendChild(badgeRow);
518572
}
519573

520574
const contentDiv = document.createElement("div");
@@ -525,7 +579,7 @@
525579
messagesContainer?.appendChild(msgDiv);
526580
messagesContainer?.scrollTo({ top: messagesContainer.scrollHeight, behavior: "smooth" });
527581

528-
return contentDiv;
582+
return { contentEl: contentDiv, telemetryEl };
529583
}
530584

531585
form?.addEventListener("submit", async (e) => {
@@ -539,7 +593,10 @@
539593
if (sendBtn) sendBtn.disabled = true;
540594

541595
appendMessage("user", prompt);
542-
const assistantContent = appendMessage("assistant", "Thinking...");
596+
const { contentEl: assistantContent, telemetryEl } = appendMessage(
597+
"assistant",
598+
"Thinking...",
599+
);
543600

544601
try {
545602
const res = await fetch("/api/chat", {
@@ -548,6 +605,15 @@
548605
body: JSON.stringify({ prompt }),
549606
});
550607

608+
const extSource = res.headers.get("x-aj-extraction-source");
609+
const extIntent = res.headers.get("x-aj-intent");
610+
const extLatency = res.headers.get("x-aj-extraction-latency-ms");
611+
const cascadeMode = res.headers.get("x-aj-cascade-mode");
612+
if (telemetryEl && extSource && extIntent) {
613+
telemetryEl.textContent = `⚡ ${extSource}: ${extIntent} (${extLatency ?? "0"}ms • ${cascadeMode ?? "cascade"})`;
614+
telemetryEl.classList.remove("hidden");
615+
}
616+
551617
if (!res.ok) {
552618
const errorJson = (await res.json().catch(() => ({}))) as {
553619
error?: string;

0 commit comments

Comments
 (0)