Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 69 additions & 79 deletions docs/app/components/content/AssistantDemo.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<script setup lang="ts">
import { Chat } from '@ai-sdk/vue'
import { DefaultChatTransport } from 'ai'
import { DefaultChatTransport, isToolUIPart, isTextUIPart, getToolName } from 'ai'
import { isToolStreaming } from '@nuxt/ui/utils/ai'
import AssistantComark from '../../../../layer/modules/assistant/runtime/components/AssistantComark'

const config = useRuntimeConfig()
const { t, locale } = useDocusI18n()
Expand Down Expand Up @@ -35,34 +37,22 @@ const chat = isEnabled.value
})
: null

const lastMessage = computed(() => chat?.messages.at(-1))
const showThinking = computed(() =>
chat?.status === 'streaming'
&& lastMessage.value?.role === 'assistant'
&& !lastMessage.value?.parts?.some((p: { type: string }) => p.type === 'text'),
)

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function getMessageToolCalls(message: any) {
if (!message?.parts) return []
return message.parts
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.filter((p: any) => p.type === 'data-tool-calls')
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.flatMap((p: any) => p.data?.tools || [])
}

function handleSubmit(event?: Event) {
event?.preventDefault()
function getToolText(part: Parameters<typeof getToolName>[0]) {
const toolName = getToolName(part)
const input = (part as { input?: Record<string, string> }).input
const verb = part.state === 'output-available' ? 'Searched' : 'Searching'
const readVerb = part.state === 'output-available' ? 'Read' : 'Reading'

if (!input.value.trim() || !chat) {
return
}
return {
'list-pages': `${verb} pages`,
'get-page': `${readVerb} ${input?.path || '...'}`,
}[toolName] || `${verb} ${toolName}`
}

chat.sendMessage({
text: input.value,
})
function handleSubmit() {
if (!input.value.trim() || !chat) return

chat.sendMessage({ text: input.value })
input.value = ''
}

Expand Down Expand Up @@ -90,7 +80,7 @@ function resetChat() {
>
<div class="size-10 rounded-full bg-primary/10 flex items-center justify-center mb-3">
<UIcon
name="i-lucide-sparkles"
name="i-custom:ai"
class="size-5 text-primary"
/>
</div>
Expand Down Expand Up @@ -119,26 +109,31 @@ function resetChat() {
:ui="{ indicator: '*:bg-accented', root: 'h-auto!' }"
class="px-4 py-4"
>
<template #indicator>
<UChatTool
icon="i-lucide-brain"
text="Thinking..."
streaming
/>
</template>

<template #content="{ message }">
<div class="flex flex-col gap-2">
<AssistantLoading
v-if="message.role === 'assistant' && (getMessageToolCalls(message).length > 0 || (showThinking && message.id === lastMessage?.id))"
:tool-calls="getMessageToolCalls(message)"
:is-loading="showThinking && message.id === lastMessage?.id"
<template
v-for="(part, index) in message.parts"
:key="`${message.id}-${part.type}-${index}`"
>
<AssistantComark
v-if="isTextUIPart(part) && part.text"
:markdown="part.text"
/>
<template
v-for="(part, index) in message.parts"
:key="`${message.id}-${part.type}-${index}${'state' in part ? `-${part.state}` : ''}`"
>
<MDCCached
v-if="part.type === 'text' && part.text"
:value="part.text"
:cache-key="`demo-${message.id}-${index}`"
:parser-options="{ highlight: false }"
class="*:first:mt-0 *:last:mb-0"
/>
</template>
</div>

<UChatTool
v-else-if="isToolUIPart(part)"
:text="getToolText(part)"
:icon="getToolName(part) === 'get-page' ? 'i-lucide-file-text' : 'i-lucide-search'"
:streaming="isToolStreaming(part)"
/>
</template>
</template>
</UChatMessages>
</template>
Expand All @@ -160,41 +155,36 @@ function resetChat() {
</div>

<div class="p-3">
<form
class="flex items-center gap-2"
@submit.prevent="handleSubmit"
<UChatPrompt
v-model="input"
:disabled="!isEnabled"
:placeholder="t('assistant.askAnything')"
variant="subtle"
size="sm"
@submit="handleSubmit"
>
<UInput
v-model="input"
:disabled="!isEnabled"
:placeholder="t('assistant.askAnything')"
size="sm"
class="flex-1"
:ui="{
base: 'bg-elevated',
}"
@keydown.enter.exact.prevent="handleSubmit"
/>
<div class="flex items-center gap-1">
<UButton
v-if="chat?.messages.length"
icon="i-lucide-trash-2"
color="neutral"
variant="ghost"
size="xs"
:disabled="!isEnabled"
@click="resetChat"
/>
<UButton
type="submit"
icon="i-lucide-arrow-up"
color="primary"
size="xs"
:disabled="!isEnabled || !input.trim() || chat?.status === 'streaming'"
:loading="chat?.status === 'streaming'"
/>
</div>
</form>
<template #footer>
<div class="ml-auto flex items-center gap-2">
<UButton
v-if="chat?.messages.length"
icon="i-lucide-trash-2"
color="neutral"
variant="ghost"
size="xs"
:disabled="!isEnabled"
@click="resetChat"
/>

<UChatPromptSubmit
size="xs"
:status="chat?.status || 'ready'"
:disabled="!isEnabled || (chat?.status === 'ready' && !input.trim())"
@stop="chat?.stop()"
@reload="chat?.regenerate()"
/>
</div>
</template>
</UChatPrompt>
</div>
</div>
</template>
2 changes: 2 additions & 0 deletions docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
},
"dependencies": {
"@nuxt/ui": "^4.6.1",
"@ai-sdk/vue": "3.0.141",
"ai": "6.0.141",
"@nuxtjs/i18n": "^10.2.4",
"@vercel/analytics": "^2.0.1",
"@vercel/speed-insights": "^2.0.0",
Expand Down
45 changes: 24 additions & 21 deletions layer/app/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const { seo } = appConfig
const { forced: forcedColorMode } = useDocusColorMode()
const site = useSiteConfig()
const { locale, locales, isEnabled, switchLocalePath } = useDocusI18n()
const { isEnabled: isAssistantEnabled, panelWidth: assistantPanelWidth, shouldPushContent } = useAssistant()
const { isEnabled: isAssistantEnabled } = useAssistant()

const nuxtUiLocale = computed(() => nuxtUiLocales[locale.value as keyof typeof nuxtUiLocales] || nuxtUiLocales.en)
const lang = computed(() => nuxtUiLocale.value.code)
Expand Down Expand Up @@ -67,28 +67,31 @@ const { subNavigationMode } = useSubNavigation(navigation)
<UApp :locale="nuxtUiLocale">
<NuxtLoadingIndicator color="var(--ui-primary)" />

<div
:class="['transition-[margin-right] duration-200 ease-linear will-change-[margin-right]', { 'docus-sub-header': subNavigationMode === 'header' }]"
:style="{ marginRight: shouldPushContent ? `${assistantPanelWidth}px` : '0' }"
>
<AppHeader v-if="$route.meta.header !== false" />
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
<AppFooter v-if="$route.meta.footer !== false" />
</div>
<div class="flex">
<div
class="flex-1 min-w-0"
:class="{ 'docus-sub-header': subNavigationMode === 'header' }"
>
<AppHeader v-if="$route.meta.header !== false" />
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
<AppFooter v-if="$route.meta.footer !== false" />

<ClientOnly>
<LazyUContentSearch
:files="files"
:navigation="navigation"
:color-mode="!forcedColorMode"
/>
<LazyAssistantFloatingInput v-if="isAssistantEnabled" />
</ClientOnly>
</div>

<ClientOnly>
<LazyUContentSearch
:files="files"
:navigation="navigation"
:color-mode="!forcedColorMode"
/>
<template v-if="isAssistantEnabled">
<ClientOnly v-if="isAssistantEnabled">
<LazyAssistantPanel />
<LazyAssistantFloatingInput />
</template>
</ClientOnly>
</ClientOnly>
</div>
</UApp>
</template>

Expand Down
3 changes: 1 addition & 2 deletions layer/app/components/docs/DocsAsideRight.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ const props = defineProps<{

const links = computed(() => props.page?.body?.toc?.links || [])

const { shouldPushContent: shouldHideToc } = useAssistant()
const { subNavigationMode } = useSubNavigation()
const appConfig = useAppConfig()
const { t } = useDocusI18n()
Expand All @@ -19,7 +18,7 @@ const contentTocVariants = useUIConfig('contentToc')
<template>
<div>
<UContentToc
v-if="links.length && !shouldHideToc"
v-if="links.length"
:highlight="contentTocVariants.highlight ?? true"
:highlight-color="contentTocVariants.highlightColor"
:highlight-variant="contentTocVariants.highlightVariant"
Expand Down
8 changes: 5 additions & 3 deletions layer/app/pages/[[lang]]/[...slug].vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,9 @@ definePageMeta({

const route = useRoute()
const { locale, isEnabled, t } = useDocusI18n()
const { isOpen } = useAssistant()
const appConfig = useAppConfig()
const navigation = inject<Ref<ContentNavigationItem[]>>('navigation')
const { shouldPushContent: shouldHideToc } = useAssistant()

const collectionName = computed(() => isEnabled.value ? `docs_${locale.value}` : 'docs')

const [{ data: page }, { data: surround }] = await Promise.all([
Expand Down Expand Up @@ -75,7 +74,10 @@ addPrerenderPath(`/raw${route.path}.md`)
<template>
<UPage
v-if="page"
:key="`page-${shouldHideToc}`"
:ui="isOpen ? {
center: 'lg:col-span-10',
right: 'lg:col-span-0',
} : undefined"
>
<UPageHeader
:title="page.title"
Expand Down
7 changes: 4 additions & 3 deletions layer/modules/assistant/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ export default defineNuxtModule<AssistantModuleOptions>({
meta: {
name: 'assistant',
},
moduleDependencies: {
'@comark/nuxt': {},
},
setup(_options, nuxt) {
const legacyOptions = nuxt.options.assistant
if (legacyOptions && Object.keys(legacyOptions).length > 0) {
Expand Down Expand Up @@ -63,8 +66,6 @@ export default defineNuxtModule<AssistantModuleOptions>({
'AssistantChat',
'AssistantPanel',
'AssistantFloatingInput',
'AssistantLoading',
'AssistantMatrix',
]

components.forEach(name =>
Expand All @@ -86,7 +87,7 @@ export default defineNuxtModule<AssistantModuleOptions>({
model: options.model,
}

const routePath = options.apiPath.replace(/^\//, '')
const routePath = options.apiPath!.replace(/^\//, '')
addServerHandler({
route: `/${routePath}`,
handler: resolve('./runtime/server/api/search'),
Expand Down
10 changes: 10 additions & 0 deletions layer/modules/assistant/runtime/components/AssistantComark.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import highlight from '@comark/nuxt/plugins/highlight'

// @ts-expect-error - defineComarkComponent is auto-imported by @comark/nuxt
export default defineComarkComponent({
name: 'AssistantComark',
plugins: [
highlight(),
],
class: '*:first:mt-0 *:last:mb-0',
})
Loading
Loading