|
37 | 37 | <p class="text-[11px] text-muted-foreground">Grounded in Sanity Content Lake • Clairic & Workers AI</p> |
38 | 38 | </div> |
39 | 39 | </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> |
51 | 65 | </header> |
52 | 66 |
|
53 | 67 | <!-- Messages Container --> |
|
191 | 205 | <script> |
192 | 206 | const openBtn = document.getElementById("ai-chat-open-btn"); |
193 | 207 | const closeBtn = document.getElementById("ai-chat-close-btn"); |
| 208 | + const switchSearchBtn = document.getElementById("ai-chat-switch-search-btn"); |
194 | 209 | const modal = document.getElementById("ai-chat-modal"); |
195 | 210 | const form = document.getElementById("ai-chat-form") as HTMLFormElement | null; |
196 | 211 | const input = document.getElementById("ai-chat-input") as HTMLInputElement | null; |
|
200 | 215 |
|
201 | 216 | let isStreaming = false; |
202 | 217 |
|
203 | | - function openModal() { |
| 218 | + function openModal(initialPrompt?: string) { |
204 | 219 | if (modal) { |
205 | 220 | 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 | + } |
207 | 227 | } |
208 | 228 | } |
209 | 229 |
|
|
213 | 233 | } |
214 | 234 | } |
215 | 235 |
|
216 | | - openBtn?.addEventListener("click", openModal); |
| 236 | + (window as unknown as { openAgentAjModal?: (prompt?: string) => void }).openAgentAjModal = openModal; |
| 237 | + |
| 238 | + openBtn?.addEventListener("click", () => openModal()); |
217 | 239 | 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 | + }); |
218 | 258 |
|
219 | 259 | modal?.addEventListener("click", (e) => { |
220 | 260 | if (e.target === modal) { |
|
498 | 538 | container.innerHTML = html + cardsHtml; |
499 | 539 | } |
500 | 540 |
|
501 | | - function appendMessage(role: "user" | "assistant", initialText = ""): HTMLElement { |
| 541 | + function appendMessage( |
| 542 | + role: "user" | "assistant", |
| 543 | + initialText = "", |
| 544 | + ): { contentEl: HTMLElement; telemetryEl?: HTMLElement } { |
502 | 545 | if (welcomeContainer) { |
503 | 546 | welcomeContainer.classList.add("hidden"); |
504 | 547 | } |
|
510 | 553 | : "mr-auto bg-muted/60 text-foreground rounded-2xl rounded-tl-xs px-4 py-2.5 border border-border" |
511 | 554 | }`; |
512 | 555 |
|
| 556 | + let telemetryEl: HTMLElement | undefined; |
| 557 | + |
513 | 558 | 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 | + |
514 | 562 | 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"; |
516 | 564 | 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); |
518 | 572 | } |
519 | 573 |
|
520 | 574 | const contentDiv = document.createElement("div"); |
|
525 | 579 | messagesContainer?.appendChild(msgDiv); |
526 | 580 | messagesContainer?.scrollTo({ top: messagesContainer.scrollHeight, behavior: "smooth" }); |
527 | 581 |
|
528 | | - return contentDiv; |
| 582 | + return { contentEl: contentDiv, telemetryEl }; |
529 | 583 | } |
530 | 584 |
|
531 | 585 | form?.addEventListener("submit", async (e) => { |
|
539 | 593 | if (sendBtn) sendBtn.disabled = true; |
540 | 594 |
|
541 | 595 | appendMessage("user", prompt); |
542 | | - const assistantContent = appendMessage("assistant", "Thinking..."); |
| 596 | + const { contentEl: assistantContent, telemetryEl } = appendMessage( |
| 597 | + "assistant", |
| 598 | + "Thinking...", |
| 599 | + ); |
543 | 600 |
|
544 | 601 | try { |
545 | 602 | const res = await fetch("/api/chat", { |
|
548 | 605 | body: JSON.stringify({ prompt }), |
549 | 606 | }); |
550 | 607 |
|
| 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 | + |
551 | 617 | if (!res.ok) { |
552 | 618 | const errorJson = (await res.json().catch(() => ({}))) as { |
553 | 619 | error?: string; |
|
0 commit comments