Compare commits

..

4 Commits

Author SHA1 Message Date
Benjamin Canac
16a4b0bbb1 up 2025-05-15 16:48:25 +02:00
Benjamin Canac
dd34d49cd9 Merge branch 'v3' into docs/ai-search 2025-05-15 16:47:37 +02:00
Benjamin Canac
1820e3cc6f up 2025-05-09 19:10:05 +02:00
Benjamin Canac
450948646a docs(app): implement AI search 2025-05-09 18:26:28 +02:00
19 changed files with 965 additions and 691 deletions

View File

@@ -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>

View 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', icon: 'i-lucide-user' }"
: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>

View File

@@ -25,7 +25,6 @@ function getEmojiFlag(locale: string): string {
kk: 'kz', // Kazakh -> Kazakhstan
km: 'kh', // Khmer -> Cambodia
ko: 'kr', // Korean -> South Korea
ms: 'my', // Malay -> Malaysia
nb: 'no', // Norwegian Bokmål -> Norway
sl: 'si', // Slovenian -> Slovenia
sv: 'se', // Swedish -> Sweden

View 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>

View 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
}

View File

@@ -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'
}]
}

View File

@@ -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>

View File

@@ -5,20 +5,20 @@
"dependencies": {
"@ai-sdk/vue": "^1.2.12",
"@iconify-json/logos": "^1.2.4",
"@iconify-json/lucide": "^1.2.44",
"@iconify-json/lucide": "^1.2.43",
"@iconify-json/simple-icons": "^1.2.34",
"@iconify-json/vscode-icons": "^1.2.21",
"@nuxt/content": "^3.5.1",
"@nuxt/image": "^1.10.0",
"@nuxt/ui": "latest",
"@nuxt/ui-pro": "https://pkg.pr.new/@nuxt/ui-pro@9038c43",
"@nuxt/ui-pro": "^3.1.2",
"@nuxthub/core": "^0.8.27",
"@nuxtjs/plausible": "^1.2.0",
"@octokit/rest": "^21.1.1",
"@rollup/plugin-yaml": "^4.1.2",
"@vueuse/integrations": "^13.2.0",
"@vueuse/nuxt": "^13.2.0",
"ai": "^4.3.16",
"ai": "^4.3.15",
"capture-website": "^4.2.0",
"joi": "^17.13.3",
"motion-v": "^1.0.2",
@@ -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",

View File

@@ -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,30 @@ 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'),
maxTokens: 10000,
system: 'You are a helpful assistant that can answer questions and help.',
messages
model: workersAI('@cf/meta/llama-3.3-70b-instruct-fp8-fast'),
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()
})

View File

