Headless ASK ai composable providing semantic search (list mode) and AI chat (generate mode) following NLWeb specification. Ephemeral sessions stored in sessionStorage.
import { useAISearch } from '#ai-search/composables/useAISearch'
function useAISearch(options?: UseAISearchOptions): UseAISearchReturn
mode (optional)'search' | 'chat''search'const ai = useAISearch({ mode: 'search' })
const ai = useAISearch({ mode: 'chat' })
sessionKey (optional)string'ai-search-session'const ai = useAISearch({
mode: 'chat',
sessionKey: 'my-custom-session-key'
})
limit (optional)number10const ai = useAISearch({
mode: 'search',
limit: 20
})
debounce (optional)number (milliseconds)0 (disabled)autoSearch: true)const ai = useAISearch({
mode: 'search',
autoSearch: true,
debounce: 300
})
autoSearch (optional)booleanfalseconst ai = useAISearch({
mode: 'search',
autoSearch: true,
debounce: 300
})
onStreamChunk (optional)(chunk: string) => voidconst ai = useAISearch({
mode: 'chat',
onStreamChunk: (chunk) => {
console.log('Received chunk:', chunk)
}
})
onError (optional)(error: Error) => voidconst ai = useAISearch({
onError: (error) => {
console.error('Ask AI error:', error)
}
})
modeReadonly<Ref<'search' | 'chat'>>const ai = useAISearch()
console.log(ai.mode.value) // 'search'
queryRef<string>const ai = useAISearch()
ai.query.value = 'nuxt modules'
isLoadingReadonly<Ref<boolean>>const ai = useAISearch()
if (ai.isLoading.value) {
console.log('Loading...')
}
errorReadonly<Ref<Error | null>>const ai = useAISearch()
if (ai.error.value) {
console.error(ai.error.value.message)
}
searchResults (search mode)Readonly<Ref<SearchResult[]>>const ai = useAISearch({ mode: 'search' })
ai.searchResults.value.forEach(result => {
console.log(result.name, result.score)
})
messages (chat mode)Readonly<Ref<Message[]>>const ai = useAISearch({ mode: 'chat' })
ai.messages.value.forEach(msg => {
console.log(`${msg.role}: ${msg.content}`)
})
submit()() => Promise<void>search() or sendMessage() based on mode)const ai = useAISearch()
await ai.submit()
search()() => Promise<void>const ai = useAISearch({ mode: 'search' })
ai.query.value = 'nuxt modules'
await ai.search()
console.log(ai.searchResults.value)
sendMessage()() => Promise<void>const ai = useAISearch({ mode: 'chat' })
ai.query.value = 'How do I create a module?'
await ai.sendMessage()
console.log(ai.messages.value)
switchMode()(newMode: 'search' | 'chat') => voidconst ai = useAISearch({ mode: 'search' })
ai.switchMode('chat')
clearSession()() => voidconst ai = useAISearch({ mode: 'chat' })
ai.clearSession()
copyAllMessages()() => Promise<void>const ai = useAISearch({ mode: 'chat' })
await ai.copyAllMessages()
exportAsMarkdown()() => voidconst ai = useAISearch({ mode: 'chat' })
ai.exportAsMarkdown()
abort()() => voidconst ai = useAISearch()
ai.abort()
Messageinterface Message {
id: string
role: 'user' | 'assistant'
content: string
timestamp: number
}
SearchResultinterface SearchResult {
url: string
name: string
score: number
markdown?: string
description?: string
}
UseAISearchOptionsinterface UseAISearchOptions {
mode?: 'search' | 'chat'
sessionKey?: string
limit?: number
debounce?: number
autoSearch?: boolean
onStreamChunk?: (chunk: string) => void
onError?: (error: Error) => void
}
<script setup lang="ts">
const ai = useAISearch({ mode: 'search' })
</script>
<template>
<div>
<input v-model="ai.query.value" @keyup.enter="ai.submit()" />
<button @click="ai.submit()" :disabled="ai.isLoading.value">
Search
</button>
<div v-if="ai.searchResults.value.length">
<div v-for="result in ai.searchResults.value" :key="result.url">
<a :href="result.url">{{ result.name }}</a>
<p>{{ result.description }}</p>
<small>Score: {{ (result.score * 100).toFixed(0) }}%</small>
</div>
</div>
</div>
</template>
<script setup lang="ts">
const ai = useAISearch({
mode: 'search',
autoSearch: true,
debounce: 300
})
</script>
<template>
<div>
<input
v-model="ai.query.value"
placeholder="Search as you type..."
/>
<div v-if="ai.isLoading.value">Loading...</div>
<ul v-else-if="ai.searchResults.value.length">
<li v-for="result in ai.searchResults.value" :key="result.url">
<a :href="result.url">{{ result.name }}</a>
</li>
</ul>
</div>
</template>
<script setup lang="ts">
const ai = useAISearch({
mode: 'chat',
onStreamChunk: (chunk) => {
console.log('Chunk:', chunk)
}
})
</script>
<template>
<div>
<div v-for="msg in ai.messages.value" :key="msg.id">
<strong>{{ msg.role }}:</strong>
<p>{{ msg.content }}</p>
</div>
<input v-model="ai.query.value" @keyup.enter="ai.sendMessage()" />
<button @click="ai.sendMessage()" :disabled="ai.isLoading.value">
Send
</button>
<button v-if="ai.isLoading.value" @click="ai.abort()">
Stop
</button>
</div>
</template>
<script setup lang="ts">
const ai = useAISearch({ mode: 'search' })
</script>
<template>
<div>
<div>
<button
@click="ai.switchMode('search')"
:disabled="ai.mode.value === 'search'"
>
Search
</button>
<button
@click="ai.switchMode('chat')"
:disabled="ai.mode.value === 'chat'"
>
Chat
</button>
</div>
<!-- Search Mode -->
<div v-if="ai.mode.value === 'search'">
<input v-model="ai.query.value" />
<button @click="ai.search()">Search</button>
<div v-for="result in ai.searchResults.value">
{{ result.name }}
</div>
</div>
<!-- Chat Mode -->
<div v-else>
<div v-for="msg in ai.messages.value">
{{ msg.content }}
</div>
<input v-model="ai.query.value" />
<button @click="ai.sendMessage()">Send</button>
</div>
</div>
</template>
<script setup lang="ts">
const ai = useAISearch({
onError: (error) => {
console.error('Error:', error.message)
}
})
</script>
<template>
<div>
<input v-model="ai.query.value" />
<button @click="ai.submit()">Submit</button>
<div v-if="ai.error.value" class="error">
{{ ai.error.value.message }}
</div>
</div>
</template>
<script setup lang="ts">
const ai = useAISearch({
mode: 'chat',
sessionKey: 'my-custom-session'
})
</script>
<template>
<div>
<div v-for="msg in ai.messages.value">
{{ msg.content }}
</div>
<input v-model="ai.query.value" />
<button @click="ai.sendMessage()">Send</button>
<div>
<button @click="ai.copyAllMessages()">Copy All</button>
<button @click="ai.exportAsMarkdown()">Export</button>
<button @click="ai.clearSession()">Clear</button>
</div>
</div>
</template>
// Good: User-initiated search
const ai = useAISearch({ mode: 'search' })
// Use with caution: May cause many requests
const ai = useAISearch({
mode: 'search',
autoSearch: true,
debounce: 500 // Higher debounce for fewer requests
})
const ai = useAISearch({
onError: (error) => {
// Log to error tracking service
console.error('[Ask AI]', error)
// Show user-friendly message
toast.error('Search failed. Please try again.')
}
})
// Clear session on logout
onBeforeUnmount(() => {
ai.clearSession()
})
// Abort when component unmounts
onBeforeUnmount(() => {
ai.abort()
})
// Abort when switching modes
watch(() => ai.mode.value, () => {
ai.abort()
})