Files
assistant/components/MessageList.uvue

176 lines
3.6 KiB
Plaintext

<template>
<view class="message-list">
<!-- Empty state -->
<view v-if="messages.length === 0 && !isLoading" class="message-list__empty">
<text class="message-list__empty-icon">💡</text>
<text class="message-list__empty-title">{{ emptyTitle }}</text>
<text class="message-list__empty-subtitle">{{ emptySubtitle }}</text>
</view>
<!-- Messages -->
<scroll-view
v-if="messages.length > 0 || isLoading"
class="message-list__scroll"
ref="scrollViewRef"
scroll-y="true"
show-scrollbar="false"
scroll-with-animation="true"
:scroll-top="scrollToBottom"
>
<view class="message-list__inner">
<MessageBubble
v-for="msg in messages"
:key="msg.id"
:message="msg"
@retry="onRetry(msg.id)"
/>
<!-- Typing indicator -->
<TypingIndicator :visible="isLoading" />
</view>
<!-- Bottom spacer for comfortable scrolling -->
<view class="message-list__spacer"></view>
</scroll-view>
<!-- Scroll-to-bottom FAB -->
<view
v-if="showScrollFab"
class="message-list__fab"
@tap="scrollToLatest"
>
<text class="message-list__fab-icon">↓</text>
</view>
</view>
</template>
<script setup lang="uts">
import type { Message } from '@/types/index'
import MessageBubble from './MessageBubble.uvue'
import TypingIndicator from './TypingIndicator.uvue'
const props = defineProps<{
messages: Message[]
isLoading: boolean
emptyTitle: string
emptySubtitle: string
}>()
const emit = defineEmits<{
retry: [messageId: string]
}>()
// Auto-scroll: increment this to scroll to bottom
const scrollToBottom = ref(0)
const showScrollFab = ref(false)
// Watch for new messages to auto-scroll
watch(
() => props.messages.length,
() => {
// Trigger scroll to bottom on new message
nextTick(() => {
scrollToBottom.value = Date.now()
})
}
)
// Watch for isLoading to auto-scroll when typing starts
watch(
() => props.isLoading,
(loading: boolean) => {
if (loading) {
nextTick(() => {
scrollToBottom.value = Date.now()
})
}
}
)
function scrollToLatest(): void {
scrollToBottom.value = Date.now()
showScrollFab.value = false
}
function onRetry(messageId: string): void {
emit('retry', messageId)
}
</script>
<style scoped>
.message-list {
flex: 1;
display: flex;
flex-direction: column;
position: relative;
overflow: hidden;
}
/* Empty state */
.message-list__empty {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 64rpx;
}
.message-list__empty-icon {
font-size: 80rpx;
margin-bottom: 24rpx;
opacity: 0.6;
}
.message-list__empty-title {
font-size: 34rpx;
font-weight: 600;
color: rgba(255, 255, 255, 0.6);
margin-bottom: 12rpx;
}
.message-list__empty-subtitle {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.3);
text-align: center;
line-height: 1.5;
}
/* Scroll area */
.message-list__scroll {
flex: 1;
}
.message-list__inner {
padding-top: 16rpx;
padding-bottom: 8rpx;
}
.message-list__spacer {
height: 32rpx;
}
/* Scroll-to-bottom FAB */
.message-list__fab {
position: absolute;
bottom: 16rpx;
right: 24rpx;
width: 64rpx;
height: 64rpx;
border-radius: 50%;
background: rgba(108, 92, 231, 0.85);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.4);
z-index: 10;
transition: opacity 0.25s ease, transform 0.25s ease;
}
.message-list__fab-icon {
font-size: 36rpx;
color: #ffffff;
font-weight: 600;
}
</style>