From 12359e5631930981550fac2ad2f6f8c44efcdcb7 Mon Sep 17 00:00:00 2001 From: Danny Neira <16809145+dannyneira@users.noreply.github.com> Date: Thu, 22 Oct 2026 10:20:42 -0600 Subject: [PATCH 01/18] Add native Kapa conversation history Co-Authored-By: Warp --- package-lock.json | 43 ++------ package.json | 3 +- src/components/KapaChatLauncher.css | 120 +++++++++++++++++++++ src/components/KapaChatLauncher.tsx | 116 +++++++++++++++++++- src/lib/kapa-conversation-history.mjs | 27 +++++ src/lib/kapa-conversation-history.test.mjs | 41 +++++++ 6 files changed, 313 insertions(+), 37 deletions(-) create mode 100644 src/lib/kapa-conversation-history.mjs create mode 100644 src/lib/kapa-conversation-history.test.mjs diff --git a/package-lock.json b/package-lock.json index 02a7fe97e..179036a6b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@astrojs/sitemap": "^3.7.2", "@astrojs/starlight": "^0.41.4", "@astrojs/vercel": "^11.0.3", - "@kapaai/react-sdk": "^0.9.2", + "@kapaai/react-sdk": "^0.17.0", "@radix-ui/react-popover": "^1.1.15", "@vercel/analytics": "^2.0.1", "@vercel/speed-insights": "^2.0.0", @@ -1244,15 +1244,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/runtime": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", - "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/template": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@babel/template/-/template-8.0.0.tgz", @@ -2206,25 +2197,11 @@ "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", "license": "MIT" }, - "node_modules/@hcaptcha/loader": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@hcaptcha/loader/-/loader-2.3.0.tgz", - "integrity": "sha512-i4lnNxKBe+COf3R1nFZEWaZoHIoJjvDgWqvcNrdZq8ehoSNMN6KVZ56dcQ02qKie2h3+BkbkwlJA9DOIuLlK/g==", - "license": "MIT" - }, "node_modules/@hcaptcha/react-hcaptcha": { - "version": "1.17.4", - "resolved": "https://registry.npmjs.org/@hcaptcha/react-hcaptcha/-/react-hcaptcha-1.17.4.tgz", - "integrity": "sha512-rIvgesG1N7SS9sAYYHFoWm+nXqRrxq7RcA9z2pKkDWV+S1GdfmrTNYA1aPyVWVe3eowphTCwyDJvl97Swwy0mw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.17.9", - "@hcaptcha/loader": "^2.3.0" - }, - "peerDependencies": { - "react": ">= 16.3.0", - "react-dom": ">= 16.3.0" - } + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@hcaptcha/react-hcaptcha/-/react-hcaptcha-2.2.0.tgz", + "integrity": "sha512-/HrPgNLRrJziLrh2s+qFRO33ubBSuHn9sdNsQcP0wQElpORxlpR6wnq3X9j/bZGd+uQtYIvPl4QSU2la+Jl9Hg==", + "license": "MIT" }, "node_modules/@iconify/types": { "version": "2.0.0", @@ -2849,15 +2826,15 @@ } }, "node_modules/@kapaai/react-sdk": { - "version": "0.9.10", - "resolved": "https://registry.npmjs.org/@kapaai/react-sdk/-/react-sdk-0.9.10.tgz", - "integrity": "sha512-osQyFgBJmhNM207MpB0aZbjs2kjNHjEJbNc5YEReJuyFE72Iydn9CoZGxox2GYRgMIlQ7wljHCzgQqkvR8lXqA==", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@kapaai/react-sdk/-/react-sdk-0.17.0.tgz", + "integrity": "sha512-wBqknpqI++w4/ttbdZl8Hf+PBk1Jm+Z3J11S/SwVYViW5xyD5JKn/4CwbjR6FTMr5WLCznjRq28LOH2Ot21IRQ==", "license": "MIT", "dependencies": { "@fingerprintjs/fingerprintjs-pro-react": "^2.7.0", "@fingerprintjs/fingerprintjs-pro-spa": "^1.3.0", - "@hcaptcha/react-hcaptcha": "^1.12.0", - "js-cookie": "^3.0.5", + "@hcaptcha/react-hcaptcha": "^2.0.2", + "js-cookie": "^3.0.7", "tldts": "^7.0.7" }, "peerDependencies": { diff --git a/package.json b/package.json index d56eb55f6..6d07c8dd8 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "seo:audit": "node scripts/seo-audit.mjs", "test:heading-tocs": "node scripts/validate-heading-tocs.mjs", "test:heading-vars": "node --test src/plugins/heading-vars-core.test.mjs", + "test:kapa-history": "node --test src/lib/kapa-conversation-history.test.mjs", "test:kapa-webmcp": "node --test src/lib/kapa-webmcp.test.mjs", "test:homepage-json-ld": "node scripts/validate-homepage-json-ld.mjs", "postinstall": "patch-package" @@ -32,7 +33,7 @@ "@astrojs/sitemap": "^3.7.2", "@astrojs/starlight": "^0.41.4", "@astrojs/vercel": "^11.0.3", - "@kapaai/react-sdk": "^0.9.2", + "@kapaai/react-sdk": "^0.17.0", "@radix-ui/react-popover": "^1.1.15", "@vercel/analytics": "^2.0.1", "@vercel/speed-insights": "^2.0.0", diff --git a/src/components/KapaChatLauncher.css b/src/components/KapaChatLauncher.css index fd70b9fd4..31058d057 100644 --- a/src/components/KapaChatLauncher.css +++ b/src/components/KapaChatLauncher.css @@ -325,6 +325,122 @@ gap: 1rem; } +.sl-kapa-history { + display: flex; + flex: 1; + flex-direction: column; + gap: 1rem; +} + +.sl-kapa-history__header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1rem; +} + +.sl-kapa-history__header h2, +.sl-kapa-history__header p, +.sl-kapa-history__empty { + margin: 0; +} + +.sl-kapa-history__header h2 { + color: var(--sl-color-white); + font-size: var(--sl-text-lg); + line-height: 1.3; +} + +.sl-kapa-history__header p, +.sl-kapa-history__empty { + margin-top: 0.375rem; + color: var(--sl-color-gray-3); + font-size: var(--sl-text-xs); + line-height: 1.45; +} + +.sl-kapa-history__clear { + flex: none; + min-height: 2rem; + padding: 0.375rem 0.5rem; + border: 1px solid var(--sl-color-gray-5); + border-radius: var(--sl-radius-sm); + background: transparent; + color: var(--sl-color-gray-2); + font: inherit; + font-size: var(--sl-text-xs); + font-weight: 600; + line-height: 1.2; + cursor: pointer; +} + +.sl-kapa-history__clear:hover { + border-color: var(--warp-control-border-hover); + background: var(--warp-control-bg-hover); + color: var(--sl-color-white); +} + +.sl-kapa-history__clear:focus-visible, +.sl-kapa-history__thread:focus-visible { + outline: 2px solid var(--sl-color-accent-high); + outline-offset: 2px; +} + +.sl-kapa-history__empty { + margin: auto 0; + padding: 1rem; + border: 1px dashed var(--sl-color-gray-5); + border-radius: var(--sl-radius-lg); +} + +.sl-kapa-history__list { + display: flex; + flex-direction: column; + gap: 0.5rem; + margin: 0; + padding: 0; + list-style: none; +} + +.sl-kapa-history__thread { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + width: 100%; + min-height: 3rem; + padding: 0.625rem 0.75rem; + border: 1px solid var(--sl-color-gray-5); + border-radius: var(--sl-radius-sm); + background: color-mix(in srgb, var(--sl-color-black), var(--sl-color-gray-6) 28%); + color: var(--sl-color-gray-1); + font: inherit; + font-size: var(--sl-text-sm); + line-height: 1.4; + text-align: left; + cursor: pointer; +} + +.sl-kapa-history__thread:hover, +.sl-kapa-history__thread[aria-current='page'] { + border-color: var(--warp-control-border-hover); + background: var(--warp-control-bg-hover); +} + +.sl-kapa-history__thread > span:first-child { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.sl-kapa-history__active { + flex: none; + color: var(--sl-color-text-accent); + font-size: var(--sl-text-2xs); + font-weight: 600; +} + .sl-kapa-empty-state { margin: auto 0; padding: 1rem; @@ -1088,6 +1204,10 @@ } @media (max-width: 36rem) { + .sl-kapa-history__header { + flex-direction: column; + } + .sl-kapa-meta { flex-direction: column; align-items: flex-start; diff --git a/src/components/KapaChatLauncher.tsx b/src/components/KapaChatLauncher.tsx index 816f333a5..78d61c1fc 100644 --- a/src/components/KapaChatLauncher.tsx +++ b/src/components/KapaChatLauncher.tsx @@ -8,14 +8,25 @@ import { useRef, useState, } from 'react'; -import { CaptchaAction, KapaProvider, useCaptcha, useChat } from '@kapaai/react-sdk'; +import { + CaptchaAction, + KapaProvider, + useCaptcha, + useChat, + useConversationHistory, +} from '@kapaai/react-sdk'; import { PUBLIC_KAPA_INTEGRATION_ID, PUBLIC_KAPA_PROJECT_ID } from 'astro:env/client'; import { isMac, keymatch } from 'keymatch'; import ReactMarkdown from 'react-markdown'; +import { + KAPA_CONVERSATION_HISTORY_OPTIONS, + getConversationHistoryView, +} from '../lib/kapa-conversation-history.mjs'; import { LuExternalLink, LuCheck, LuCopy, + LuHistory, LuLoaderCircle, LuMessageSquare, LuPlug, @@ -51,6 +62,19 @@ type GenericRecord = Record; type HandoffApiSuccess = { message?: string; }; +type ConversationHistoryView = { + title: string; + empty: string; + privacy: string; + clear: string; + canClear: boolean; + isEmpty: boolean; + items: Array<{ + threadId: string; + title: string; + isActive: boolean; + }>; +}; function isObject(value: unknown): value is GenericRecord { return typeof value === 'object' && value !== null; @@ -529,6 +553,7 @@ function ChatSurface({ title, welcomeMessage, autoOpen = false, onNewConversatio const [query, setQuery] = useState(''); const [hasStartedConversation, setHasStartedConversation] = useState(false); const [isAppleDevice, setIsAppleDevice] = useState(false); + const [isHistoryOpen, setIsHistoryOpen] = useState(false); const [storedThreadId, setStoredThreadId] = useState(null); const [downvotedAnswerIds, setDownvotedAnswerIds] = useState>({}); const [handoffEmailInput, setHandoffEmailInput] = useState(''); @@ -552,6 +577,12 @@ function ChatSurface({ title, welcomeMessage, autoOpen = false, onNewConversatio submitQuery, threadId, } = useChat(); + const { + clear: clearConversationHistory, + enabled: isConversationHistoryEnabled, + restoreThread, + threads: conversationHistoryThreads, + } = useConversationHistory(); const { executeCaptcha } = useCaptcha(); useEffect(() => { @@ -654,6 +685,10 @@ function ChatSurface({ title, welcomeMessage, autoOpen = false, onNewConversatio const hasConversation = conversation.length > 0; const isBusy = isGeneratingAnswer || isPreparingAnswer; + const conversationHistory = getConversationHistoryView( + conversationHistoryThreads, + threadId + ) as ConversationHistoryView; const submit = () => { const value = query.trim(); @@ -723,6 +758,19 @@ function ChatSurface({ title, welcomeMessage, autoOpen = false, onNewConversatio setHandoffSuccessMessage(null); }; + const restoreSavedConversation = (savedThreadId: string) => { + closeHandoffForm(); + restoreThread(savedThreadId); + setHasStartedConversation(true); + setIsHistoryOpen(false); + }; + + const clearSavedConversations = () => { + clearConversationHistory(); + localStorage.removeItem('warp_docs_kapa_thread_id'); + setStoredThreadId(null); + }; + const submitSupportHandoff = async (qa: { id?: string | null; question?: string }) => { if (isSubmittingHandoff) return; const userEmail = handoffEmailInput.trim().toLowerCase(); @@ -894,6 +942,20 @@ function ChatSurface({ title, welcomeMessage, autoOpen = false, onNewConversatio