fix(llm): 前端续连
This commit is contained in:
parent
7ae8e5e00f
commit
81bfdb1860
@ -25,7 +25,6 @@ public class LLMChatCoreService : ITransient
|
|||||||
private readonly SqlSugarRepository<LLMChatSummaryHistory> _chatSummaryHistoryService;
|
private readonly SqlSugarRepository<LLMChatSummaryHistory> _chatSummaryHistoryService;
|
||||||
private readonly IOptions<LLMOptions> _llmOption;
|
private readonly IOptions<LLMOptions> _llmOption;
|
||||||
private readonly UserManager _userManager;
|
private readonly UserManager _userManager;
|
||||||
private Kernel _kernel;
|
|
||||||
private readonly SysCacheService _sysCacheService;
|
private readonly SysCacheService _sysCacheService;
|
||||||
|
|
||||||
public LLMChatCoreService(ILogger<LLMChatCoreService> logger,
|
public LLMChatCoreService(ILogger<LLMChatCoreService> logger,
|
||||||
|
|||||||
@ -46,7 +46,7 @@ public class SseService : ControllerBase
|
|||||||
await Response.WriteAsync($"event: ping\n");
|
await Response.WriteAsync($"event: ping\n");
|
||||||
await Response.WriteAsync($"data: pong\n\n");
|
await Response.WriteAsync($"data: pong\n\n");
|
||||||
await Response.Body.FlushAsync(cancellationToken);
|
await Response.Body.FlushAsync(cancellationToken);
|
||||||
await Task.Delay(5000, cancellationToken);
|
await Task.Delay(3000, cancellationToken);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
|
|||||||
@ -25,5 +25,6 @@ export default {
|
|||||||
thinkingDone: 'Denken abgeschlossen',
|
thinkingDone: 'Denken abgeschlossen',
|
||||||
thinkingFailed: 'Denken fehlgeschlagen',
|
thinkingFailed: 'Denken fehlgeschlagen',
|
||||||
thinkingPrepare: 'Denken vorbereiten...',
|
thinkingPrepare: 'Denken vorbereiten...',
|
||||||
|
backEndError: 'Fehler beim Verbinden mit dem Backend-Service, bitte überprüfen Sie, ob das Backend normal ist',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -25,5 +25,6 @@ export default {
|
|||||||
thinkingDone: 'Thinking Done',
|
thinkingDone: 'Thinking Done',
|
||||||
thinkingFailed: 'Thinking Failed',
|
thinkingFailed: 'Thinking Failed',
|
||||||
thinkingPrepare: 'Thinking Prepare...',
|
thinkingPrepare: 'Thinking Prepare...',
|
||||||
|
backEndError: 'Failed to connect to the backend service, please check if the backend is normal',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -25,5 +25,6 @@ export default {
|
|||||||
thinkingDone: 'Pensamiento completado',
|
thinkingDone: 'Pensamiento completado',
|
||||||
thinkingFailed: 'Pensamiento fallido',
|
thinkingFailed: 'Pensamiento fallido',
|
||||||
thinkingPrepare: 'Pensando para preparar...',
|
thinkingPrepare: 'Pensando para preparar...',
|
||||||
|
backEndError: 'Error al conectar con el servicio backend, por favor verifique si el backend está funcionando',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -25,5 +25,6 @@ export default {
|
|||||||
thinkingDone: 'Keskustelu valmis',
|
thinkingDone: 'Keskustelu valmis',
|
||||||
thinkingFailed: 'Keskustelu epäonnistui',
|
thinkingFailed: 'Keskustelu epäonnistui',
|
||||||
thinkingPrepare: 'Keskustelu valmistelemassa...',
|
thinkingPrepare: 'Keskustelu valmistelemassa...',
|
||||||
|
backEndError: 'Virhe yhdistämässä backend-palvelua, tarkista, onko backend ollut kunnossa',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -25,5 +25,6 @@ export default {
|
|||||||
thinkingDone: 'Pensée terminée',
|
thinkingDone: 'Pensée terminée',
|
||||||
thinkingFailed: 'Pensée échouée',
|
thinkingFailed: 'Pensée échouée',
|
||||||
thinkingPrepare: 'Pensée en cours de préparation...',
|
thinkingPrepare: 'Pensée en cours de préparation...',
|
||||||
|
backEndError: 'Erreur lors de la connexion au service backend, veuillez vérifier si le backend est normal',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -25,5 +25,6 @@ export default {
|
|||||||
thinkingDone: 'Berpikir selesai',
|
thinkingDone: 'Berpikir selesai',
|
||||||
thinkingFailed: 'Berpikir gagal',
|
thinkingFailed: 'Berpikir gagal',
|
||||||
thinkingPrepare: 'Mempersiapkan berpikir...',
|
thinkingPrepare: 'Mempersiapkan berpikir...',
|
||||||
|
backEndError: 'Gagal menghubungkan ke layanan backend, silakan periksa apakah backend berfungsi',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -25,5 +25,6 @@ export default {
|
|||||||
thinkingDone: 'Pensamento completato',
|
thinkingDone: 'Pensamento completato',
|
||||||
thinkingFailed: 'Pensamento fallito',
|
thinkingFailed: 'Pensamento fallito',
|
||||||
thinkingPrepare: 'Pensando per prepararsi...',
|
thinkingPrepare: 'Pensando per prepararsi...',
|
||||||
|
backEndError: 'Errore nel collegarsi al servizio backend, controlla se il backend è normale',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -25,5 +25,6 @@ export default {
|
|||||||
thinkingDone: '思考が完了しました',
|
thinkingDone: '思考が完了しました',
|
||||||
thinkingFailed: '思考に失敗しました',
|
thinkingFailed: '思考に失敗しました',
|
||||||
thinkingPrepare: '思考を準備中...',
|
thinkingPrepare: '思考を準備中...',
|
||||||
|
backEndError: 'バックエンドサービスへの接続に失敗しました。バックエンドが正常に動作していることを確認してください',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -25,5 +25,6 @@ export default {
|
|||||||
thinkingDone: '생각 완료',
|
thinkingDone: '생각 완료',
|
||||||
thinkingFailed: '생각 실패',
|
thinkingFailed: '생각 실패',
|
||||||
thinkingPrepare: '생각 준비 중...',
|
thinkingPrepare: '생각 준비 중...',
|
||||||
|
backEndError: '백엔드 서비스에 연결하는 데 실패했습니다. 백엔드가 정상적으로 작동하는지 확인해주세요.',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -25,5 +25,6 @@ export default {
|
|||||||
thinkingDone: 'Berpikir selesai',
|
thinkingDone: 'Berpikir selesai',
|
||||||
thinkingFailed: 'Berpikir gagal',
|
thinkingFailed: 'Berpikir gagal',
|
||||||
thinkingPrepare: 'Mempersiapkan berpikir...',
|
thinkingPrepare: 'Mempersiapkan berpikir...',
|
||||||
|
backEndError: 'Gagal menghubungkan ke layanan backend, silakan periksa apakah backend berfungsi',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -25,5 +25,6 @@ export default {
|
|||||||
thinkingDone: 'Tenking ferdig',
|
thinkingDone: 'Tenking ferdig',
|
||||||
thinkingFailed: 'Tenking feilet',
|
thinkingFailed: 'Tenking feilet',
|
||||||
thinkingPrepare: 'Tenking forbereder...',
|
thinkingPrepare: 'Tenking forbereder...',
|
||||||
|
backEndError: 'Feil ved tilkobling til backend-tjenesten, sjekk om backend-tjenesten er normal',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -25,5 +25,6 @@ export default {
|
|||||||
thinkingDone: 'Myślenie zakończone',
|
thinkingDone: 'Myślenie zakończone',
|
||||||
thinkingFailed: 'Myślenie nie powiodło się',
|
thinkingFailed: 'Myślenie nie powiodło się',
|
||||||
thinkingPrepare: 'Rozpoczynam myślenie...',
|
thinkingPrepare: 'Rozpoczynam myślenie...',
|
||||||
|
backEndError: 'Błąd podczas łączenia z backendem, sprawdź, czy backend działa poprawnie',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -25,5 +25,6 @@ export default {
|
|||||||
thinkingDone: 'Pensamento concluído',
|
thinkingDone: 'Pensamento concluído',
|
||||||
thinkingFailed: 'Pensamento falhou',
|
thinkingFailed: 'Pensamento falhou',
|
||||||
thinkingPrepare: 'Pensando para preparar...',
|
thinkingPrepare: 'Pensando para preparar...',
|
||||||
|
backEndError: 'Erro ao conectar com o backend, por favor verifique se o backend está funcionando',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -25,5 +25,6 @@ export default {
|
|||||||
thinkingDone: 'Мышление завершено',
|
thinkingDone: 'Мышление завершено',
|
||||||
thinkingFailed: 'Мышление не удалось',
|
thinkingFailed: 'Мышление не удалось',
|
||||||
thinkingPrepare: 'Мышление для подготовки...',
|
thinkingPrepare: 'Мышление для подготовки...',
|
||||||
|
backEndError: 'Ошибка при подключении к сервису, пожалуйста, проверьте, настроен ли сервис',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -25,6 +25,7 @@ export default {
|
|||||||
thinkingDone: 'คิดลึก',
|
thinkingDone: 'คิดลึก',
|
||||||
thinkingFailed: 'คิดลึก',
|
thinkingFailed: 'คิดลึก',
|
||||||
thinkingPrepare: 'คิดลึก',
|
thinkingPrepare: 'คิดลึก',
|
||||||
|
backEndError: 'ลบัพของระบบไม่สามารถดึงข้อมูลระบบได้ กรุณาตรวจสอบอีกครั้ง',
|
||||||
},
|
},
|
||||||
|
|
||||||
};
|
};
|
||||||
@ -25,5 +25,6 @@ export default {
|
|||||||
thinkingDone: 'Suy nghĩ đã hoàn tất',
|
thinkingDone: 'Suy nghĩ đã hoàn tất',
|
||||||
thinkingFailed: 'Suy nghĩ thất bại',
|
thinkingFailed: 'Suy nghĩ thất bại',
|
||||||
thinkingPrepare: 'Đang suy nghĩ làm bài tập...',
|
thinkingPrepare: 'Đang suy nghĩ làm bài tập...',
|
||||||
|
backEndError: 'Lỗi khi kết nối với máy chủ, vui lòng kiểm tra xem máy chủ có hoạt động không',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -25,5 +25,6 @@ export default {
|
|||||||
thinkingDone: '思考完成',
|
thinkingDone: '思考完成',
|
||||||
thinkingFailed: '思考失败',
|
thinkingFailed: '思考失败',
|
||||||
thinkingPrepare: '正在思考做准备...',
|
thinkingPrepare: '正在思考做准备...',
|
||||||
|
backEndError: '连接后端服务有错误,请检查后端是否正常',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -25,5 +25,6 @@ export default {
|
|||||||
thinkingDone: '思考完成',
|
thinkingDone: '思考完成',
|
||||||
thinkingFailed: '思考失敗',
|
thinkingFailed: '思考失敗',
|
||||||
thinkingPrepare: '正在思考做準備...',
|
thinkingPrepare: '正在思考做準備...',
|
||||||
|
backEndError: '連接後端服務有錯誤,請檢查後端是否正常',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -25,5 +25,6 @@ export default {
|
|||||||
thinkingDone: '思考完成',
|
thinkingDone: '思考完成',
|
||||||
thinkingFailed: '思考失敗',
|
thinkingFailed: '思考失敗',
|
||||||
thinkingPrepare: '正在思考做準備...',
|
thinkingPrepare: '正在思考做準備...',
|
||||||
|
backEndError: '連接後端服務有錯誤,請檢查後端是否正常',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -2,7 +2,8 @@
|
|||||||
<div style="flex: 1; background: #f3f4f6; overflow: auto">
|
<div style="flex: 1; background: #f3f4f6; overflow: auto">
|
||||||
<el-container>
|
<el-container>
|
||||||
<!-- 侧边栏 -->
|
<!-- 侧边栏 -->
|
||||||
<el-aside v-show="!isFold" :class="isFold ? 'sidebar-fold' : 'expand-sidebar'" style="background: #f3f4f6; border-right: 1px solid #f0f0f0; display: flex; flex-direction: column">
|
<el-aside v-show="!isFold" :class="isFold ? 'sidebar-fold' : 'expand-sidebar'"
|
||||||
|
style="background: #f3f4f6; border-right: 1px solid #f0f0f0; display: flex; flex-direction: column">
|
||||||
<div class="chat-action">
|
<div class="chat-action">
|
||||||
<el-tooltip :content="$t('message.chat.newChat')" placement="top">
|
<el-tooltip :content="$t('message.chat.newChat')" placement="top">
|
||||||
<div class="chat-action-item" @click.stop="handleNewChat">
|
<div class="chat-action-item" @click.stop="handleNewChat">
|
||||||
@ -15,28 +16,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; flex-direction: column; align-items: center; padding-bottom: 8px; padding-top: 0px; margin-top: 0px">
|
<div
|
||||||
<el-avatar :size="60" style="background-color: #f3f4f6" src="/chat.png" fit="fill" class="avatar-with-shadow" />
|
style="display: flex; flex-direction: column; align-items: center; padding-bottom: 8px; padding-top: 0px; margin-top: 0px">
|
||||||
<div style="margin-top: 10px; font-weight: bold; font-size: 18px; color: #333">{{ $t('message.chat.title') }}</div>
|
<el-avatar :size="60" style="background-color: #f3f4f6" src="/chat.png" fit="fill"
|
||||||
|
class="avatar-with-shadow" />
|
||||||
|
<div style="margin-top: 10px; font-weight: bold; font-size: 18px; color: #333">{{
|
||||||
|
$t('message.chat.title') }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="flex: 1; overflow-y: auto; width: 100%">
|
<div style="flex: 1; overflow-y: auto; width: 100%">
|
||||||
<div style="display: flex; flex-direction: column; gap: 12px; height: 100%">
|
<div style="display: flex; flex-direction: column; gap: 12px; height: 100%">
|
||||||
<Conversations
|
<Conversations style="border-radius: 0%; width: 255px" v-model:active="activeHistoryKey"
|
||||||
style="border-radius: 0%; width: 255px"
|
:items="sideBarHistoryList" row-key="key" :show-tooltip="true" showToTopBtn
|
||||||
v-model:active="activeHistoryKey"
|
:label-max-width="210" :load-more="loadMoreHistoryItems"
|
||||||
:items="sideBarHistoryList"
|
:load-more-loading="isHistoryListLoading" showBuiltInMenu @change="handleChange">
|
||||||
row-key="key"
|
|
||||||
:show-tooltip="true"
|
|
||||||
showToTopBtn
|
|
||||||
:label-max-width="210"
|
|
||||||
:load-more="loadMoreHistoryItems"
|
|
||||||
:load-more-loading="isHistoryListLoading"
|
|
||||||
showBuiltInMenu
|
|
||||||
@change="handleChange"
|
|
||||||
>
|
|
||||||
<template #menu="{ item }">
|
<template #menu="{ item }">
|
||||||
<div class="menu-buttons">
|
<div class="menu-buttons">
|
||||||
<el-button v-for="menuItem in actionMenuItems" :key="menuItem.key" link size="default" @click="handleMenuClick(menuItem.key, item)">
|
<el-button v-for="menuItem in actionMenuItems" :key="menuItem.key" link
|
||||||
|
size="default" @click="handleMenuClick(menuItem.key, item)">
|
||||||
<el-icon v-if="menuItem.icon">
|
<el-icon v-if="menuItem.icon">
|
||||||
<component :is="menuItem.icon" />
|
<component :is="menuItem.icon" />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
@ -51,8 +47,7 @@
|
|||||||
|
|
||||||
<!-- 主体内容 -->
|
<!-- 主体内容 -->
|
||||||
<el-container>
|
<el-container>
|
||||||
<el-main
|
<el-main style="
|
||||||
style="
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@ -65,12 +60,12 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
"
|
">
|
||||||
>
|
|
||||||
<div class="main_action_toolbar" style="position: absolute; top: 0px; width: 500px; left: 20px">
|
<div class="main_action_toolbar" style="position: absolute; top: 0px; width: 500px; left: 20px">
|
||||||
<div v-if="isFold" class="chat-action-item main_action_item">
|
<div v-if="isFold" class="chat-action-item main_action_item">
|
||||||
<el-tooltip :content="$t('message.chat.expandChat')" placement="top">
|
<el-tooltip :content="$t('message.chat.expandChat')" placement="top">
|
||||||
<Expand style="width: 1.5em; height: 1.5em; color: #333" @click.stop="handleExpandChat" />
|
<Expand style="width: 1.5em; height: 1.5em; color: #333"
|
||||||
|
@click.stop="handleExpandChat" />
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="isFold" class="chat-action-item main_action_item" @click="handleNewChat">
|
<div v-if="isFold" class="chat-action-item main_action_item" @click="handleNewChat">
|
||||||
@ -88,10 +83,12 @@
|
|||||||
</span>
|
</span>
|
||||||
<template #dropdown>
|
<template #dropdown>
|
||||||
<el-dropdown-menu>
|
<el-dropdown-menu>
|
||||||
<el-dropdown-item v-for="item in modellList.models" :key="item.modelName" @click="handleChangeModel(item)">
|
<el-dropdown-item v-for="item in modellList.models" :key="item.modelName"
|
||||||
|
@click="handleChangeModel(item)">
|
||||||
<div class="model-item">
|
<div class="model-item">
|
||||||
<span>{{ item.providerName }}/{{ item.modelName }}</span>
|
<span>{{ item.providerName }}/{{ item.modelName }}</span>
|
||||||
<el-icon v-if="item.modelName == modellList.currentModel" style="color: #409eff">
|
<el-icon v-if="item.modelName == modellList.currentModel"
|
||||||
|
style="color: #409eff">
|
||||||
<Select />
|
<Select />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
</div>
|
</div>
|
||||||
@ -102,21 +99,33 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="isNew" class="new_chat_title">
|
<div v-if="isNew" class="new_chat_title">
|
||||||
<div style="margin-top: 60px; font-size: 36px; font-weight: bold; letter-spacing: 2px">Hello, {{ userName }}</div>
|
<div style="margin-top: 60px; font-size: 36px; font-weight: bold; letter-spacing: 2px">Hello, {{
|
||||||
<div style="margin-top: 20px; width: 100%; text-align: center; color: #999; font-size: 16px; font-weight: bold; letter-spacing: 2px">
|
userName }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style="margin-top: 20px; width: 100%; text-align: center; color: #999; font-size: 16px; font-weight: bold; letter-spacing: 2px">
|
||||||
{{ $t('message.chat.subTitle') }}
|
{{ $t('message.chat.subTitle') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="chat_content">
|
<div v-else class="chat_content">
|
||||||
<BubbleList class="chat_content_list" ref="chatRef" :list="chatList" maxHeight="100%" style="padding: 0 60px 100px 60px" @complete="handleBubbleComplete" :triggerIndices="triggerIndices">
|
<BubbleList class="chat_content_list" ref="chatRef" :list="chatList" maxHeight="100%"
|
||||||
|
style="padding: 0 60px 100px 60px" @complete="handleBubbleComplete"
|
||||||
|
:triggerIndices="triggerIndices">
|
||||||
<template #header="{ item }">
|
<template #header="{ item }">
|
||||||
<div v-if="item.role == 'assistant' && deepThinkingVisible && item.key == chatList[chatList.length - 1].key" class="header-wrapper">
|
<div v-if="item.role == 'assistant' && deepThinkingVisible && item.key == chatList[chatList.length - 1].key"
|
||||||
<Thinking max-width="100%" buttonWidth="250px" autoCollapse :content="deepThinkingMessage" :status="deepThinkingStatus" backgroundColor="#fff9e6" color="#000">
|
class="header-wrapper">
|
||||||
|
<Thinking max-width="100%" buttonWidth="250px" autoCollapse
|
||||||
|
:content="deepThinkingMessage" :status="deepThinkingStatus"
|
||||||
|
backgroundColor="#fff9e6" color="#000">
|
||||||
<template #label="{ status }">
|
<template #label="{ status }">
|
||||||
<span v-if="status === 'start'">{{ $t('message.chat.startThinking') }}</span>
|
<span v-if="status === 'start'">{{ $t('message.chat.startThinking')
|
||||||
<span v-else-if="status === 'thinking'">{{ $t('message.chat.thinking') }}</span>
|
}}</span>
|
||||||
<span v-else-if="status === 'end'">{{ $t('message.chat.thinkingDone') }}</span>
|
<span v-else-if="status === 'thinking'">{{ $t('message.chat.thinking')
|
||||||
<span v-else-if="status === 'error'">{{ $t('message.chat.thinkingFailed') }}</span>
|
}}</span>
|
||||||
|
<span v-else-if="status === 'end'">{{ $t('message.chat.thinkingDone')
|
||||||
|
}}</span>
|
||||||
|
<span v-else-if="status === 'error'">{{ $t('message.chat.thinkingFailed')
|
||||||
|
}}</span>
|
||||||
</template>
|
</template>
|
||||||
<template #content="{ content }">
|
<template #content="{ content }">
|
||||||
<span>
|
<span>
|
||||||
@ -128,12 +137,15 @@
|
|||||||
</template>
|
</template>
|
||||||
<template #footer="{ item }">
|
<template #footer="{ item }">
|
||||||
<div v-if="item.role == 'assistant'" class="footer-container">
|
<div v-if="item.role == 'assistant'" class="footer-container">
|
||||||
<el-button type="info" text :icon="CopyDocument" size="small" @click="handleCopy(item)" />
|
<el-button type="info" text :icon="CopyDocument" size="small"
|
||||||
|
@click="handleCopy(item)" />
|
||||||
<el-button type="info" text size="small">
|
<el-button type="info" text size="small">
|
||||||
<el-icon v-if="!isPlaying || playAudioKey != item.key" @click="handlePlay(item)">
|
<el-icon v-if="!isPlaying || playAudioKey != item.key"
|
||||||
|
@click="handlePlay(item)">
|
||||||
<VideoPlay />
|
<VideoPlay />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
<el-icon v-if="isPlaying && playAudioKey == item.key" @click="handlePause(item)">
|
<el-icon v-if="isPlaying && playAudioKey == item.key"
|
||||||
|
@click="handlePause(item)">
|
||||||
<VideoPause />
|
<VideoPause />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
</el-button>
|
</el-button>
|
||||||
@ -142,18 +154,9 @@
|
|||||||
</BubbleList>
|
</BubbleList>
|
||||||
</div>
|
</div>
|
||||||
<div :class="isNew ? 'chat_new_input_style' : 'chat_edit_input_style'">
|
<div :class="isNew ? 'chat_new_input_style' : 'chat_edit_input_style'">
|
||||||
<sender
|
<sender ref="senderRef" variant="updown" clearable allow-speech :loading="isSenderLoading"
|
||||||
ref="senderRef"
|
:read-only="isSenderLoading" :auto-size="{ minRows: 1, maxRows: 5 }" v-model="senderInput"
|
||||||
variant="updown"
|
@submit="handleSend" :placeholder="$t('message.chat.inputPlaceholder')">
|
||||||
clearable
|
|
||||||
allow-speech
|
|
||||||
:loading="isSenderLoading"
|
|
||||||
:read-only="isSenderLoading"
|
|
||||||
:auto-size="{ minRows: 1, maxRows: 5 }"
|
|
||||||
v-model="senderInput"
|
|
||||||
@submit="handleSend"
|
|
||||||
:placeholder="$t('message.chat.inputPlaceholder')"
|
|
||||||
>
|
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<div style="display: flex; align-items: center; gap: 8px; flex-wrap: wrap">
|
<div style="display: flex; align-items: center; gap: 8px; flex-wrap: wrap">
|
||||||
<el-button round plain>
|
<el-button round plain>
|
||||||
@ -162,11 +165,9 @@
|
|||||||
</el-icon>
|
</el-icon>
|
||||||
</el-button>
|
</el-button>
|
||||||
|
|
||||||
<div
|
<div :class="{ isDeepThinking }"
|
||||||
:class="{ isDeepThinking }"
|
|
||||||
style="display: flex; align-items: center; gap: 4px; padding: 4px 12px; border: 1px solid silver; border-radius: 15px; cursor: pointer; font-size: 12px"
|
style="display: flex; align-items: center; gap: 4px; padding: 4px 12px; border: 1px solid silver; border-radius: 15px; cursor: pointer; font-size: 12px"
|
||||||
@click="handleDeepThinking"
|
@click="handleDeepThinking">
|
||||||
>
|
|
||||||
<el-icon>
|
<el-icon>
|
||||||
<ElementPlus />
|
<ElementPlus />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
@ -187,10 +188,12 @@
|
|||||||
<div class="ai-rename-icon ai-rename-drag" @mousedown="startDrag">
|
<div class="ai-rename-icon ai-rename-drag" @mousedown="startDrag">
|
||||||
<el-avatar :size="48" src="/chat.png" />
|
<el-avatar :size="48" src="/chat.png" />
|
||||||
</div>
|
</div>
|
||||||
<el-input v-model="renameInput" @keyup.enter="confirmRename" ref="renameInputRef" class="ai-rename-input" />
|
<el-input v-model="renameInput" @keyup.enter="confirmRename" ref="renameInputRef"
|
||||||
|
class="ai-rename-input" />
|
||||||
</div>
|
</div>
|
||||||
<div class="ai-dialog-footer">
|
<div class="ai-dialog-footer">
|
||||||
<el-button @click="cancelRename" class="ai-btn-cancel">{{ $t('message.chat.cancel') }}</el-button>
|
<el-button @click="cancelRename" class="ai-btn-cancel">{{ $t('message.chat.cancel')
|
||||||
|
}}</el-button>
|
||||||
<el-button type="primary" @click="confirmRename" class="ai-btn-confirm">
|
<el-button type="primary" @click="confirmRename" class="ai-btn-confirm">
|
||||||
{{ $t('message.chat.confirm') }}
|
{{ $t('message.chat.confirm') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
@ -256,11 +259,7 @@ const deepThinkingStatus = ref('start');
|
|||||||
const deepThinkingVisible = ref(false);
|
const deepThinkingVisible = ref(false);
|
||||||
|
|
||||||
let historyListSource: ChatListOutput[] = []; //从后台获取的原始历史记录
|
let historyListSource: ChatListOutput[] = []; //从后台获取的原始历史记录
|
||||||
// 添加重连相关配置
|
|
||||||
const reconnectInterval = 5000; // 重连间隔时间(毫秒)
|
|
||||||
const maxRetries = 1000; // 最大重试次数
|
|
||||||
let retryCount = 0;
|
|
||||||
let reconnectTimer: number | null = null;
|
|
||||||
let utterance: SpeechSynthesisUtterance | null = null;
|
let utterance: SpeechSynthesisUtterance | null = null;
|
||||||
const triggerIndices = ref<BubbleListProps['triggerIndices']>('only-last');
|
const triggerIndices = ref<BubbleListProps['triggerIndices']>('only-last');
|
||||||
|
|
||||||
@ -283,14 +282,46 @@ const actionMenuItems = [
|
|||||||
icon: Delete,
|
icon: Delete,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
// 处理菜单点击
|
||||||
|
const renameDialogVisible = ref(false);
|
||||||
|
const currentEditItem = ref<any>(null);
|
||||||
|
const renameInput = ref('');
|
||||||
|
const renameModalRef = ref<HTMLElement | null>(null);
|
||||||
|
let dragData = { dragging: false, offsetX: 0, offsetY: 0 };
|
||||||
|
|
||||||
//#region sse客户端
|
//#region sse客户端
|
||||||
let eventSource: EventSource | null = null;
|
let eventSource: EventSource | null = null;
|
||||||
|
let monitorSSEConnectionHandler: NodeJS.Timeout | null = null;
|
||||||
|
let lastSseConnectionTime = Date.now();
|
||||||
|
let isSSEConnectionClosed = false;
|
||||||
|
const SSE_CONNECTION_TIMEOUT = 5000;
|
||||||
|
// 初始化sse连接
|
||||||
const initSSEConnection = () => {
|
const initSSEConnection = () => {
|
||||||
if (eventSource) {
|
initSSEConnectionCore();
|
||||||
eventSource.close();
|
// 监控sse连接
|
||||||
|
monitorSSEConnectionHandler = setInterval(() => {
|
||||||
|
isSSEConnectionClosed = Date.now() - lastSseConnectionTime > SSE_CONNECTION_TIMEOUT;
|
||||||
|
if (isSSEConnectionClosed) {
|
||||||
|
console.log("SSE connection timed out, reconnecting");
|
||||||
|
try {
|
||||||
|
initSSEConnectionCore();
|
||||||
|
} catch (err) {
|
||||||
|
console.log("SSE connection timed out, reconnecting failed", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, SSE_CONNECTION_TIMEOUT);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 检查sse连接状态
|
||||||
|
const checkSSEConnectionStatus = () => {
|
||||||
|
if (isSSEConnectionClosed) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// 初始化sse连接核心代码
|
||||||
|
const initSSEConnectionCore = () => {
|
||||||
|
closeSSEConnection();
|
||||||
|
|
||||||
eventSource = new EventSource('/sse/chat/' + userId());
|
eventSource = new EventSource('/sse/chat/' + userId());
|
||||||
|
|
||||||
@ -328,66 +359,27 @@ const initSSEConnection = () => {
|
|||||||
chatList.value[chatList.value.length - 1].content = currentChatItemMessage.value;
|
chatList.value[chatList.value.length - 1].content = currentChatItemMessage.value;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
currentChatItemMessage.value = currentChatItemMessage.value + data; // 先接收流式数据
|
currentChatItemMessage.value = currentChatItemMessage.value + data; // 先接收流式数据存放在临时变量中
|
||||||
|
|
||||||
// 收到消息后重置重试计数
|
|
||||||
retryCount = 0;
|
|
||||||
reconnectTimer = null;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 收到ping消息, 用于心跳检测
|
// 收到ping消息, 用于心跳检测
|
||||||
eventSource.addEventListener('ping', (event) => {
|
eventSource.addEventListener('ping', (event) => {
|
||||||
console.log('heat beat:', event.data);
|
console.log('heat beat:', event.data);
|
||||||
// 收到ping后重置重试计数
|
lastSseConnectionTime = Date.now();
|
||||||
retryCount = 0;
|
|
||||||
reconnectTimer = null;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 检查连接状态
|
|
||||||
const checkConnection = () => {
|
|
||||||
if (eventSource && eventSource.readyState === EventSource.CLOSED) {
|
|
||||||
console.log('Connection closed, attempting to reconnect...');
|
|
||||||
if (retryCount < maxRetries) {
|
|
||||||
retryCount++;
|
|
||||||
console.log(`Attempting to reconnect (${retryCount}/${maxRetries})...`);
|
|
||||||
reconnectTimer = window.setTimeout(() => {
|
|
||||||
initSSEConnection();
|
|
||||||
}, reconnectInterval);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 定期检查连接状态
|
|
||||||
const connectionCheckInterval = setInterval(checkConnection, 3000);
|
|
||||||
|
|
||||||
eventSource.onerror = () => {
|
eventSource.onerror = () => {
|
||||||
console.log('SSE connection error');
|
console.log('SSE connection error');
|
||||||
if (eventSource?.readyState === EventSource.CLOSED) {
|
|
||||||
if (connectionCheckInterval) {
|
|
||||||
clearInterval(connectionCheckInterval);
|
|
||||||
}
|
|
||||||
if (retryCount < maxRetries) {
|
|
||||||
retryCount++;
|
|
||||||
console.log(`Attempting to reconnect (${retryCount}/${maxRetries})...`);
|
|
||||||
reconnectTimer = window.setTimeout(() => {
|
|
||||||
initSSEConnection();
|
|
||||||
}, reconnectInterval);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
eventSource.onopen = (event) => {
|
eventSource.onopen = (event) => {
|
||||||
console.log('SSE connection opened:', event);
|
console.log('SSE connection opened:', event);
|
||||||
retryCount = 0; // 连接成功后重置重试计数
|
|
||||||
reconnectTimer = null;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeSSEConnection = () => {
|
const closeSSEConnection = () => {
|
||||||
if (reconnectTimer) {
|
|
||||||
clearTimeout(reconnectTimer);
|
|
||||||
reconnectTimer = null;
|
|
||||||
}
|
|
||||||
if (eventSource) {
|
if (eventSource) {
|
||||||
eventSource.close();
|
eventSource.close();
|
||||||
eventSource = null;
|
eventSource = null;
|
||||||
@ -401,6 +393,10 @@ const handleBubbleComplete = (instance: TypewriterInstance, index: number) => {
|
|||||||
//#endregion sse客户端
|
//#endregion sse客户端
|
||||||
const handleSend = async () => {
|
const handleSend = async () => {
|
||||||
if (!senderInput.value.trim()) return;
|
if (!senderInput.value.trim()) return;
|
||||||
|
if (!checkSSEConnectionStatus()) {
|
||||||
|
ElMessage.error(t('message.chat.backEndError'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
isSenderLoading.value = true;
|
isSenderLoading.value = true;
|
||||||
currentChatItemMessage.value = '';
|
currentChatItemMessage.value = '';
|
||||||
if (isNew.value) {
|
if (isNew.value) {
|
||||||
@ -675,12 +671,7 @@ const loadMoreHistoryItems = async () => {
|
|||||||
isHistoryListLoading.value = false;
|
isHistoryListLoading.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理菜单点击
|
|
||||||
const renameDialogVisible = ref(false);
|
|
||||||
const currentEditItem = ref<any>(null);
|
|
||||||
const renameInput = ref('');
|
|
||||||
const renameModalRef = ref<HTMLElement | null>(null);
|
|
||||||
let dragData = { dragging: false, offsetX: 0, offsetY: 0 };
|
|
||||||
|
|
||||||
const handleMenuClick = (menuKey: string, item: any) => {
|
const handleMenuClick = (menuKey: string, item: any) => {
|
||||||
switch (menuKey) {
|
switch (menuKey) {
|
||||||
@ -839,6 +830,10 @@ onMounted(async () => {
|
|||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
closeSSEConnection();
|
closeSSEConnection();
|
||||||
|
if (monitorSSEConnectionHandler) {
|
||||||
|
clearInterval(monitorSSEConnectionHandler);
|
||||||
|
monitorSSEConnectionHandler = null;
|
||||||
|
}
|
||||||
if (utterance) {
|
if (utterance) {
|
||||||
window.speechSynthesis.cancel();
|
window.speechSynthesis.cancel();
|
||||||
utterance = null;
|
utterance = null;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user