@@ -116,15 +116,15 @@
"@internationalized/date": "^3.8.0",
"@internationalized/number": "^3.6.1",
"@nuxt/fonts": "^0.11.4",
"@nuxt/icon": "^1.13.0",
"@nuxt/icon": "^1.12.0",
"@nuxt/kit": "^3.17.3",
"@nuxt/schema": "^3.17.3",
"@nuxtjs/color-mode": "^3.5.2",
"@standard-schema/spec": "^1.0.0",
"@tailwindcss/postcss": "^4.1.7",
"@tailwindcss/vite": "^4.1.7",
"@tailwindcss/postcss": "^4.1.6",
"@tailwindcss/vite": "^4.1.6",
"@tanstack/vue-table": "^8.21.3",
"@unhead/vue": "^2.0.9",
"@unhead/vue": "^2.0.8",
"@vueuse/core": "^13.2.0",
"@vueuse/integrations": "^13.2.0",
"colortranslator": "^4.1.0",
@@ -147,7 +147,7 @@
"reka-ui": "^2.2.1",
"scule": "^1.3.0",
"tailwind-variants": "^1.0.0",
"tailwindcss": "^4.1.7",
"tailwindcss": "^4.1.6",
"tinyglobby": "^0.2.13",
"unplugin": "^2.3.4",
"unplugin-auto-import": "^19.2.0",
@@ -156,13 +156,13 @@
"vue-component-type-helpers": "^2.2.10"
},
"devDependencies": {
"@nuxt/eslint-config": "^1.4.0",
"@nuxt/eslint-config": "^1.3.1",
"@nuxt/module-builder": "^1.0.1",
"@nuxt/test-utils": "^3.19.0",
"@nuxt/test-utils": "^3.18.0",
"@release-it/conventional-changelog": "^10.0.1",
"@vue/test-utils": "^2.4.6",
"embla-carousel": "^8.6.0",
"eslint": "^9.27.0",
"eslint": "^9.26.0",
"happy-dom": "^17.4.7",
"nuxt": "^3.17.3",
"release-it": "^19.0.2",

View File

@@ -9,7 +9,7 @@
"typecheck": "nuxt typecheck"
},
"dependencies": {
"@iconify-json/lucide": "^1.2.44",
"@iconify-json/lucide": "^1.2.43",
"@iconify-json/simple-icons": "^1.2.34",
"@nuxt/ui": "latest",
"@nuxthub/core": "^0.8.27",

1072
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@ import icons from './theme/icons'
import { pick } from './runtime/utils'
export const getDefaultUiConfig = (colors?: string[], csp?: { nonce?: string }) => ({
export const getDefaultUiConfig = (colors?: string[]) => ({
colors: pick({
primary: 'green',
secondary: 'blue',
@@ -12,10 +12,7 @@ export const getDefaultUiConfig = (colors?: string[], csp?: { nonce?: string })
error: 'red',
neutral: 'slate'
}, [...(colors || []), 'neutral' as any]),
icons,
csp: csp || {
nonce: ''
}
icons
})
export const defaultOptions = {
@@ -25,9 +22,6 @@ export const defaultOptions = {
theme: {
colors: undefined,
transitions: true
},
csp: {
nonce: ''
}
}

View File

@@ -28,19 +28,6 @@ export interface ModuleOptions {
*/
colorMode?: boolean
/**
* Configure Content Security Policy for Nuxt UI
* @defaultValue `{ nonce: '' }`
* @link https://ui.nuxt.com/getting-started/installation/nuxt#csp
*/
csp?: {
/**
* Enable nonce for inline styles.
* @defaultValue ``
*/
nonce?: string
}
/**
* Customize how the theme is generated
* @link https://ui.nuxt.com/getting-started/theme
@@ -83,7 +70,7 @@ export default defineNuxtModule<ModuleOptions>({
nuxt.options.alias['#ui'] = resolve('./runtime')
nuxt.options.appConfig.ui = defu(nuxt.options.appConfig.ui || {}, getDefaultUiConfig(options.theme.colors, options.csp))
nuxt.options.appConfig.ui = defu(nuxt.options.appConfig.ui || {}, getDefaultUiConfig(options.theme.colors))
// Isolate root node from portaled components
nuxt.options.app.rootAttrs = nuxt.options.app.rootAttrs || {}

View File

@@ -21,11 +21,9 @@ export { default as hy } from './hy'
export { default as id } from './id'
export { default as it } from './it'
export { default as ja } from './ja'
export { default as kk } from './kk'
export { default as km } from './km'
export { default as kk } from './kk'
export { default as ko } from './ko'
export { default as lt } from './lt'
export { default as ms } from './ms'
export { default as nb_no } from './nb_no'
export { default as nl } from './nl'
export { default as pl } from './pl'
@@ -39,8 +37,8 @@ export { default as sv } from './sv'
export { default as th } from './th'
export { default as tj } from './tj'
export { default as tr } from './tr'
export { default as ug_cn } from './ug_cn'
export { default as uk } from './uk'
export { default as ug_cn } from './ug_cn'
export { default as ur } from './ur'
export { default as uz } from './uz'
export { default as vi } from './vi'

View File

@@ -1,56 +0,0 @@
import type { Messages } from '../types'
import { defineLocale } from '../composables/defineLocale'
export default defineLocale<Messages>({
name: 'Lietuvių',
code: 'lt',
messages: {
inputMenu: {
noMatch: 'Nėra atitinkančių duomenų',
noData: 'Nėra duomenų',
create: 'Sukurti „{label}“'
},
calendar: {
prevYear: 'Ankstesni metai',
nextYear: 'Kiti metai',
prevMonth: 'Ankstesnis mėnuo',
nextMonth: 'Kitas mėnuo'
},
inputNumber: {
increment: 'Padidinti',
decrement: 'Sumažinti'
},
commandPalette: {
placeholder: 'Įveskite komandą arba ieškokite...',
noMatch: 'Nėra atitinkančių duomenų',
noData: 'Nėra duomenų',
close: 'Uždaryti'
},
selectMenu: {
noMatch: 'Nėra atitinkančių duomenų',
noData: 'Nėra duomenų',
create: 'Sukurti „{label}“',
search: 'Ieškoti...'
},
toast: {
close: 'Uždaryti'
},
carousel: {
prev: 'Atgal',
next: 'Pirmyn',
goto: 'Eiti į skaidrę {slide}'
},
modal: {
close: 'Uždaryti'
},
slideover: {
close: 'Uždaryti'
},
alert: {
close: 'Uždaryti'
},
table: {
noData: 'Nėra duomenų'
}
}
})

View File

@@ -1,56 +0,0 @@
import type { Messages } from '../types'
import { defineLocale } from '../composables/defineLocale'
export default defineLocale<Messages>({
name: 'Melayu',
code: 'ms',
messages: {
inputMenu: {
noMatch: 'Tiada data yang sepadan',
noData: 'Tiada data',
create: 'Cipta "{label}"'
},
calendar: {
prevYear: 'Tahun sebelum',
nextYear: 'Tahun seterusnya',
prevMonth: 'Bulan sebelum',
nextMonth: 'Bulan seterusnya'
},
inputNumber: {
increment: 'Naikkan',
decrement: 'Kurangkan'
},
commandPalette: {
placeholder: 'Taip arahan atau carian...',
noMatch: 'Tiada data yang sepadan',
noData: 'Tiada data',
close: 'Tutup'
},
selectMenu: {
noMatch: 'Tiada data yang sepadan',
noData: 'Tiada data',
create: 'Cipta "{label}"',
search: 'Cari...'
},
toast: {
close: 'Tutup'
},
carousel: {
prev: 'Sebelum',
next: 'Seterusnya',
goto: 'Pergi ke slaid {slide}'
},
modal: {
close: 'Tutup'
},
slideover: {
close: 'Tutup'
},
alert: {
close: 'Tutup'
},
table: {
noData: 'Tiada data'
}
}
})

View File

@@ -23,8 +23,6 @@ export default defineNuxtPlugin(() => {
const appConfig = useAppConfig()
const nuxtApp = useNuxtApp()
const nonce = computed(() => appConfig.ui?.csp?.nonce)
const root = computed(() => {
const { neutral, ...colors } = appConfig.ui.colors
@@ -46,8 +44,7 @@ export default defineNuxtPlugin(() => {
style: [{
innerHTML: () => root.value,
tagPriority: -2,
id: 'nuxt-ui-colors',
...(nonce.value ? { nonce: nonce.value } : {})
id: 'nuxt-ui-colors'
}]
}
@@ -57,15 +54,10 @@ export default defineNuxtPlugin(() => {
style.innerHTML = root.value
style.setAttribute('data-nuxt-ui-colors', '')
if (nonce.value) {
style.setAttribute('nonce', nonce.value)
}
document.head.appendChild(style)
headData.script = [{
innerHTML: 'document.head.removeChild(document.querySelector(\'[data-nuxt-ui-colors]\'))',
...(nonce.value ? { nonce: nonce.value } : {})
innerHTML: 'document.head.removeChild(document.querySelector(\'[data-nuxt-ui-colors]\'))'
}]
}

View File

@@ -165,9 +165,6 @@ type AppConfigUI = {
}
icons?: Partial<typeof icons>
tv?: typeof defaultConfig
csp?: {
nonce?: string
}
} & TVConfig<typeof ui>
declare module '@nuxt/schema' {