Use markRaw to prevent Vue from deep-proxying the Chat class instance, which broke its internal Vue refs. Add dedent for multiline prompts. Tests use mock transport by default. Set TEST_REAL_LLM=1 and OPENROUTER_API_KEY to run against real OpenRouter.
97 lines
3.2 KiB
Vue
97 lines
3.2 KiB
Vue
<script setup lang="ts">
|
|
import { ScrollAreaRoot, ScrollAreaScrollbar, ScrollAreaThumb, ScrollAreaViewport } from 'reka-ui'
|
|
import { computed, markRaw, nextTick, ref, watch } from 'vue'
|
|
|
|
import APIKeySetup from '@/components/chat/APIKeySetup.vue'
|
|
import ChatInput from '@/components/chat/ChatInput.vue'
|
|
import ChatMessage from '@/components/chat/ChatMessage.vue'
|
|
import { useAIChat } from '@/composables/use-chat'
|
|
|
|
import type { Chat } from '@ai-sdk/vue'
|
|
import type { UIMessage } from 'ai'
|
|
|
|
const { isConfigured, ensureChat } = useAIChat()
|
|
|
|
const chat = ref<Chat<UIMessage> | null>(null)
|
|
const messagesEnd = ref<HTMLDivElement>()
|
|
|
|
const messages = computed(() => chat.value?.messages ?? [])
|
|
const status = computed(() => chat.value?.status ?? 'ready')
|
|
|
|
function scrollToBottom() {
|
|
nextTick(() => {
|
|
messagesEnd.value?.scrollIntoView({ behavior: 'smooth', block: 'end' })
|
|
})
|
|
}
|
|
|
|
watch(messages, scrollToBottom, { deep: true })
|
|
|
|
function handleSubmit(text: string) {
|
|
if (!chat.value) {
|
|
const c = ensureChat()
|
|
if (c) chat.value = markRaw(c)
|
|
}
|
|
chat.value?.sendMessage({ text }).catch(() => {})
|
|
}
|
|
|
|
function handleStop() {
|
|
chat.value?.stop()
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="flex min-w-0 flex-1 flex-col overflow-hidden">
|
|
<APIKeySetup v-if="!isConfigured" />
|
|
|
|
<template v-else>
|
|
<ScrollAreaRoot class="min-h-0 flex-1">
|
|
<ScrollAreaViewport class="h-full px-3 py-3 [&>div]:h-full">
|
|
<!-- Empty state -->
|
|
<div
|
|
v-if="messages.length === 0"
|
|
class="flex h-full flex-col items-center justify-center gap-3 text-muted"
|
|
>
|
|
<icon-lucide-message-circle class="size-8 opacity-50" />
|
|
<p class="text-xs">Describe what you want to create or change.</p>
|
|
</div>
|
|
|
|
<!-- Messages -->
|
|
<div v-else class="flex flex-col gap-3">
|
|
<ChatMessage v-for="msg in messages" :key="msg.id" :message="msg" />
|
|
|
|
<!-- Typing indicator -->
|
|
<div v-if="status === 'submitted'" class="flex gap-2">
|
|
<div
|
|
class="flex size-6 shrink-0 items-center justify-center rounded-full bg-muted/20 text-[10px] font-bold text-muted"
|
|
>
|
|
AI
|
|
</div>
|
|
<div class="flex items-center gap-1 py-2">
|
|
<span
|
|
class="size-1.5 animate-bounce rounded-full bg-muted"
|
|
style="animation-delay: 0ms"
|
|
/>
|
|
<span
|
|
class="size-1.5 animate-bounce rounded-full bg-muted"
|
|
style="animation-delay: 150ms"
|
|
/>
|
|
<span
|
|
class="size-1.5 animate-bounce rounded-full bg-muted"
|
|
style="animation-delay: 300ms"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div ref="messagesEnd" />
|
|
</div>
|
|
</ScrollAreaViewport>
|
|
<ScrollAreaScrollbar orientation="vertical" class="flex w-1.5 touch-none select-none p-px">
|
|
<ScrollAreaThumb class="relative flex-1 rounded-full bg-muted/30" />
|
|
</ScrollAreaScrollbar>
|
|
</ScrollAreaRoot>
|
|
|
|
<ChatInput :status="status" @submit="handleSubmit" @stop="handleStop" />
|
|
</template>
|
|
</div>
|
|
</template>
|