mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-14 12:14:41 +01:00
docs(app): implement AI search
This commit is contained in:
@@ -12,7 +12,6 @@ const { data: files } = useLazyAsyncData('search', () => queryCollectionSearchSe
|
||||
})
|
||||
|
||||
const links = useLinks()
|
||||
const searchLinks = useSearchLinks()
|
||||
const color = computed(() => colorMode.value === 'dark' ? (colors as any)[appConfig.ui.colors.neutral][900] : 'white')
|
||||
const radius = computed(() => `:root { --ui-radius: ${appConfig.theme.radius}rem; }`)
|
||||
const blackAsPrimary = computed(() => appConfig.theme.blackAsPrimary ? `:root { --ui-primary: black; } .dark { --ui-primary: white; }` : ':root {}')
|
||||
@@ -42,7 +41,6 @@ useServerSeoMeta({
|
||||
|
||||
useFaviconFromTheme()
|
||||
|
||||
const { frameworks, modules } = useSharedData()
|
||||
const { mappedNavigation, filteredNavigation } = useContentNavigation(navigation)
|
||||
|
||||
provide('navigation', mappedNavigation)
|
||||
@@ -65,23 +63,7 @@ provide('navigation', mappedNavigation)
|
||||
<template v-if="!route.path.startsWith('/examples')">
|
||||
<Footer />
|
||||
|
||||
<ClientOnly>
|
||||
<LazyUContentSearch
|
||||
:links="searchLinks"
|
||||
:files="files"
|
||||
:groups="[{
|
||||
id: 'framework',
|
||||
label: 'Framework',
|
||||
items: frameworks
|
||||
}, {
|
||||
id: 'module',
|
||||
label: 'Module',
|
||||
items: modules
|
||||
}]"
|
||||
:navigation="filteredNavigation"
|
||||
:fuse="{ resultLimit: 100 }"
|
||||
/>
|
||||
</ClientOnly>
|
||||
<Search :files="files" :navigation="filteredNavigation" />
|
||||
</template>
|
||||
</UApp>
|
||||
</template>
|
||||
|
||||
199
docs/app/components/Search.client.vue
Normal file
199
docs/app/components/Search.client.vue
Normal file
@@ -0,0 +1,199 @@
|
||||
<script setup lang="ts">
|
||||
import type { DefineComponent } from 'vue'
|
||||
import type { ContentNavigationItem } from '@nuxt/content'
|
||||
import { useChat } from '@ai-sdk/vue'
|
||||
import ProseStreamPre from './prose/PreStream.vue'
|
||||
|
||||
const components = {
|
||||
pre: ProseStreamPre as unknown as DefineComponent
|
||||
}
|
||||
|
||||
interface ContentSearchFile {
|
||||
id: string
|
||||
title: string
|
||||
titles: string[]
|
||||
level: number
|
||||
content: string
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
files?: ContentSearchFile[]
|
||||
navigation?: ContentNavigationItem[]
|
||||
}>()
|
||||
|
||||
const { frameworks, modules } = useSharedData()
|
||||
const { messages, input, handleSubmit, status, error, reload, setMessages } = useChat({
|
||||
maxSteps: 2
|
||||
})
|
||||
|
||||
const ai = ref(false)
|
||||
const searchTerm = ref('')
|
||||
|
||||
const links = computed(() => [{
|
||||
label: 'Ask AI',
|
||||
icon: 'i-lucide-bot',
|
||||
onSelect: (e: any) => {
|
||||
e.preventDefault()
|
||||
|
||||
ai.value = true
|
||||
}
|
||||
}, {
|
||||
label: 'Docs',
|
||||
icon: 'i-lucide-square-play',
|
||||
to: '/getting-started'
|
||||
}, {
|
||||
label: 'Components',
|
||||
icon: 'i-lucide-square-code',
|
||||
to: '/components'
|
||||
}, {
|
||||
icon: 'i-lucide-sparkles',
|
||||
label: 'Pro > Features',
|
||||
description: 'A collection of premium Vue components.',
|
||||
to: '/pro'
|
||||
}, {
|
||||
icon: 'i-lucide-credit-card',
|
||||
label: 'Pro > Pricing',
|
||||
description: 'Free in development, buy when ready to launch.',
|
||||
to: '/pro/pricing'
|
||||
}, {
|
||||
icon: 'i-lucide-panels-top-left',
|
||||
label: 'Pro > Templates',
|
||||
description: 'Official templates made with Nuxt UI Pro.',
|
||||
to: '/pro/templates'
|
||||
}, {
|
||||
icon: 'i-lucide-circle-check',
|
||||
label: 'Pro > Activate',
|
||||
description: 'Enable Nuxt UI Pro in your production projects.',
|
||||
to: '/pro/activate'
|
||||
}, {
|
||||
label: 'Figma',
|
||||
icon: 'i-simple-icons-figma',
|
||||
to: '/figma'
|
||||
}, {
|
||||
icon: 'i-lucide-presentation',
|
||||
label: 'Community > Showcase',
|
||||
description: 'Check out some of the amazing projects built with Nuxt UI.',
|
||||
to: '/showcase'
|
||||
}, {
|
||||
label: 'Community > Contribution',
|
||||
description: 'A comprehensive guide on contributing to Nuxt UI, including project structure, development workflow, and best practices.',
|
||||
icon: 'i-lucide-git-pull-request-arrow',
|
||||
to: '/getting-started/contribution'
|
||||
}, {
|
||||
label: 'Community > Roadmap',
|
||||
description: 'Track our development progress in real-time.',
|
||||
icon: 'i-lucide-map',
|
||||
to: '/roadmap'
|
||||
}, {
|
||||
label: 'Community > Devtools',
|
||||
description: 'Integrate Nuxt UI with Nuxt Devtools with Compodium.',
|
||||
icon: 'i-lucide-code',
|
||||
to: 'https://github.com/romhml/compodium',
|
||||
target: '_blank'
|
||||
}, {
|
||||
label: 'Community > Team',
|
||||
description: 'Meet the team behind Nuxt UI.',
|
||||
icon: 'i-lucide-users',
|
||||
to: '/team'
|
||||
}, {
|
||||
label: 'Releases',
|
||||
icon: 'i-lucide-rocket',
|
||||
to: 'https://github.com/nuxt/ui/releases',
|
||||
target: '_blank'
|
||||
}])
|
||||
|
||||
const groups = computed(() => [{
|
||||
id: 'ai',
|
||||
label: 'AI',
|
||||
ignoreFilter: true,
|
||||
items: [{
|
||||
label: searchTerm.value ? `Ask Nuxt AI for “${searchTerm.value}”` : 'Ask Nuxt AI',
|
||||
icon: 'i-lucide-bot',
|
||||
onSelect: (e: any) => {
|
||||
e.preventDefault()
|
||||
|
||||
ai.value = true
|
||||
|
||||
if (searchTerm.value) {
|
||||
setMessages([{
|
||||
id: '1',
|
||||
role: 'user',
|
||||
content: searchTerm.value
|
||||
}])
|
||||
|
||||
reload()
|
||||
}
|
||||
}
|
||||
}]
|
||||
}, {
|
||||
id: 'framework',
|
||||
label: 'Framework',
|
||||
items: frameworks.value
|
||||
}, {
|
||||
id: 'module',
|
||||
label: 'Module',
|
||||
items: modules.value
|
||||
}])
|
||||
|
||||
function onClose(e: Event) {
|
||||
console.log('onClose')
|
||||
e.preventDefault()
|
||||
|
||||
ai.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LazyUContentSearch
|
||||
v-model:search-term="searchTerm"
|
||||
:links="links"
|
||||
:files="files"
|
||||
:groups="groups"
|
||||
:navigation="navigation"
|
||||
:fuse="{ resultLimit: 100 }"
|
||||
>
|
||||
<template v-if="ai" #content>
|
||||
<UChatPalette>
|
||||
<UChatMessages
|
||||
:messages="messages"
|
||||
:status="status"
|
||||
:user="{ side: 'left', variant: 'naked', avatar: { src: 'https://github.com/benjamincanac.png' } }"
|
||||
:assistant="{ icon: 'i-lucide-bot' }"
|
||||
>
|
||||
<template #content="{ message }">
|
||||
<MDCCached
|
||||
v-if="message.toolInvocations?.[0]?.state === 'result'"
|
||||
:value="message.toolInvocations?.[0]?.result"
|
||||
:cache-key="message.id"
|
||||
unwrap="p"
|
||||
:components="components"
|
||||
:parser-options="{ highlight: false }"
|
||||
/>
|
||||
<MDCCached
|
||||
v-else-if="message.content.length > 0"
|
||||
:value="message.content"
|
||||
:cache-key="message.id"
|
||||
unwrap="p"
|
||||
:components="components"
|
||||
:parser-options="{ highlight: false }"
|
||||
/>
|
||||
<span v-else class="italic font-light">
|
||||
Searching documentation...
|
||||
</span>
|
||||
</template>
|
||||
</UChatMessages>
|
||||
|
||||
<template #prompt>
|
||||
<UChatPrompt
|
||||
v-model="input"
|
||||
icon="i-lucide-search"
|
||||
variant="naked"
|
||||
:error="error"
|
||||
@submit="handleSubmit"
|
||||
@close="onClose"
|
||||
/>
|
||||
</template>
|
||||
</UChatPalette>
|
||||
</template>
|
||||
</LazyUContentSearch>
|
||||
</template>
|
||||
44
docs/app/components/prose/PreStream.vue
Normal file
44
docs/app/components/prose/PreStream.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
import { ShikiCachedRenderer } from 'shiki-stream/vue'
|
||||
|
||||
const colorMode = useColorMode()
|
||||
const highlighter = await useHighlighter()
|
||||
const props = defineProps<{
|
||||
code: string
|
||||
language: string
|
||||
class?: string
|
||||
meta?: string
|
||||
}>()
|
||||
const trimmedCode = computed(() => {
|
||||
return props.code.trim().replace(/`+$/, '')
|
||||
})
|
||||
const lang = computed(() => {
|
||||
switch (props.language) {
|
||||
case 'vue':
|
||||
return 'vue'
|
||||
case 'javascript':
|
||||
return 'js'
|
||||
case 'typescript':
|
||||
return 'ts'
|
||||
case 'css':
|
||||
return 'css'
|
||||
default:
|
||||
return props.language
|
||||
}
|
||||
})
|
||||
const key = computed(() => {
|
||||
return `${lang.value}-${colorMode.value}`
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ProsePre v-bind="props">
|
||||
<ShikiCachedRenderer
|
||||
:key="key"
|
||||
:highlighter="highlighter"
|
||||
:code="trimmedCode"
|
||||
:lang="lang"
|
||||
:theme="colorMode.value === 'dark' ? 'material-theme-palenight' : 'material-theme-lighter'"
|
||||
/>
|
||||
</ProsePre>
|
||||
</template>
|
||||
21
docs/app/composables/useHighlighter.ts
Normal file
21
docs/app/composables/useHighlighter.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { createHighlighter, type HighlighterGeneric } from 'shiki'
|
||||
import { createJavaScriptRegexEngine } from 'shiki/engine-javascript.mjs'
|
||||
|
||||
let highlighter: HighlighterGeneric<any, any> | null = null
|
||||
|
||||
let promise: Promise<HighlighterGeneric<any, any>> | null = null
|
||||
|
||||
export const useHighlighter = async () => {
|
||||
if (!promise) {
|
||||
promise = createHighlighter({
|
||||
langs: ['vue', 'js', 'ts', 'css', 'html', 'json', 'yaml', 'markdown', 'bash'],
|
||||
themes: ['material-theme-palenight', 'material-theme-lighter'],
|
||||
engine: createJavaScriptRegexEngine()
|
||||
})
|
||||
}
|
||||
if (!highlighter) {
|
||||
highlighter = await promise
|
||||
}
|
||||
|
||||
return highlighter
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
export function useSearchLinks() {
|
||||
return [{
|
||||
label: 'Docs',
|
||||
icon: 'i-lucide-square-play',
|
||||
to: '/getting-started'
|
||||
}, {
|
||||
label: 'Components',
|
||||
icon: 'i-lucide-square-code',
|
||||
to: '/components'
|
||||
}, {
|
||||
icon: 'i-lucide-sparkles',
|
||||
label: 'Pro > Features',
|
||||
description: 'A collection of premium Vue components.',
|
||||
to: '/pro'
|
||||
}, {
|
||||
icon: 'i-lucide-credit-card',
|
||||
label: 'Pro > Pricing',
|
||||
description: 'Free in development, buy when ready to launch.',
|
||||
to: '/pro/pricing'
|
||||
}, {
|
||||
icon: 'i-lucide-panels-top-left',
|
||||
label: 'Pro > Templates',
|
||||
description: 'Official templates made with Nuxt UI Pro.',
|
||||
to: '/pro/templates'
|
||||
}, {
|
||||
icon: 'i-lucide-circle-check',
|
||||
label: 'Pro > Activate',
|
||||
description: 'Enable Nuxt UI Pro in your production projects.',
|
||||
to: '/pro/activate'
|
||||
}, {
|
||||
label: 'Figma',
|
||||
icon: 'i-simple-icons-figma',
|
||||
to: '/figma'
|
||||
}, {
|
||||
icon: 'i-lucide-presentation',
|
||||
label: 'Community > Showcase',
|
||||
description: 'Check out some of the amazing projects built with Nuxt UI.',
|
||||
to: '/showcase'
|
||||
}, {
|
||||
label: 'Community > Contribution',
|
||||
description: 'A comprehensive guide on contributing to Nuxt UI, including project structure, development workflow, and best practices.',
|
||||
icon: 'i-lucide-git-pull-request-arrow',
|
||||
to: '/getting-started/contribution'
|
||||
}, {
|
||||
label: 'Community > Roadmap',
|
||||
description: 'Track our development progress in real-time.',
|
||||
icon: 'i-lucide-map',
|
||||
to: '/roadmap'
|
||||
}, {
|
||||
label: 'Community > Devtools',
|
||||
description: 'Integrate Nuxt UI with Nuxt Devtools with Compodium.',
|
||||
icon: 'i-lucide-code',
|
||||
to: 'https://github.com/romhml/compodium',
|
||||
target: '_blank'
|
||||
}, {
|
||||
label: 'Community > Team',
|
||||
description: 'Meet the team behind Nuxt UI.',
|
||||
icon: 'i-lucide-users',
|
||||
to: '/team'
|
||||
}, {
|
||||
label: 'Releases',
|
||||
icon: 'i-lucide-rocket',
|
||||
to: 'https://github.com/nuxt/ui/releases',
|
||||
target: '_blank'
|
||||
}]
|
||||
}
|
||||
@@ -15,7 +15,6 @@ const { data: files } = useLazyAsyncData('search', () => queryCollectionSearchSe
|
||||
})
|
||||
|
||||
const links = useLinks()
|
||||
const searchLinks = useSearchLinks()
|
||||
const color = computed(() => colorMode.value === 'dark' ? (colors as any)[appConfig.ui.colors.neutral][900] : 'white')
|
||||
const radius = computed(() => `:root { --ui-radius: ${appConfig.theme.radius}rem; }`)
|
||||
const blackAsPrimary = computed(() => appConfig.theme.blackAsPrimary ? `:root { --ui-primary: black; } .dark { --ui-primary: white; }` : ':root {}')
|
||||
@@ -49,7 +48,6 @@ useServerSeoMeta({
|
||||
|
||||
useFaviconFromTheme()
|
||||
|
||||
const { frameworks, modules } = useSharedData()
|
||||
const { mappedNavigation, filteredNavigation } = useContentNavigation(navigation)
|
||||
|
||||
provide('navigation', mappedNavigation)
|
||||
@@ -67,22 +65,6 @@ provide('navigation', mappedNavigation)
|
||||
|
||||
<Footer />
|
||||
|
||||
<ClientOnly>
|
||||
<LazyUContentSearch
|
||||
:links="searchLinks"
|
||||
:files="files"
|
||||
:groups="[{
|
||||
id: 'framework',
|
||||
label: 'Framework',
|
||||
items: frameworks
|
||||
}, {
|
||||
id: 'module',
|
||||
label: 'Module',
|
||||
items: modules
|
||||
}]"
|
||||
:navigation="filteredNavigation"
|
||||
:fuse="{ resultLimit: 100 }"
|
||||
/>
|
||||
</ClientOnly>
|
||||
<Search :files="files" :navigation="filteredNavigation" />
|
||||
</UApp>
|
||||
</template>
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"nuxt-llms": "^0.1.2",
|
||||
"nuxt-og-image": "^5.1.3",
|
||||
"prettier": "^3.5.3",
|
||||
"shiki-stream": "^0.1.2",
|
||||
"shiki-transformer-color-highlight": "^1.0.0",
|
||||
"sortablejs": "^1.15.6",
|
||||
"superstruct": "^2.0.2",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { streamText } from 'ai'
|
||||
import { streamText, tool } from 'ai'
|
||||
import { createWorkersAI } from 'workers-ai-provider'
|
||||
import { z } from 'zod'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const { messages } = await readBody(event)
|
||||
@@ -12,11 +13,29 @@ export default defineEventHandler(async (event) => {
|
||||
}
|
||||
: undefined
|
||||
const workersAI = createWorkersAI({ binding: hubAI(), gateway })
|
||||
const autorag = hubAutoRAG('ui3')
|
||||
|
||||
return streamText({
|
||||
model: workersAI('@cf/meta/llama-3.2-3b-instruct'),
|
||||
model: workersAI('@cf/meta/llama-3.3-70b-instruct-fp8-fast'),
|
||||
maxTokens: 10000,
|
||||
system: 'You are a helpful assistant that can answer questions and help.',
|
||||
messages
|
||||
messages,
|
||||
system: `You are a helpful assistant for Nuxt UI. Check your knowledge base before answering any questions.
|
||||
Only respond to questions using information from tool calls.
|
||||
if no relevant information is found in the tool calls, respond, "Sorry, I don't know."
|
||||
Format your markdown response using the following rules:
|
||||
- Use the vue lang for code blocks syntax highlighting.
|
||||
- Don't use markdown headings.
|
||||
`,
|
||||
tools: {
|
||||
searchDocumentation: tool({
|
||||
description: `search the documentation for information to answer questions.`,
|
||||
parameters: z.object({
|
||||
question: z.string().describe('the users question')
|
||||
}),
|
||||
execute: async ({ question }) => {
|
||||
return (await autorag.aiSearch({ query: question })).response
|
||||
}
|
||||
})
|
||||
}
|
||||
}).toDataStreamResponse()
|
||||
})
|
||||
|
||||
21
pnpm-lock.yaml
generated
21
pnpm-lock.yaml
generated
@@ -296,6 +296,9 @@ importers:
|
||||
prettier:
|
||||
specifier: ^3.5.3
|
||||
version: 3.5.3
|
||||
shiki-stream:
|
||||
specifier: ^0.1.2
|
||||
version: 0.1.2(react@19.1.0)(vue@3.5.13(typescript@5.8.3))
|
||||
shiki-transformer-color-highlight:
|
||||
specifier: ^1.0.0
|
||||
version: 1.0.0
|
||||
@@ -6562,6 +6565,17 @@ packages:
|
||||
resolution: {integrity: sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
shiki-stream@0.1.2:
|
||||
resolution: {integrity: sha512-VtzJT2Sn2vwFoJEhKv71/M6Cl7e/m6p0vIVDgJsYKUpV7E+0zayJsuVuU2ltiSEeWUrqncHSPx8i/xKrRqK6Mw==}
|
||||
peerDependencies:
|
||||
react: ^19.0.0
|
||||
vue: ^3.2.0
|
||||
peerDependenciesMeta:
|
||||
react:
|
||||
optional: true
|
||||
vue:
|
||||
optional: true
|
||||
|
||||
shiki-transformer-color-highlight@1.0.0:
|
||||
resolution: {integrity: sha512-WwoXcbSQF4Hcfu/F4V7jvZxqmix4f8KNYNlYiNwz0w9RcABqhdNQOLeRRw3VNV2LBYdNcNR0qd9HVnlV+D+uzg==}
|
||||
|
||||
@@ -15246,6 +15260,13 @@ snapshots:
|
||||
|
||||
shell-quote@1.8.2: {}
|
||||
|
||||
shiki-stream@0.1.2(react@19.1.0)(vue@3.5.13(typescript@5.8.3)):
|
||||
dependencies:
|
||||
'@shikijs/core': 3.3.0
|
||||
optionalDependencies:
|
||||
react: 19.1.0
|
||||
vue: 3.5.13(typescript@5.8.3)
|
||||
|
||||
shiki-transformer-color-highlight@1.0.0:
|
||||
dependencies:
|
||||
'@shikijs/core': 3.3.0
|
||||
|
||||
Reference in New Issue
Block a user