Skip to content
Merged
Show file tree
Hide file tree
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
3 changes: 2 additions & 1 deletion src/main/presenter/sqlitePresenter/tables/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,8 +293,9 @@ export class MessagesTable extends BaseTable {
FROM messages
WHERE conversation_id = ?
AND parent_id = ?
AND role = 'assistant'
AND is_variant = 0
ORDER BY created_at ASC
ORDER BY created_at DESC
LIMIT 1
`
)
Expand Down
78 changes: 31 additions & 47 deletions src/renderer/src/components/message/MessageItemAssistant.vue
Original file line number Diff line number Diff line change
Expand Up @@ -177,21 +177,12 @@ const emit = defineEmits<{
// 获取当前会话ID
const currentThreadId = computed(() => chatStore.getActiveThreadId() || '')

// 将 currentVariantIndex 从 ref 改造为 computed 属性
// 这确保了其值总是与 Pinia store 中的状态同步,从根本上消除了竞态条件。
// currentVariantIndex: 0 = 主消息, 1-N = 对应的变体索引
const currentVariantIndex = computed(() => {
const selectedVariantId = chatStore.selectedVariantsMap.get(props.message.id)
const selectedVariantId = chatStore.selectedVariantsMap[props.message.id]
if (!selectedVariantId) return 0

// 如果 store 中没有记录,则显示主消息 (索引 0)
if (!selectedVariantId) {
return 0
}

// 在所有变体中查找已选择的 ID
const variantIndex = allVariants.value.findIndex((v) => v.id === selectedVariantId)

// 如果找到了,返回其索引 + 1 (因为索引 0 是主消息)
// 如果没找到 (数据过时或已删除),则安全地回退到主消息
return variantIndex !== -1 ? variantIndex + 1 : 0
})

Expand All @@ -205,19 +196,26 @@ const currentMessage = computed(() => {
return variant || props.message
})

// 计算当前消息的所有变体(包括缓存中的)
// 计算当前消息的所有变体(包括缓存中的,过滤掉主消息本身
const allVariants = computed(() => {
const messageVariants = props.message.variants || []
const combinedVariants = messageVariants.map((variant) => {
const cachedVariant = Array.from(chatStore.getGeneratingMessagesCache().values()).find(
(cached) => {
const msg = cached.message as AssistantMessage
return msg.is_variant && msg.id === variant.id
}
)
return cachedVariant ? cachedVariant.message : variant
const variantsById = new Map<string, AssistantMessage>()

// 只添加真正的变体(is_variant !== 0),过滤掉主消息本身
messageVariants.forEach((variant) => {
if (variant.is_variant !== 0) {
variantsById.set(variant.id, variant as AssistantMessage)
}
})
return combinedVariants

for (const [, cached] of chatStore.getGeneratingMessagesCache().entries()) {
const msg = cached.message
if (msg.role === 'assistant' && msg.is_variant && msg.parentId === props.message.id) {
variantsById.set(msg.id, msg as AssistantMessage)
}
}

return Array.from(variantsById.values())
})

// 计算变体总数
Expand All @@ -240,14 +238,12 @@ watch(
// 仅当新变体被添加时触发
// 并且当前会话不是正在生成中的消息,避免在生成过程中频繁切换
if (newLength > oldLength && !chatStore.generatingThreadIds.has(currentThreadId.value)) {
const newVariantIndex = newLength // 新变体的索引

const mainMessageId = props.message.id
// newVariantIndex 此时至少为 1
const selectedVariant = allVariants.value[newVariantIndex - 1]
// 获取最后一个变体(数组最后一个元素)
const lastVariant = allVariants.value[newLength - 1]

// 只有当 selectedVariant 存在时才调用 updateSelectedVariant,确保是有效的变体
chatStore.updateSelectedVariant(mainMessageId, selectedVariant ? selectedVariant.id : null)
// 只有当 lastVariant 存在时才调用 updateSelectedVariant,确保是有效的变体
chatStore.updateSelectedVariant(mainMessageId, lastVariant ? lastVariant.id : null)
}
}
)
Expand Down Expand Up @@ -324,30 +320,18 @@ const handleAction = (action: HandleActionType) => {
.trim()
)
} else if (action === 'prev' || action === 'next') {
// 修改 prev/next 逻辑以遵循单向数据流
let newIndex = currentVariantIndex.value // 获取当前计算出的索引
let newIndex = currentVariantIndex.value

switch (action) {
case 'prev':
if (newIndex > 0) newIndex--
break
case 'next':
if (newIndex < totalVariants.value - 1) newIndex++
break
if (action === 'prev' && newIndex > 0) {
newIndex--
} else if (action === 'next' && newIndex < totalVariants.value - 1) {
newIndex++
}

// 如果计算出的新索引与当前索引相同,则不执行任何操作
if (newIndex === currentVariantIndex.value) return

const mainMessageId = props.message.id
// 如果 newIndex 是 0,则 selectedVariant 为 null,表示选择主消息
const selectedVariant = newIndex > 0 ? allVariants.value[newIndex - 1] : null
const selectedVariantId = selectedVariant ? selectedVariant.id : null

// 不再直接修改本地 state,而是调用 store 的 action 来更新全局状态
// store 的更新会通过 computed 属性自动反馈到 UI
chatStore.updateSelectedVariant(mainMessageId, selectedVariantId)

const selectedVariantId = newIndex > 0 ? allVariants.value[newIndex - 1]?.id : null
chatStore.updateSelectedVariant(props.message.id, selectedVariantId)
emit('variantChanged', props.message.id)
} else if (action === 'copyImage') {
// 使用原始消息的ID,因为DOM中的data-message-id使用的是message.id
Expand Down
15 changes: 5 additions & 10 deletions src/renderer/src/components/message/MessageList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
<div @mouseenter="minimap.handleHover(item.id)" @mouseleave="minimap.handleHover(null)">
<MessageItemAssistant
v-if="item.message?.role === 'assistant'"
:ref="retry.setAssistantRef(index)"
:message="item.message"
:is-capturing-image="capture.isCapturing.value"
@copy-image="handleCopyImage"
Expand All @@ -33,7 +32,7 @@
<MessageItemUser
v-else-if="item.message?.role === 'user'"
:message="item.message"
@retry="handleRetry(index)"
@retry="handleRetry(item.message?.id)"
@scroll-to-bottom="scrollToBottom"
/>
<MessageItemPlaceholder
Expand Down Expand Up @@ -103,7 +102,6 @@ import { useMessageScroll } from '@/composables/message/useMessageScroll'
import { useCleanDialog } from '@/composables/message/useCleanDialog'
import { useMessageMinimap } from '@/composables/message/useMessageMinimap'
import { useMessageCapture } from '@/composables/message/useMessageCapture'
import { useMessageRetry } from '@/composables/message/useMessageRetry'
import { DynamicScroller, DynamicScrollerItem } from 'vue-virtual-scroller'
import { getAllMessageDomInfo, getMessageDomInfo } from '@/lib/messageRuntimeCache'

Expand Down Expand Up @@ -159,9 +157,6 @@ const minimap = useMessageMinimap(scroll.scrollInfo)
const capture = useMessageCapture()

// Message retry
const loadedMessages = computed(() =>
props.items.map((item) => item.message).filter((message): message is Message => Boolean(message))
)

const minimapMessages = computed(() => {
const mapped = props.items.map((item) => {
Expand All @@ -179,7 +174,6 @@ const minimapMessages = computed(() => {
if (current.length > 0) return current
return chatStore.variantAwareMessages
})
const retry = useMessageRetry(loadedMessages)

// === Constants ===
const HIGHLIGHT_CLASS = 'selection-highlight'
Expand Down Expand Up @@ -225,7 +219,7 @@ const getMessageSizeKey = (item: MessageListItem) => {
const getVariantSizeKey = (item: MessageListItem) => {
const message = item.message
if (!message || message.role !== 'assistant') return ''
return chatStore.selectedVariantsMap.get(message.id) ?? ''
return chatStore.selectedVariantsMap[message.id] ?? ''
}

// === Event Handlers ===
Expand All @@ -247,8 +241,9 @@ const handleCopyImage = async (
await capture.captureMessage({ messageId, parentId, fromTop, modelInfo })
}

const handleRetry = async (index: number) => {
if (await retry.retryFromUserMessage(index)) {
const handleRetry = async (messageId?: string) => {
if (!messageId) return
if (await chatStore.retryFromUserMessage(messageId)) {
scrollToBottom(true)
}
}
Expand Down
5 changes: 2 additions & 3 deletions src/renderer/src/components/message/MessageToolbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,13 @@
<TooltipContent>{{ t('thread.toolbar.previousVariant') }}</TooltipContent>
</Tooltip>
<span v-show="isAssistant && hasVariants">
{{ currentVariantIndex !== undefined ? currentVariantIndex + 1 : 1 }} /
{{ totalVariants }}
{{ (currentVariantIndex ?? 0) + 1 }} / {{ totalVariants }}
</span>
<Tooltip :delayDuration="200">
<TooltipTrigger as-child>
<Button
v-show="isAssistant && hasVariants"
:disabled="hasVariants && totalVariants === (currentVariantIndex ?? 0) + 1"
:disabled="(currentVariantIndex ?? 0) >= (totalVariants || 0) - 1"
variant="ghost"
size="icon"
class="w-4 h-4 text-muted-foreground hover:text-primary hover:bg-transparent"
Expand Down
28 changes: 18 additions & 10 deletions src/renderer/src/composables/message/useMessageRetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,38 @@ import { ref, type Ref } from 'vue'
import { useChatStore } from '@/stores/chat'
import type { Message } from '@shared/chat'

export function useMessageRetry(messages: Ref<Array<Message>>) {
export function useMessageRetry(messages: Ref<Array<Message | null>>) {
const chatStore = useChatStore()

// Simplified ref management - use Map for better type safety
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const assistantRefs = ref(new Map<number, any>())
const assistantRefs = ref(new Map<string, any>())

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const setAssistantRef = (index: number) => (el: any) => {
const setAssistantRef = (messageId?: string) => (el: any) => {
if (!messageId) return
if (el) {
assistantRefs.value.set(index, el)
assistantRefs.value.set(messageId, el)
} else {
assistantRefs.value.delete(index)
assistantRefs.value.delete(messageId)
}
}

const retryFromUserMessage = async (userMessageIndex: number) => {
const retryFromUserMessage = async (userMessageId?: string) => {
if (!userMessageId) return false

let triggered = false
const orderedMessages = messages.value

// Find next assistant message
for (let i = userMessageIndex + 1; i < messages.value.length; i++) {
if (messages.value[i].role === 'assistant') {
const userMessageIndex = orderedMessages.findIndex((msg) => msg?.id === userMessageId)
if (userMessageIndex === -1) return false

for (let i = userMessageIndex + 1; i < orderedMessages.length; i++) {
const nextMessage = orderedMessages[i]
if (nextMessage?.role === 'assistant') {
try {
const assistantRef = assistantRefs.value.get(i)
const assistantRef = assistantRefs.value.get(nextMessage.id)
if (assistantRef && typeof assistantRef.handleAction === 'function') {
assistantRef.handleAction('retry')
triggered = true
Expand All @@ -40,7 +48,7 @@ export function useMessageRetry(messages: Ref<Array<Message>>) {
// Fallback: regenerate from user message
if (!triggered) {
try {
const userMsg = messages.value[userMessageIndex]
const userMsg = orderedMessages[userMessageIndex]
if (userMsg && userMsg.role === 'user') {
await chatStore.regenerateFromUserMessage(userMsg.id)
return true
Expand Down
Loading