mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-05-27 01:01:51 +08:00
feat: 支持停止对话
This commit is contained in:
@@ -175,15 +175,10 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-6">
|
||||
<div class="max-w-5xl p-6">
|
||||
<!-- 页面标题 -->
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="flex h-12 w-12 items-center justify-center rounded-2xl bg-gradient-to-br from-blue-500 to-indigo-600"
|
||||
>
|
||||
<UIcon name="i-heroicons-user-group" class="h-6 w-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white">群成员管理</h2>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
@@ -197,10 +192,9 @@ onMounted(() => {
|
||||
<div class="mb-4">
|
||||
<UInput
|
||||
v-model="searchQuery"
|
||||
placeholder="搜索群昵称、账号名称、ID 或别名..."
|
||||
placeholder="搜索群昵称、账号名称、ID 或别名"
|
||||
icon="i-heroicons-magnifying-glass"
|
||||
size="lg"
|
||||
:ui="{ icon: { trailing: { pointer: '' } } }"
|
||||
class="w-100"
|
||||
>
|
||||
<template #trailing v-if="searchQuery">
|
||||
<UButton icon="i-heroicons-x-mark" variant="link" color="neutral" size="xs" @click="searchQuery = ''" />
|
||||
@@ -229,7 +223,6 @@ onMounted(() => {
|
||||
<table class="w-full">
|
||||
<thead class="sticky top-0 bg-gray-50 dark:bg-gray-800">
|
||||
<tr class="text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400">
|
||||
<th class="px-4 py-4">ID</th>
|
||||
<th class="px-4 py-4">账号名称</th>
|
||||
<th class="px-4 py-4">群昵称</th>
|
||||
<th class="px-4 py-4">
|
||||
@@ -244,7 +237,7 @@ onMounted(() => {
|
||||
/>
|
||||
</button>
|
||||
</th>
|
||||
<th class="px-4 py-4">自定义别名</th>
|
||||
<th class="px-4 py-4 w-64">自定义别名</th>
|
||||
<th class="px-4 py-4 text-right">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -254,28 +247,26 @@ onMounted(() => {
|
||||
:key="member.id"
|
||||
class="hover:bg-gray-50 dark:hover:bg-gray-800/50"
|
||||
>
|
||||
<!-- 平台ID -->
|
||||
<!-- 账号名称 (ID) -->
|
||||
<td class="px-4 py-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
class="flex h-8 w-8 items-center justify-center rounded-full bg-gradient-to-br from-pink-400 to-pink-600 text-xs font-medium text-white"
|
||||
class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-gradient-to-br from-pink-400 to-pink-600 text-xs font-medium text-white"
|
||||
>
|
||||
{{ getFirstChar(member) }}
|
||||
</div>
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400">{{ member.platformId }}</span>
|
||||
<div>
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ member.accountName || '-' }}
|
||||
</span>
|
||||
<span class="ml-1 text-sm text-gray-500 dark:text-gray-400">({{ member.platformId }})</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- 账号名称 -->
|
||||
<td class="px-4 py-4">
|
||||
<span class="text-sm text-gray-900 dark:text-white">
|
||||
{{ member.accountName || '-' }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<!-- 群昵称 -->
|
||||
<td class="px-4 py-4">
|
||||
<span v-if="member.groupNickname" class="text-sm font-medium text-blue-600 dark:text-blue-400">
|
||||
<span v-if="member.groupNickname" class="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ member.groupNickname }}
|
||||
</span>
|
||||
<span v-else class="text-sm text-gray-400 dark:text-gray-500">-</span>
|
||||
@@ -290,13 +281,12 @@ onMounted(() => {
|
||||
|
||||
<!-- 别名 - 直接编辑 -->
|
||||
<td class="px-4 py-4">
|
||||
<div class="relative max-w-xs">
|
||||
<div class="max-w-xs">
|
||||
<UInputTags
|
||||
:model-value="member.aliases"
|
||||
@update:model-value="(val) => updateAliases(member, val)"
|
||||
placeholder="输入后回车..."
|
||||
size="sm"
|
||||
:ui="{ base: 'min-h-[36px]' }"
|
||||
placeholder="输入后回车添加"
|
||||
class="w-80"
|
||||
/>
|
||||
<!-- 保存中指示器 -->
|
||||
<div v-if="savingAliasesId === member.id" class="absolute right-2 top-1/2 -translate-y-1/2">
|
||||
@@ -307,15 +297,7 @@ onMounted(() => {
|
||||
|
||||
<!-- 操作 -->
|
||||
<td class="px-4 py-4 text-right">
|
||||
<UTooltip text="删除成员">
|
||||
<UButton
|
||||
icon="i-heroicons-trash"
|
||||
variant="ghost"
|
||||
color="error"
|
||||
size="sm"
|
||||
@click="showDeleteConfirm(member)"
|
||||
/>
|
||||
</UTooltip>
|
||||
<UButton label="删除" variant="link" color="error" size="xs" @click="showDeleteConfirm(member)" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -210,6 +210,7 @@ watch(
|
||||
<template v-for="msg in messages" :key="msg.id">
|
||||
<!-- 聊天消息 -->
|
||||
<ChatMessage
|
||||
v-if="msg.role === 'user' || msg.content"
|
||||
:role="msg.role"
|
||||
:content="msg.content"
|
||||
:timestamp="msg.timestamp"
|
||||
@@ -338,7 +339,7 @@ watch(
|
||||
</template>
|
||||
|
||||
<!-- AI 思考中指示器 -->
|
||||
<div v-if="isAIThinking && !messages[messages.length - 1]?.isStreaming" class="flex items-start gap-3">
|
||||
<div v-if="isAIThinking && !messages[messages.length - 1]?.content" class="flex items-start gap-3">
|
||||
<div
|
||||
class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-linear-to-br from-violet-500 to-purple-600"
|
||||
>
|
||||
@@ -376,6 +377,17 @@ watch(
|
||||
<span class="h-1.5 w-1.5 animate-bounce rounded-full bg-violet-500 [animation-delay:150ms]" />
|
||||
<span class="h-1.5 w-1.5 animate-bounce rounded-full bg-violet-500 [animation-delay:300ms]" />
|
||||
</span>
|
||||
<span
|
||||
v-else-if="currentToolStatus.status === 'done'"
|
||||
class="flex items-center gap-1.5 text-xs text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
<span>处理结果中</span>
|
||||
<span class="flex gap-1">
|
||||
<span class="h-1 w-1 animate-bounce rounded-full bg-gray-400 [animation-delay:0ms]" />
|
||||
<span class="h-1 w-1 animate-bounce rounded-full bg-gray-400 [animation-delay:150ms]" />
|
||||
<span class="h-1 w-1 animate-bounce rounded-full bg-gray-400 [animation-delay:300ms]" />
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<!-- 已使用的工具列表 -->
|
||||
<div v-if="toolsUsedInCurrentRound.length > 1" class="flex flex-wrap gap-1">
|
||||
@@ -409,7 +421,7 @@ watch(
|
||||
<div class="mx-auto max-w-3xl">
|
||||
<ChatInput
|
||||
:disabled="isAIThinking"
|
||||
:status="isAIThinking ? (isLoadingSource ? 'submitted' : 'streaming') : 'ready'"
|
||||
:status="isAIThinking ? 'streaming' : 'ready'"
|
||||
@send="handleSend"
|
||||
@stop="handleStop"
|
||||
/>
|
||||
|
||||
@@ -49,7 +49,15 @@ function handleStop() {
|
||||
variant="subtle"
|
||||
@submit="handleSubmit"
|
||||
>
|
||||
<UChatPromptSubmit :status="chatStatus" class="rounded-full" color="primary" @stop="handleStop" />
|
||||
<UButton
|
||||
v-if="chatStatus === 'streaming'"
|
||||
icon="i-heroicons-stop-20-solid"
|
||||
color="primary"
|
||||
variant="solid"
|
||||
class="rounded-full"
|
||||
@click="handleStop"
|
||||
/>
|
||||
<UChatPromptSubmit v-else :status="chatStatus" class="rounded-full" color="primary" @stop="handleStop" />
|
||||
</UChatPrompt>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -73,6 +73,8 @@ export function useAIChat(sessionId: string, timeFilter?: { startTs: number; end
|
||||
let isAborted = false
|
||||
// 当前请求 ID,用于区分不同请求的响应
|
||||
let currentRequestId = ''
|
||||
// 当前 Agent 请求 ID,用于中止请求
|
||||
let currentAgentRequestId = ''
|
||||
|
||||
// 生成消息 ID
|
||||
function generateId(prefix: string): string {
|
||||
@@ -151,7 +153,8 @@ export function useAIChat(sessionId: string, timeFilter?: { startTs: number; end
|
||||
|
||||
console.log('[AI] 调用 Agent API...', context)
|
||||
|
||||
const result = await window.agentApi.runStream(content, context, (chunk) => {
|
||||
// 获取 requestId 和 promise
|
||||
const { requestId: agentReqId, promise: agentPromise } = window.agentApi.runStream(content, context, (chunk) => {
|
||||
// 如果已中止或请求 ID 不匹配,忽略后续 chunks
|
||||
if (isAborted || thisRequestId !== currentRequestId) {
|
||||
console.log('[AI] 已中止或请求已过期,忽略 chunk', {
|
||||
@@ -255,6 +258,12 @@ export function useAIChat(sessionId: string, timeFilter?: { startTs: number; end
|
||||
}
|
||||
})
|
||||
|
||||
// 存储 Agent 请求 ID(用于中止)
|
||||
currentAgentRequestId = agentReqId
|
||||
console.log('[AI] Agent 请求已启动,agentReqId:', agentReqId)
|
||||
|
||||
// 等待 Agent 完成
|
||||
const result = await agentPromise
|
||||
console.log('[AI] Agent 返回结果:', result)
|
||||
|
||||
// 如果请求已过期,不更新
|
||||
@@ -397,7 +406,7 @@ export function useAIChat(sessionId: string, timeFilter?: { startTs: number; end
|
||||
/**
|
||||
* 停止生成
|
||||
*/
|
||||
function stopGeneration(): void {
|
||||
async function stopGeneration(): Promise<void> {
|
||||
if (!isAIThinking.value) return
|
||||
|
||||
console.log('[AI] 用户停止生成')
|
||||
@@ -406,6 +415,18 @@ export function useAIChat(sessionId: string, timeFilter?: { startTs: number; end
|
||||
isLoadingSource.value = false
|
||||
currentToolStatus.value = null
|
||||
|
||||
// 调用主进程中止 Agent 请求
|
||||
if (currentAgentRequestId) {
|
||||
console.log('[AI] 中止 Agent 请求:', currentAgentRequestId)
|
||||
try {
|
||||
await window.agentApi.abort(currentAgentRequestId)
|
||||
console.log('[AI] Agent 请求已中止')
|
||||
} catch (error) {
|
||||
console.error('[AI] 中止 Agent 请求失败:', error)
|
||||
}
|
||||
currentAgentRequestId = ''
|
||||
}
|
||||
|
||||
// 标记最后一条 AI 消息为已完成
|
||||
const lastMessage = messages.value[messages.value.length - 1]
|
||||
if (lastMessage && lastMessage.role === 'assistant' && lastMessage.isStreaming) {
|
||||
|
||||
Reference in New Issue
Block a user