Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add kapa widget #1661

Merged
merged 14 commits into from
Oct 21, 2024
6 changes: 6 additions & 0 deletions app/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,9 @@ onMounted(() => {
</ClientOnly>
</div>
</template>

<style>
#kapa-widget-container {
visibility: hidden;
}
</style>
70 changes: 49 additions & 21 deletions app/composables/useNavigation.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createSharedComposable } from '@vueuse/core'

const _useNavigation = () => {
const nuxtApp = useNuxtApp()
const headerLinks = computed(() => {
const route = useRoute()

Expand Down Expand Up @@ -178,32 +179,59 @@ const _useNavigation = () => {
}]
}]

const searchLinks = computed(() => [...headerLinks.value.map((link) => {
const searchLinks = computed(() => [
{
label: 'Ask AI',
icon: 'i-ph-magic-wand',
to: 'javascript:void(0);',
// @ts-expect-error this is not typed
click: () => nuxtApp.$kapa?.openModal()
},
...headerLinks.value.map((link) => {
// Remove `/docs` and `/enterprise` links from command palette
if (link.search === false) {
return {
label: link.label,
icon: link.icon,
children: link.children
if (link.search === false) {
return {
label: link.label,
icon: link.icon,
children: link.children
}
}
}

return link
}).filter(Boolean), {
label: 'Team',
icon: 'i-ph-users',
to: '/team'
}, {
label: 'Design Kit',
icon: 'i-ph-palette',
to: '/design-kit'
}, {
label: 'Newsletter',
icon: 'i-ph-envelope-simple',
to: '/newsletter'
}])
return link
}).filter(Boolean), {
label: 'Team',
icon: 'i-ph-users',
to: '/team'
}, {
label: 'Design Kit',
icon: 'i-ph-palette',
to: '/design-kit'
}, {
label: 'Newsletter',
icon: 'i-ph-envelope-simple',
to: '/newsletter'
}])

const searchGroups = [{
key: 'ask-ai-search',
label: 'AI',
icon: 'i-ph-magic-wand',
search: async (q) => {
if (!q) {
return []
}

return [{
label: `Ask AI about "${q}"`,
icon: 'i-ph-magic-wand',
to: 'javascript:void(0);',
click() {
// @ts-expect-error this is not typed
useNuxtApp().$kapa.openModal(q)
}
}]
}
}, {
key: 'modules-search',
label: 'Modules',
search: async (q) => {
Expand Down
170 changes: 170 additions & 0 deletions app/plugins/kapa.client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
const kapa = {
'key': 'kapa',
'src': 'https://widget.kapa.ai/kapa-widget.bundle.js',
'data-website-id': 'fb3af718-9db2-440d-9da9-14e6c5fca2aa',
// 'data-button-hide': true,
'data-project-name': 'Nuxt',
'data-project-color': '#00DC82',
'data-button-text-color': '#000000',
'data-project-logo': 'https://nuxt.com/assets/design-kit/icon-black.svg',
'data-modal-image': 'https://nuxt.com/assets/design-kit/icon-green.svg',
'data-button-padding': '0.5rem',
'data-button-width': '5.5rem',
'data-modal-disclaimer': 'This is a custom LLM for answering questions about Nuxt. Answers are based on the contents of the documentation, GitHub information and Stack Overflow articles. Please note that answers are generated by AI and may not be fully accurate, so please use your best judgement.',
'data-user-analytics-fingerprint-enabled': 'true',
'crossorigin': false
} as const

interface OnModalOpenArgs {
mode: 'search' | 'ai'
}

interface OnModalCloseArgs {
mode: 'search' | 'ai'
}

interface OnAskAIQuerySubmitArgs {
threadId: string | null
questionAnswerId: string
question: string
}

interface OnAskAIExampleQuerySubmitArgs {
threadId: string | null
questionAnswerId: string
question: string
}

interface OnAskAIAnswerCompletedArgs {
threadId: string
questionAnswerId: string
question: string
answer: string
conversation: { questionAnswerId: string, question: string, answer: string }[]
}

interface OnAskAIFeedbackSubmitArgs {
reaction: string
comment: {
issue: string
irrelevant: boolean
incorrect: boolean
unaddressed: boolean
}
threadId: string
questionAnswerId: string
question: string
answer: string
conversation: { questionAnswerId: string, question: string, answer: string }[]
}

interface OnAskAILinkClickArgs {
href: string
threadId: string
questionAnswerId: string
question: string
answer: string
}

interface OnAskAISourceClickArgs {
source: {
title: string
subtitle: string
url: string
}
threadId: string
questionAnswerId: string
question: string
answer: string
}

interface OnAskAIAnswerCopyArgs {
threadId: string
questionAnswerId: string
question: string
answer: string
}

interface OnAskAIGenerationStopArgs {
threadId: string | null
question: string
conversation: { questionAnswerId: string, question: string, answer: string }[]
}

interface OnAskAIConversationResetArgs {
threadId: string
conversation: { questionAnswerId: string, question: string, answer: string }[]
}

interface OnModeSwitchArgs {
mode: 'search' | 'ai'
}

interface OnSearchResultsCompletedArgs {
query: string
searchResults: { title: string, subtitle: string, url: string, sourceName: string }[]
}

interface OnSearchResultsShowMoreClickArgs {
query: string
searchResults: { title: string, subtitle: string, url: string, sourceName: string }[]
}

interface OnSearchResultClickArgs {
query: string
searchResult: { title: string, subtitle: string, url: string, sourceName: string }
rank: number
}

interface Kapa {
(event: 'onModalOpen', handler: (args: OnModalOpenArgs) => void): void
(event: 'onModalClose', handler: (args: OnModalCloseArgs) => void): void
(event: 'onAskAIQuerySubmit', handler: (args: OnAskAIQuerySubmitArgs) => void): void
(event: 'onAskAIExampleQuerySubmit', handler: (args: OnAskAIExampleQuerySubmitArgs) => void): void
(event: 'onAskAIAnswerCompleted', handler: (args: OnAskAIAnswerCompletedArgs) => void): void
(event: 'onAskAIFeedbackSubmit', handler: (args: OnAskAIFeedbackSubmitArgs) => void): void
(event: 'onAskAILinkClick', handler: (args: OnAskAILinkClickArgs) => void): void
(event: 'onAskAISourceClick', handler: (args: OnAskAISourceClickArgs) => void): void
(event: 'onAskAIAnswerCopy', handler: (args: OnAskAIAnswerCopyArgs) => void): void
(event: 'onAskAIGenerationStop', handler: (args: OnAskAIGenerationStopArgs) => void): void
(event: 'onAskAIConversationReset', handler: (args: OnAskAIConversationResetArgs) => void): void
(event: 'onModeSwitch', handler: (args: OnModeSwitchArgs) => void): void
(event: 'onSearchResultsCompleted', handler: (args: OnSearchResultsCompletedArgs) => void): void
(event: 'onSearchResultsShowMoreClick', handler: (args: OnSearchResultsShowMoreClickArgs) => void): void
(event: 'onSearchResultClick', handler: (args: OnSearchResultClickArgs) => void): void
}

declare global {
interface Window {
Kapa: Kapa
}
}

export default defineNuxtPlugin((nuxtApp) => {
useScript<{ Kapa: Kapa }>(kapa, {
trigger: 'onNuxtReady',
use() {
return { Kapa: window.Kapa }
}
})

nuxtApp.provide('kapa', {
async openModal(q) {
// @ts-expect-error this is not typed
document.querySelector('#kapa-widget-container button')?.click()
if (q) {
let input = null
let i = 0
do {
input = document.querySelector('#kapa-widget-portal .mantine-Textarea-input')
await new Promise(resolve => setTimeout(resolve, 100))
i++
} while (!input && i < 20)
input.value = q
// await new Promise(resolve => setTimeout(resolve, 50))
// input.dispatchEvent(new Event('input', { bubbles: true }))
// document.querySelector('#kapa-widget-portal button.mantine-ActionIcon-root')?.click()
}
}
})
})
5 changes: 5 additions & 0 deletions nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,11 @@ export default defineNuxtConfig({
}
}
},
icon: {
clientBundle: {
scan: true
}
},
twoslash: {
floatingVueOptions: {
classMarkdown: 'prose prose-primary dark:prose-invert'
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"nuxt-content-twoslash": "0.1.1",
"shiki": "^1.22.0",
"twoslash": "^0.2.12",
"typescript": "^5.6.3",
"vitest": "^2.1.3",
"vue": "^3.5.12",
"vue-tsc": "^2.1.6"
Expand Down
Loading