Skip to content
Merged
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
106 changes: 104 additions & 2 deletions src/renderer/src/components/MessageNavigationSidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
</div>

<!-- 消息列表 -->
<div class="flex-1 overflow-y-auto">
<div ref="messagesContainer" class="flex-1 overflow-y-auto scroll-smooth relative">
<div class="p-2 space-y-1">
<div
v-for="(message, index) in filteredMessages"
Expand Down Expand Up @@ -98,6 +98,21 @@
{{ searchQuery ? t('chat.navigation.noResults') : t('chat.navigation.noMessages') }}
</p>
</div>

<!-- 滚动锚点 -->
<div ref="scrollAnchor" class="h-2" />

<!-- 滚动到底部按钮 -->
<div v-if="showScrollToBottomButton && !searchQuery.trim()" class="absolute bottom-4 right-4">
<Button
variant="outline"
size="icon"
class="h-8 w-8 rounded-full shadow-lg bg-background border"
@click="scrollToBottom"
>
<Icon icon="lucide:arrow-down" class="h-4 w-4" />
</Button>
</div>
</div>

<div class="p-4 border-t border-border">
Expand All @@ -109,7 +124,7 @@
</template>

<script setup lang="ts">
import { ref, computed } from 'vue'
import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
import { Icon } from '@iconify/vue'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
Expand All @@ -135,6 +150,9 @@ const { t } = useI18n()
const themeStore = useThemeStore()

const searchQuery = ref('')
const messagesContainer = ref<HTMLDivElement>()
const scrollAnchor = ref<HTMLDivElement>()
const showScrollToBottomButton = ref(false)

// 获取消息完整内容用于搜索
const getFullMessageContent = (message: Message): string => {
Expand Down Expand Up @@ -228,10 +246,94 @@ const highlightSearchQuery = (text: string): string => {
)
}

// 滚动到底部
const scrollToBottom = () => {
nextTick(() => {
scrollAnchor.value?.scrollIntoView({
behavior: 'instant',
block: 'end'
})
})
}

Comment on lines +249 to +258
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

Fix non‑standard scrollIntoView option and add smooth/auto mode

behavior: 'instant' is not a valid ScrollBehavior (valid: 'auto' | 'smooth'), so browsers may ignore it. Also, you’ll likely want smooth scrolling for the button click and non-animated scrolling for auto-scroll on new messages. Expose an options param and switch behavior accordingly.

Apply this diff:

-// 滚动到底部
-const scrollToBottom = () => {
-  nextTick(() => {
-    scrollAnchor.value?.scrollIntoView({
-      behavior: 'instant',
-      block: 'end'
-    })
-  })
-}
+// Scroll to bottom
+type ScrollToBottomOpts = { smooth?: boolean }
+const scrollToBottom = (opts: ScrollToBottomOpts = {}) => {
+  nextTick(() => {
+    const container = messagesContainer.value
+    const smooth = !!opts.smooth
+    const prevBehavior = container?.style.scrollBehavior
+    // If container has CSS 'scroll-behavior: smooth', force instant for programmatic auto-scrolls
+    if (!smooth && container) container.style.scrollBehavior = 'auto'
+    scrollAnchor.value?.scrollIntoView({
+      behavior: smooth ? 'smooth' : 'auto',
+      block: 'end'
+    })
+    if (!smooth && container) container.style.scrollBehavior = prevBehavior ?? ''
+  })
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 滚动到底部
const scrollToBottom = () => {
nextTick(() => {
scrollAnchor.value?.scrollIntoView({
behavior: 'instant',
block: 'end'
})
})
}
// Scroll to bottom
type ScrollToBottomOpts = { smooth?: boolean }
const scrollToBottom = (opts: ScrollToBottomOpts = {}) => {
nextTick(() => {
const container = messagesContainer.value
const smooth = !!opts.smooth
const prevBehavior = container?.style.scrollBehavior
// If container has CSS 'scroll-behavior: smooth', force instant for programmatic auto-scrolls
if (!smooth && container) container.style.scrollBehavior = 'auto'
scrollAnchor.value?.scrollIntoView({
behavior: smooth ? 'smooth' : 'auto',
block: 'end'
})
if (!smooth && container) container.style.scrollBehavior = prevBehavior ?? ''
})
}
🤖 Prompt for AI Agents
In src/renderer/src/components/MessageNavigationSidebar.vue around lines 249 to
258, the scrollToBottom function uses a non‑standard behavior value ('instant')
for scrollIntoView; change it to accept an options parameter (e.g. mode: 'auto'
| 'smooth' with a default) and call scrollAnchor.value?.scrollIntoView({
behavior: mode, block: 'end' }) inside nextTick; update all callers so the
button click passes 'smooth' and automatic/new‑message calls pass 'auto' (or
rely on the default) to ensure valid values and the desired animated vs
non‑animated scrolling.

// 滚动到指定消息
const scrollToMessage = (messageId: string) => {
emit('scrollToMessage', messageId)
}

watch(
() => props.messages.length,
(newLength, oldLength) => {
// 只在新增消息时滚动,避免过度滚动
if (newLength > oldLength && !searchQuery.value.trim()) {
nextTick(() => {
scrollToBottom()
})
}
}
)

watch(
() => props.isOpen,
(newIsOpen) => {
if (newIsOpen && !searchQuery.value.trim()) {
nextTick(() => {
scrollToBottom()
setupScrollObserver()
})
} else if (newIsOpen) {
nextTick(() => {
setupScrollObserver()
})
}
}
)

let intersectionObserver: IntersectionObserver | null = null

const setupScrollObserver = () => {
if (intersectionObserver) {
intersectionObserver.disconnect()
}

intersectionObserver = new IntersectionObserver(
(entries) => {
const entry = entries[0]
showScrollToBottomButton.value = !entry.isIntersecting
},
{
root: messagesContainer.value,
rootMargin: '0px 0px 20px 0px',
threshold: 0
}
)

if (scrollAnchor.value) {
intersectionObserver.observe(scrollAnchor.value)
}
}

// 组件挂载时滚动到底部
onMounted(() => {
if (props.isOpen && !searchQuery.value.trim()) {
nextTick(() => {
scrollToBottom()
setupScrollObserver()
})
} else {
nextTick(() => {
setupScrollObserver()
})
}
})

// 清理观察器
onUnmounted(() => {
if (intersectionObserver) {
intersectionObserver.disconnect()
intersectionObserver = null
}
})
</script>

<style scoped>
Expand Down