Skip to content
Draft
Show file tree
Hide file tree
Changes from 8 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