feat(ai): add fork conversation UI button on AI replies

This commit is contained in:
digua committed 2026-05-30 14:19:42 +08:00
1 parent 6224ca996e
commit e78c8f9368
8 files changed
+62 -5

No files matched your search

@@ -347,10 +347,10 @@ describe('AIConversationManager forkConversation', () => {
assert.equal(forked.sessionId, 's1')
const forkedMessages = manager.getMessages(forked.id)
assert.equal(forkedMessages.length, 2)
assert.equal(forkedMessages.length, 3)
assert.deepEqual(
forkedMessages.map((m) => m.content),
['q1', 'a1']
['q1', 'a1', 'q2']
)
// Original conversation should be untouched
@@ -374,8 +374,11 @@ describe('AIConversationManager forkConversation', () => {
assert.equal(forked.title, 'MyChat (fork)')
const forkedMessages = manager.getMessages(forked.id)
assert.equal(forkedMessages.length, 1)
assert.equal(forkedMessages[0]?.content, 'q1')
assert.equal(forkedMessages.length, 2)
assert.deepEqual(
forkedMessages.map((m) => m.content),
['q1', 'a1']
)
manager.close()
} finally {
cleanup(dir)
@@ -609,7 +609,7 @@ export class AIConversationManager {
throw new Error('Message not on active path')
}
const messagesToCopy = activePath.slice(0, cutIndex)
const messagesToCopy = activePath.slice(0, cutIndex + 1)
const now = Math.floor(Date.now() / 1000)
const newConvId = this.generateId('conv')
const forkTitle = title || `${source.title || 'Untitled'} (fork)`
+15
View File
@@ -259,6 +259,19 @@ async function handleEditMessage(payload: { messageId: string; content: string;
conversationListRef.value?.refresh()
}
async function handleForkConversation(messageId: string) {
if (!currentConversationId.value) return
try {
const forked = await useAIService().forkConversation(currentConversationId.value, messageId)
await loadConversation(forked.id)
conversationListRef.value?.refresh()
scrollToBottom(true)
toast.success(t('ai.chat.fork.success'))
} catch (error) {
toast.fail(t('ai.chat.fork.failed'), { description: String(error) })
}
}
// 切换数据源面板
function toggleSourcePanel() {
isSourcePanelCollapsed.value = !isSourcePanelCollapsed.value
@@ -444,11 +457,13 @@ watch(
(pair.assistant.contentBlocks && pair.assistant.contentBlocks.length > 0))
"
:role="pair.assistant.role"
:message-id="pair.assistant.id"
:content="pair.assistant.content"
:timestamp="pair.assistant.timestamp"
:is-streaming="pair.assistant.isStreaming"
:content-blocks="pair.assistant.contentBlocks"
:show-capture-button="!pair.assistant.isStreaming"
@fork="handleForkConversation"
/>
</div>
</template>
@@ -27,6 +27,7 @@ const props = defineProps<{
const emit = defineEmits<{
edit: [payload: { messageId: string; content: string; overwriteSubsequent?: boolean }]
fork: [messageId: string]
}>()
// 格式化时间
@@ -41,6 +42,7 @@ const isEditing = ref(false)
const editContent = ref(props.content)
const editTextareaRef = ref<HTMLTextAreaElement | null>(null)
const canEdit = computed(() => isUser.value && props.editable && !props.isStreaming && !!props.messageId)
const canFork = computed(() => !isUser.value && !isSummary.value && !props.isStreaming && !!props.messageId)
const overwriteSubsequent = ref(false)
// 创建 markdown-it 实例
@@ -574,6 +576,15 @@ async function handleCopyMarkdown() {
<UTooltip v-if="canEdit" :text="t('ai.chat.message.edit.tooltip')" class="no-capture">
<UButton icon="i-heroicons-pencil-square" variant="ghost" color="primary" size="xs" @click="startEditing" />
</UTooltip>
<UTooltip v-if="canFork" :text="t('ai.chat.message.fork.tooltip')" class="no-capture">
<UButton
icon="i-heroicons-arrow-top-right-on-square"
variant="ghost"
color="primary"
size="xs"
@click="emit('fork', messageId!)"
/>
</UTooltip>
<!-- 截屏按钮(仅 AI 回复显示) -->
<CaptureButton
v-if="showCaptureButton && !isUser && !isStreaming"
+7
View File
@@ -38,6 +38,10 @@
"needMoreThanSkill": "Selecting a skill alone is not enough. Enter a specific question.",
"needQuestion": "Enter a question before sending"
},
"fork": {
"success": "Conversation forked",
"failed": "Failed to fork conversation"
},
"backgroundTask": {
"runningShort": "Running",
"runningTitle": "AI is still thinking",
@@ -58,6 +62,9 @@
"submit": "Send",
"overwriteSubsequent": "Overwrite all subsequent messages"
},
"fork": {
"tooltip": "Fork conversation from here"
},
"tools": {
"get_chat_overview": "Get Chat Overview",
"search_messages": "Search Messages",
+7
View File
@@ -38,6 +38,10 @@
"needMoreThanSkill": "スキルを選んだだけでは送信できません。具体的な質問を入力してください。",
"needQuestion": "質問を入力してから送信してください"
},
"fork": {
"success": "会話をフォークしました",
"failed": "会話のフォークに失敗しました"
},
"backgroundTask": {
"runningShort": "進行中",
"runningTitle": "AI がバックグラウンドで推論中です",
@@ -58,6 +62,9 @@
"submit": "送信",
"overwriteSubsequent": "以降のすべての会話を上書き"
},
"fork": {
"tooltip": "ここから会話をフォーク"
},
"tools": {
"get_chat_overview": "チャット概要を取得",
"search_messages": "チャット履歴を検索",
+7
View File
@@ -38,6 +38,10 @@
"needMoreThanSkill": "仅选择技能还不能发送,请继续输入具体问题",
"needQuestion": "请输入问题后再发送"
},
"fork": {
"success": "已复刻对话",
"failed": "复刻对话失败"
},
"backgroundTask": {
"runningShort": "进行中",
"runningTitle": "AI 正在后台推理",
@@ -58,6 +62,9 @@
"submit": "发送",
"overwriteSubsequent": "覆盖后续所有对话"
},
"fork": {
"tooltip": "从此处复刻对话"
},
"tools": {
"get_chat_overview": "获取聊天概览",
"search_messages": "搜索聊天记录",
+7
View File
@@ -38,6 +38,10 @@
"needMoreThanSkill": "只選擇技能還不能送出,請繼續輸入具體問題",
"needQuestion": "請輸入問題後再送出"
},
"fork": {
"success": "已複製對話",
"failed": "複製對話失敗"
},
"backgroundTask": {
"runningShort": "進行中",
"runningTitle": "AI 正在背景推理",
@@ -58,6 +62,9 @@
"submit": "送出",
"overwriteSubsequent": "覆蓋後續所有對話"
},
"fork": {
"tooltip": "從此處複製對話"
},
"tools": {
"get_chat_overview": "取得聊天概覽",
"search_messages": "搜尋聊天紀錄",