Skip to content
Draft
Show file tree
Hide file tree
Changes from 12 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
7 changes: 1 addition & 6 deletions app/layouts/dashboard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,10 @@ const { renameChat, deleteChat, groups, refresh: refreshChats } = useChats()

const sidebarOpen = ref(false)

await useFetch<ChatListItem[]>('/api/chats', {
key: 'chats',
default: () => []
})

watch(loggedIn, () => {
refreshChats()
sidebarOpen.value = false
})
}, { immediate: true })

const items = computed(() => groups.value?.flatMap(group => [
{ label: group.label, type: 'label' as const },
Expand Down
105 changes: 105 additions & 0 deletions layers/nuxi/app/components/agent/AgentChatBody.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<script setup lang="ts">
import type { UIMessage } from 'ai'
import type { AgentChatHandle } from '../../composables/eve/types'

const input = defineModel<string>('input', { required: true })

const props = withDefaults(defineProps<{
chat: AgentChatHandle
chatId: string
prompt: { pasteAttachments: TextPasteAttachment[], canSubmit: boolean }
usage?: { used: number, remaining: number, limit: number }
rateLimitReached?: boolean
compact?: boolean
showFaqEmpty?: boolean
faqQuestions?: FaqCategory[]
showActions?: boolean
showUserTimestamps?: boolean
spacingOffset?: number
getVote: (messageId: string) => boolean | null
variant?: 'subtle' | 'naked'
size?: 'sm' | 'md'
promptClass?: string
promptUi?: Record<string, string>
submitDisabled?: boolean
rateLimitVariant?: 'sticky' | 'footer'
loginHintBar?: boolean
}>(), {
compact: false,
showFaqEmpty: false,
faqQuestions: () => [],
showActions: true,
showUserTimestamps: false,
spacingOffset: 0,
variant: 'subtle',
rateLimitVariant: 'sticky',
loginHintBar: false
})

const emit = defineEmits<{
submit: []
paste: [event: ClipboardEvent]
removeAttachment: [index: number]
restoreAttachment: [index: number]
vote: [message: UIMessage, isUpvoted: boolean]
askQuestion: [question: string]
}>()

const { loggedIn } = useUserSession()
const showLoginHint = computed(() => props.loginHintBar || !loggedIn.value)

const promptRef = useTemplateRef('promptRef')
defineExpose({
focus: () => promptRef.value?.focus()
})
</script>

<template>
<AgentChatMessages
:chat="chat"
:chat-id="chatId"
:compact="compact"
:show-faq-empty="showFaqEmpty"
:faq-questions="faqQuestions"
:show-actions="showActions"
:show-user-timestamps="showUserTimestamps"
:spacing-offset="spacingOffset"
:get-vote="getVote"
:class="compact ? 'flex flex-col gap-4' : 'flex-1 pt-4 pb-4 sm:pb-6'"
@ask-question="emit('askQuestion', $event)"
@vote="(message, isUpvoted) => emit('vote', message, isUpvoted)"
/>

<AgentRateLimitBanner v-if="rateLimitReached" :variant="rateLimitVariant" />

<div
v-else
class="flex flex-col gap-1.5"
:class="rateLimitVariant === 'sticky' ? 'sticky bottom-0 z-10 bg-default' : ''"
>
<slot name="before-prompt" />

<AgentLoginHint v-if="showLoginHint" :bar="loginHintBar" />

<AgentChatPrompt
ref="promptRef"
v-model="input"
v-bind="prompt"
:chat="chat"
:usage="usage"
:variant="variant"
:size="size"
:submit-disabled="submitDisabled"
:class="promptClass"
:ui="promptUi"
@submit="emit('submit')"
@paste="emit('paste', $event)"
@remove-attachment="emit('removeAttachment', $event)"
@restore-attachment="emit('restoreAttachment', $event)"
>
<template v-if="$slots['prompt-footer-left']" #footer-left>
<slot name="prompt-footer-left" />
</template>
</AgentChatPrompt>
</div>
</template>
12 changes: 11 additions & 1 deletion layers/nuxi/app/components/agent/AgentChatMessages.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ const emit = defineEmits<{
const display = computed(() =>
resolveChatDisplayState(props.chat.messages, props.chat.status)
)

function isAssistantPending(message: UIMessage): boolean {
if (message.role !== 'assistant') return false

const { status, messages } = props.chat
if (status !== 'submitted' && status !== 'streaming') return false

const last = messages.at(-1)
return last?.role === 'assistant' && message.id === last.id
}
</script>

<template>
Expand Down Expand Up @@ -59,7 +69,7 @@ const display = computed(() =>
v-if="message.role === 'assistant'"
:message="message"
:vote="getVote(message.id)"
:streaming="chat.status === 'streaming' && message.id === chat.messages.at(-1)?.id"
:streaming="isAssistantPending(message)"
:chat-id="chatId"
:can-regenerate="message.id === chat.messages.at(-1)?.id && chat.status === 'ready'"
@vote="(msg, isUpvoted) => emit('vote', msg, isUpvoted)"
Expand Down
18 changes: 12 additions & 6 deletions layers/nuxi/app/components/agent/AgentPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,16 @@ const {
const { chatList } = useChats()
const { loggedIn } = useUserSession()

type ActiveChat = { id: string, messages: UIMessage[] }
const active = shallowRef<ActiveChat>({ id: crypto.randomUUID(), messages: [] })
type ActiveChat = { id: string, messages: UIMessage[], state: ChatEveState | null }
const active = shallowRef<ActiveChat>({ id: crypto.randomUUID(), messages: [], state: null })
const chatId = computed(() => active.value.id)
const initialMessages = computed(() => active.value.messages)
const initialState = computed(() => active.value.state)
let loadToken = 0

function startNewChat() {
loadToken++
active.value = { id: crypto.randomUUID(), messages: [] }
active.value = { id: crypto.randomUUID(), messages: [], state: null }
}

async function setActiveChat(id: string) {
Expand All @@ -31,10 +32,10 @@ async function setActiveChat(id: string) {
try {
const data = await $fetch<ChatDetail>(`/api/chats/${id}`)
if (token !== loadToken) return
active.value = { id, messages: toUIMessages(data.messages ?? []) }
active.value = { id, messages: dbRowsToUIMessages(data.messages ?? []), state: data.state ?? null }
} catch {
if (token === loadToken) {
active.value = { id: crypto.randomUUID(), messages: [] }
active.value = { id: crypto.randomUUID(), messages: [], state: null }
}
}
}
Expand Down Expand Up @@ -116,6 +117,11 @@ defineShortcuts({
</UTooltip>
</template>

<AgentPanelChat :key="chatId" :chat-id="chatId" :initial-messages="initialMessages" />
<AgentPanelChat
:key="chatId"
:chat-id="chatId"
:initial-messages="initialMessages"
:initial-state="initialState"
/>
</AgentPanelShell>
</template>
Loading