@kernelift/ai-chat 2.7.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -790,253 +790,1022 @@ const handleStreamResponse = async (question: string, enableThink?: boolean) =>
790
790
 
791
791
  ## 💻 完整示例
792
792
 
793
- ### 基础聊天应用
793
+ 基于硅基流动api和 primevue ui库的demo实现如下
794
+
795
+ ### vue展示层
794
796
 
795
797
  ```vue
798
+ <script setup lang="ts">
799
+ import { ref, watch } from 'vue';
800
+ import { ChatContainer } from '@kernelift/ai-chat';
801
+ import { useChat } from './use-chat';
802
+ import '@kernelift/ai-chat/style.css';
803
+ import { CHAT_API_KEY, CHAT_BASE_URL, CHAT_DEFAULT_MODEL } from './constants';
804
+ import Button from 'primevue/button';
805
+ import Textarea from 'primevue/textarea';
806
+ import Select from 'primevue/select';
807
+ import Dialog from 'primevue/dialog';
808
+
809
+ defineOptions({
810
+ name: 'AiChat'
811
+ });
812
+
813
+ const {
814
+ isNewRecord,
815
+ chatModel,
816
+ availableModels,
817
+ showWorkspace,
818
+ userQuestion,
819
+ chatRecords,
820
+ chatMessages,
821
+ generateLoading,
822
+ senderLoading,
823
+ isLoadingModels,
824
+ handleSend,
825
+ handleCancel,
826
+ chatRecordActions,
827
+ handleChangeRecord,
828
+ handleCreateRecord,
829
+ changeShowWorkspace,
830
+ changeModel,
831
+ showEditNameDialog,
832
+ editRecord,
833
+ updateRecordName,
834
+
835
+ bubbleEventActions,
836
+ handleBubbleEvent,
837
+
838
+ showMessageDetailDialog,
839
+ messageDetail,
840
+ showEditMessageDialog,
841
+ editMessage,
842
+ handleEditMessageContent
843
+ } = useChat({
844
+ apiKey: CHAT_API_KEY,
845
+ baseURL: CHAT_BASE_URL,
846
+ uuid: 'openai',
847
+ model: CHAT_DEFAULT_MODEL
848
+ });
849
+
850
+ const tempEditContent = ref('');
851
+
852
+ watch(showEditMessageDialog, (val) => {
853
+ if (val && editMessage.value) {
854
+ tempEditContent.value = editMessage.value.content;
855
+ }
856
+ });
857
+
858
+ /**
859
+ * 处理键盘事件
860
+ * Enter: 发送消息
861
+ * Shift + Enter: 换行
862
+ */
863
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
864
+ function handleKeydown(event: KeyboardEvent, execute: any) {
865
+ if (event.key === 'Enter' && !event.shiftKey) {
866
+ event.preventDefault();
867
+ execute();
868
+ }
869
+ // Shift + Enter 默认行为(换行)会自动生效
870
+ }
871
+ </script>
872
+
796
873
  <template>
797
- <div class="chat-app">
874
+ <div class="w-full h-full relative">
798
875
  <ChatContainer
799
- v-model="inputText"
876
+ v-model="userQuestion"
800
877
  v-model:loading="senderLoading"
801
- v-model:messages="messages"
802
- v-model:record-id="activeRecordId"
803
- :records="records"
804
- :theme-mode="themeMode"
878
+ v-model:messages="chatMessages"
879
+ :records="chatRecords"
805
880
  :is-generate-loading="generateLoading"
806
- :record-actions="recordActions"
807
- has-theme-mode
808
- has-thinking
809
- :markdown-class-name="themeMode === 'dark' ? 'prose-invert' : 'prose'"
881
+ primary-color="#46139b"
882
+ :show-workspace="showWorkspace"
883
+ :has-sender-tools="true"
884
+ uuid="openai"
885
+ :input-height="180"
886
+ :default-input-height="80"
887
+ :record-actions="chatRecordActions"
888
+ :bubble-ext-events="bubbleEventActions"
810
889
  @send="handleSend"
811
890
  @cancel="handleCancel"
812
891
  @change-record="handleChangeRecord"
892
+ @clear="handleCreateRecord"
893
+ @close-workspace="showWorkspace = false"
813
894
  @bubble-event="handleBubbleEvent"
814
- @change-theme="(mode) => (themeMode = mode)"
815
895
  >
816
- <!-- 自定义空状态 -->
896
+ <template #logo>
897
+ <div class="mt-2 mb-3">
898
+ <img src="./logo.avif" alt="logo" style="width: 7.5rem; height: 1.1rem" />
899
+ </div>
900
+ </template>
901
+
902
+ <template #header-logo>
903
+ <div class="mx-2">
904
+ <img src="./logo.avif" alt="header-logo" style="width: 8.8rem; height: 1.3rem" />
905
+ </div>
906
+ </template>
907
+
908
+ <template #send-button="{ state, execute }">
909
+ <Button
910
+ size="small"
911
+ :disabled="!state.inputText && !state.loading"
912
+ rounded
913
+ :icon="state.loading ? 'pi pi-stop' : 'pi pi-send'"
914
+ :aria-label="state.loading ? 'Cancel' : 'Send'"
915
+ @click="execute"
916
+ />
917
+ </template>
918
+
919
+ <template #think-button="{ state, execute }">
920
+ <Button
921
+ size="small"
922
+ :severity="state.enableThink ? undefined : 'secondary'"
923
+ variant="outlined"
924
+ icon="pi pi-lightbulb"
925
+ rounded
926
+ label="深度思考"
927
+ :style="
928
+ state.enableThink
929
+ ? {
930
+ background: 'rgba(var(--kl-chat-primary-rgb), 0.1)',
931
+ height: '2rem'
932
+ }
933
+ : {
934
+ height: '2rem'
935
+ }
936
+ "
937
+ @click="execute"
938
+ ></Button>
939
+ </template>
940
+
941
+ <template #sender-textarea="{ height, execute }">
942
+ <Textarea
943
+ v-model="userQuestion"
944
+ class="w-full"
945
+ :style="{
946
+ height: height + 'px',
947
+ resize: 'none',
948
+ outline: 'none',
949
+ border: 'none',
950
+ boxShadow: 'none',
951
+ '--p-textarea-padding-x': '0.3rem'
952
+ }"
953
+ @keydown="handleKeydown($event, execute)"
954
+ ></Textarea>
955
+ </template>
956
+
817
957
  <template #empty>
818
- <div class="empty-state">
819
- <div class="welcome-title">AI 助手</div>
820
- <div class="welcome-desc">你好!有什么可以帮助你的吗?</div>
958
+ <div class="flex items-center justify-center flex-col">
959
+ <img src="./logo.avif" alt="header-logo" style="width: 10rem" />
960
+ <div class="p-4 text-center text-surface-600">
961
+ 本工程基于硅基流动API进行开发,提供智能对话服务,支持多种模型选择与个性化配置。
962
+ </div>
821
963
  </div>
822
964
  </template>
823
965
 
824
- <!-- 自定义 Logo -->
825
- <template #logo>
826
- <div class="brand-logo">
827
- <IconRender icon="material-symbols:chat" />
828
- <span>AI 对话</span>
966
+ <template #new-chat-button="{ execute, disabled }">
967
+ <Button
968
+ label="新建对话"
969
+ icon="pi pi-plus"
970
+ class="w-full"
971
+ rounded
972
+ :disabled="disabled"
973
+ style="height: 2.5rem"
974
+ @click="execute"
975
+ ></Button>
976
+ </template>
977
+
978
+ <template #sender-tools>
979
+ <div class="h-9 w-full flex items-center gap-2 px-3">
980
+ <div>
981
+ <!-- 模型选择 -->
982
+ <Select
983
+ v-model="chatModel"
984
+ :options="availableModels"
985
+ option-label="label"
986
+ option-value="value"
987
+ size="small"
988
+ filter
989
+ placeholder="选择模型"
990
+ style="border-radius: 0.9rem"
991
+ :loading="isLoadingModels"
992
+ :disabled="senderLoading"
993
+ overlay-class="text-sm small-dropdown"
994
+ @change="changeModel($event.value)"
995
+ />
996
+ </div>
997
+
998
+ <div class="ml-auto" v-if="!isNewRecord">
999
+ <!-- mobile时只显示icon -->
1000
+ <Button
1001
+ icon="pi pi-sitemap"
1002
+ size="small"
1003
+ rounded
1004
+ :variant="showWorkspace ? undefined : 'outlined'"
1005
+ @click="changeShowWorkspace"
1006
+ />
1007
+ </div>
1008
+ </div>
1009
+ </template>
1010
+
1011
+ <template #bubble-event="{ data }">
1012
+ <div class="flex gap-3 ml-auto items-center" v-if="data.role === 'assistant'">
1013
+ <div class="chat-bubble__event-item" @click="handleBubbleEvent('delete', data)">
1014
+ <i class="pi pi-trash" style="font-size: 0.95rem"></i>
1015
+ </div>
1016
+ <div class="chat-bubble__event-item" @click="handleBubbleEvent('edit', data)">
1017
+ <i class="pi pi-pencil" style="font-size: 0.95rem"></i>
1018
+ </div>
1019
+ </div>
1020
+ </template>
1021
+
1022
+ <template #workspace="{ record: activeRecord }">
1023
+ <div class="p-4 h-full overflow-auto">
1024
+ <div v-if="activeRecord" class="space-y-4">
1025
+ <div class="border-b border-gray-300 pb-4">
1026
+ <h3 class="text-lg font-semibold mb-2">会话信息</h3>
1027
+ </div>
1028
+
1029
+ <div class="space-y-3">
1030
+ <div>
1031
+ <label class="text-sm font-medium text-surface-600">会话名称</label>
1032
+ <p class="mt-1 text-surface-900">{{ activeRecord.name }}</p>
1033
+ </div>
1034
+
1035
+ <div>
1036
+ <label class="text-sm font-medium text-surface-600">创建时间</label>
1037
+ <p class="mt-1 text-surface-900">{{ activeRecord.createTime }}</p>
1038
+ </div>
1039
+
1040
+ <div>
1041
+ <label class="text-sm font-medium text-surface-600">会话ID</label>
1042
+ <p class="mt-1 text-surface-900 text-xs font-mono">{{ activeRecord.id }}</p>
1043
+ </div>
1044
+
1045
+ <div>
1046
+ <label class="text-sm font-medium text-surface-600">消息数量</label>
1047
+ <p class="mt-1 text-surface-900">{{ chatMessages.length }} 条消息</p>
1048
+ </div>
1049
+
1050
+ <div v-if="activeRecord.extraData">
1051
+ <label class="text-sm font-medium text-surface-600">首条消息</label>
1052
+ <p class="mt-1 text-surface-900 text-sm">{{ activeRecord.content }}</p>
1053
+ </div>
1054
+ </div>
1055
+
1056
+ <div class="border-t border-gray-300 pt-4 mt-4">
1057
+ <h4 class="text-sm font-semibold mb-2">会话统计</h4>
1058
+ <div class="grid grid-cols-2 gap-3">
1059
+ <div class="bg-surface-50 p-3 rounded border border-gray-300">
1060
+ <div class="text-xs text-surface-600">用户消息</div>
1061
+ <div class="text-lg font-semibold">
1062
+ {{ chatMessages.filter((m) => m.role === 'user').length }}
1063
+ </div>
1064
+ </div>
1065
+ <div class="bg-surface-50 p-3 rounded border border-gray-300">
1066
+ <div class="text-xs text-surface-600">AI回复</div>
1067
+ <div class="text-lg font-semibold">
1068
+ {{ chatMessages.filter((m) => m.role === 'assistant').length }}
1069
+ </div>
1070
+ </div>
1071
+ </div>
1072
+ </div>
1073
+ </div>
1074
+
1075
+ <div v-else class="flex items-center justify-center h-full text-surface-500">
1076
+ <div class="text-center">
1077
+ <i class="pi pi-inbox text-4xl mb-3"></i>
1078
+ <p>选择一个会话查看详情</p>
1079
+ </div>
1080
+ </div>
829
1081
  </div>
830
1082
  </template>
831
1083
  </ChatContainer>
1084
+
1085
+ <Dialog
1086
+ v-if="editRecord"
1087
+ v-model:visible="showEditNameDialog"
1088
+ header="编辑会话名称"
1089
+ :modal="true"
1090
+ :closable="true"
1091
+ :dismissable-mask="true"
1092
+ :style="{ width: '400px' }"
1093
+ >
1094
+ <div class="flex flex-col gap-4">
1095
+ <div class="flex flex-col gap-2">
1096
+ <label class="font-semibold">新名称</label>
1097
+ <Textarea
1098
+ v-model="editRecord.name"
1099
+ rows="2"
1100
+ class="w-full"
1101
+ placeholder="输入新的会话名称"
1102
+ />
1103
+ </div>
1104
+ <div class="flex justify-end gap-2 mt-4">
1105
+ <Button
1106
+ label="取消"
1107
+ icon="pi pi-times"
1108
+ severity="secondary"
1109
+ @click="showEditNameDialog = false"
1110
+ />
1111
+ <Button
1112
+ label="保存"
1113
+ icon="pi pi-save"
1114
+ @click="
1115
+ updateRecordName(editRecord, editRecord.name);
1116
+ showEditNameDialog = false;
1117
+ "
1118
+ />
1119
+ </div>
1120
+ </div>
1121
+ </Dialog>
1122
+
1123
+ <!-- 消息详情弹窗 -->
1124
+ <Dialog
1125
+ v-if="messageDetail"
1126
+ v-model:visible="showMessageDetailDialog"
1127
+ header="消息详情"
1128
+ :modal="true"
1129
+ :closable="true"
1130
+ :dismissable-mask="true"
1131
+ :style="{ width: '500px' }"
1132
+ >
1133
+ <div class="flex flex-col gap-3">
1134
+ <div>
1135
+ <label class="text-sm font-medium text-surface-600">消息ID</label>
1136
+ <div
1137
+ class="mt-1 p-2 bg-surface-50 rounded text-xs font-mono break-all border border-surface-200"
1138
+ >
1139
+ {{ messageDetail.id }}
1140
+ </div>
1141
+ </div>
1142
+ <div>
1143
+ <label class="text-sm font-medium text-surface-600">角色</label>
1144
+ <div class="mt-1">
1145
+ <span
1146
+ :class="{
1147
+ 'bg-blue-100 text-blue-700': messageDetail.role === 'user',
1148
+ 'bg-purple-100 text-purple-700': messageDetail.role === 'assistant'
1149
+ }"
1150
+ class="px-2 py-1 rounded text-xs font-medium"
1151
+ >
1152
+ {{ messageDetail.role }}
1153
+ </span>
1154
+ </div>
1155
+ </div>
1156
+ <div>
1157
+ <label class="text-sm font-medium text-surface-600">发送时间</label>
1158
+ <div class="mt-1 text-sm text-surface-900">
1159
+ {{ new Date(messageDetail.timestamp).toLocaleString() }}
1160
+ </div>
1161
+ </div>
1162
+ <div>
1163
+ <label class="text-sm font-medium text-surface-600">内容统计</label>
1164
+ <div class="mt-1 text-sm text-surface-900">{{ messageDetail.content.length }} 字符</div>
1165
+ </div>
1166
+ <div v-if="messageDetail.extraData && Object.keys(messageDetail.extraData).length > 0">
1167
+ <label class="text-sm font-medium text-surface-600">元数据</label>
1168
+ <pre
1169
+ class="mt-1 text-xs bg-surface-50 p-2 rounded overflow-auto border border-surface-200 max-h-40"
1170
+ >{{ JSON.stringify(messageDetail.extraData, null, 2) }}</pre
1171
+ >
1172
+ </div>
1173
+ </div>
1174
+ </Dialog>
1175
+
1176
+ <!-- 编辑消息弹窗 -->
1177
+ <Dialog
1178
+ v-if="editMessage"
1179
+ v-model:visible="showEditMessageDialog"
1180
+ header="编辑消息内容"
1181
+ :modal="true"
1182
+ :closable="true"
1183
+ :dismissable-mask="true"
1184
+ :style="{ width: '600px' }"
1185
+ >
1186
+ <div class="flex flex-col gap-4">
1187
+ <div class="flex flex-col gap-2">
1188
+ <label class="font-semibold text-sm">消息内容</label>
1189
+ <Textarea
1190
+ v-model="tempEditContent"
1191
+ rows="10"
1192
+ class="w-full font-mono text-sm leading-relaxed"
1193
+ style="resize: vertical; min-height: 200px"
1194
+ />
1195
+ </div>
1196
+ <div class="flex justify-end gap-2">
1197
+ <Button
1198
+ label="取消"
1199
+ icon="pi pi-times"
1200
+ severity="secondary"
1201
+ @click="showEditMessageDialog = false"
1202
+ />
1203
+ <Button
1204
+ label="保存"
1205
+ icon="pi pi-save"
1206
+ @click="
1207
+ handleEditMessageContent(tempEditContent);
1208
+ showEditMessageDialog = false;
1209
+ "
1210
+ />
1211
+ </div>
1212
+ </div>
1213
+ </Dialog>
832
1214
  </div>
833
1215
  </template>
834
1216
 
835
- <script setup lang="ts">
836
- import { ref, onUnmounted } from 'vue';
837
- import {
838
- ChatContainer,
839
- type BubbleEvent,
840
- type ChatMessage,
841
- type ChatRecord,
842
- type ChatRecordAction
843
- } from '@kernelift/ai-chat';
844
- import '@kernelift/ai-chat/style.css';
845
- import { useStorage } from '@vueuse/core';
1217
+ <style lang="scss" scoped>
1218
+ .chat-bubble__event-item {
1219
+ padding: 0.25rem;
1220
+ border-radius: 50%;
1221
+ cursor: pointer;
1222
+ transition: background-color 0.2s ease;
1223
+ // width: 1.625rem;
1224
+ // height: 1.625rem;
1225
+ display: flex;
1226
+ justify-content: center;
1227
+ align-items: center;
846
1228
 
847
- // 状态管理
848
- const inputText = ref('');
849
- const messages = ref<ChatMessage[]>([]);
850
- const records = useStorage<ChatRecord[]>('chat-records', []);
851
- const activeRecordId = ref<string | null>(null);
852
- const senderLoading = ref(false);
853
- const generateLoading = ref(false);
854
- const themeMode = ref<'light' | 'dark'>('light');
855
-
856
- // 记录操作
857
- const recordActions: ChatRecordAction[] = [
858
- {
859
- id: 'edit',
860
- name: '编辑',
861
- icon: 'edit',
862
- action: (record) => {
863
- console.log('编辑记录:', record);
864
- }
865
- },
866
- {
867
- id: 'delete',
868
- name: '删除',
869
- icon: 'delete',
870
- action: (record) => {
871
- records.value = records.value.filter((r) => r.id !== record.id);
1229
+ &:active {
1230
+ color: #6b7280; // gray-500
1231
+ background-color: rgba(var(--kl-chat-primary-rgb), 0.13);
1232
+ }
1233
+
1234
+ // 只有支持悬停的设备才应用悬停效果
1235
+ @media (hover: hover) and (pointer: fine) {
1236
+ &:hover {
1237
+ color: #6b7280; // gray-500
1238
+ background-color: rgba(var(--kl-chat-primary-rgb), 0.13);
872
1239
  }
873
1240
  }
874
- ];
1241
+ }
1242
+ </style>
875
1243
 
876
- // 发送消息(包含记录创建逻辑)
877
- const handleSend = async (
878
- text: string,
879
- enableThink?: boolean,
880
- enableNet?: boolean,
881
- needCreateRecord?: boolean
882
- ) => {
883
- inputText.value = '';
1244
+ <style>
1245
+ .small-dropdown {
1246
+ border-radius: 1rem !important;
1247
+ overflow: hidden;
1248
+ }
884
1249
 
885
- // 添加用户消息
886
- messages.value.push({
887
- id: Date.now().toString(),
888
- role: 'user',
889
- content: text,
890
- timestamp: Date.now(),
891
- isThinking: enableThink
892
- });
1250
+ .small-dropdown .p-inputtext {
1251
+ padding-block: 0.3rem; /* 调整内边距以适应较小的字体 */
1252
+ }
1253
+ </style>
1254
+ ```
893
1255
 
894
- // 如果需要创建新记录(当前没有激活的记录)
895
- if (needCreateRecord) {
896
- const newRecord: ChatRecord = {
897
- id: Date.now().toString(),
898
- name: text.slice(0, 30) + (text.length > 30 ? '...' : ''),
899
- content: text,
900
- type: 'text',
901
- createTime: new Date().toLocaleDateString(),
902
- userId: 'current-user',
903
- extraData: { messages: messages.value }
904
- };
1256
+ ### hook逻辑调用
905
1257
 
906
- records.value.unshift(newRecord);
907
- activeRecordId.value = newRecord.id;
1258
+ ```ts
1259
+ import type {
1260
+ BubbleEventAction,
1261
+ ChatMessage,
1262
+ ChatRecord,
1263
+ ChatRecordAction
1264
+ } from '@kernelift/ai-chat';
1265
+ import { formatDate, useAsyncState, useStorage } from '@vueuse/core';
1266
+ import { OpenAI } from 'openai/client';
1267
+ import type { ChatCompletionCreateParamsStreaming } from 'openai/resources/chat/completions/completions.mjs';
1268
+ import { computed, onUnmounted, ref, shallowRef } from 'vue';
1269
+ import { getModelList } from './api';
1270
+
1271
+ interface ChatError {
1272
+ message: string;
1273
+ code?: string;
1274
+ timestamp: number;
1275
+ }
1276
+
1277
+ interface ReasoningDelta {
1278
+ reasoning_content?: string;
1279
+ }
1280
+
1281
+ export const useChat = (options: {
1282
+ apiKey: string;
1283
+ baseURL?: string;
1284
+ model?: string;
1285
+ uuid?: string;
1286
+ }) => {
1287
+ const { apiKey, baseURL, model, uuid } = options;
1288
+ // 当前显示的消息列表
1289
+ const chatMessages = ref<ChatMessage[]>([]);
1290
+ // 所有消息记录
1291
+ const chatRecords = useStorage<ChatRecord[]>(`${uuid}-records`, []);
1292
+ // 显示工作区
1293
+ const showWorkspace = ref(false);
1294
+ // 发送中
1295
+ const senderLoading = ref(false);
1296
+ // 生成中
1297
+ const generateLoading = ref(false);
1298
+ // 新记录Id
1299
+ const newRecordId = ref<string | null>(null);
1300
+ // 当前激活的记录
1301
+ const activeRecordId = ref<string | null>(null);
1302
+ // 错误状态
1303
+ const lastError = ref<ChatError | null>(null);
1304
+
1305
+ const isNewRecord = computed(() => {
1306
+ return !!newRecordId.value && activeRecordId.value === null;
1307
+ });
1308
+
1309
+ // 流式传输
1310
+ const streamMode = ref<boolean>(true);
1311
+ // 输入问题
1312
+ const userQuestion = ref('');
1313
+ // 聊天模型
1314
+ const chatModel = ref(model || 'deepseek-ai/DeepSeek-V3.1-Terminus');
1315
+ // 主题模式
1316
+ const themeMode = ref<'light' | 'dark'>('light');
1317
+
1318
+ /**
1319
+ * @description 切换主题模式
1320
+ * @param mode
1321
+ */
1322
+ function changeThemeMode(mode: 'light' | 'dark') {
1323
+ themeMode.value = mode;
908
1324
  }
909
1325
 
910
- senderLoading.value = true;
911
- generateLoading.value = true;
1326
+ /**
1327
+ * @description 切换模型
1328
+ * @param newModel
1329
+ */
1330
+ function changeModel(newModel: string) {
1331
+ chatModel.value = newModel;
1332
+ }
912
1333
 
913
- try {
914
- // 模拟 AI 响应
915
- await simulateAIResponse(text, enableThink);
916
- } catch (error) {
917
- console.error('发送失败:', error);
918
- } finally {
919
- senderLoading.value = false;
920
- generateLoading.value = false;
1334
+ /**
1335
+ * @description 切换工作区显示状态
1336
+ */
1337
+ function changeShowWorkspace() {
1338
+ showWorkspace.value = !showWorkspace.value;
921
1339
  }
922
- };
923
1340
 
924
- // 模拟 AI 响应
925
- const simulateAIResponse = async (text: string, enableThink?: boolean) => {
926
- const responseId = Date.now().toString();
1341
+ /**
1342
+ * @description 切换流式传输模式
1343
+ * @param isStream
1344
+ */
1345
+ function changeStreamMode(isStream: boolean) {
1346
+ streamMode.value = isStream;
1347
+ }
927
1348
 
928
- messages.value.push({
929
- id: responseId,
930
- role: 'assistant',
931
- content: '',
932
- timestamp: Date.now(),
933
- loading: true,
934
- isThinking: enableThink
1349
+ const client = new OpenAI({
1350
+ apiKey: apiKey,
1351
+ baseURL: baseURL,
1352
+ // 危险,此处仅作为示范使用
1353
+ dangerouslyAllowBrowser: true
935
1354
  });
936
1355
 
937
- const targetMessage = messages.value.find((m) => m.id === responseId)!;
938
-
939
- // 模拟思考过程
940
- if (enableThink) {
941
- targetMessage.thoughtProcess = '正在分析用户问题...\n';
942
- await new Promise((resolve) => setTimeout(resolve, 1000));
943
- targetMessage.thoughtProcess += '整理相关信息...\n';
944
- await new Promise((resolve) => setTimeout(resolve, 1000));
945
- targetMessage.thoughtProcess += '生成回答...\n';
946
- await new Promise((resolve) => setTimeout(resolve, 500));
947
- targetMessage.isThinking = false;
1356
+ /**
1357
+ * @description 创建错误消息
1358
+ */
1359
+ function createErrorMessage(error: unknown): ChatError {
1360
+ let message = '请求失败,请稍后重试';
1361
+ let code: string | undefined;
1362
+
1363
+ if (error instanceof Error) {
1364
+ message = error.message;
1365
+ if ('code' in error) {
1366
+ code = String(error.code);
1367
+ }
1368
+ } else if (typeof error === 'string') {
1369
+ message = error;
1370
+ }
1371
+
1372
+ // Handle specific error types
1373
+ if (message.includes('abort')) {
1374
+ message = '请求已取消';
1375
+ code = 'ABORTED';
1376
+ } else if (message.includes('network')) {
1377
+ message = '网络连接失败,请检查网络设置';
1378
+ code = 'NETWORK_ERROR';
1379
+ } else if (message.includes('timeout')) {
1380
+ message = '请求超时,请稍后重试';
1381
+ code = 'TIMEOUT';
1382
+ } else if (message.includes('401')) {
1383
+ message = 'API密钥无效,请检查配置';
1384
+ code = 'UNAUTHORIZED';
1385
+ } else if (message.includes('429')) {
1386
+ message = '请求过于频繁,请稍后再试';
1387
+ code = 'RATE_LIMIT';
1388
+ }
1389
+
1390
+ return {
1391
+ message,
1392
+ code,
1393
+ timestamp: Date.now()
1394
+ };
948
1395
  }
949
1396
 
950
- // 模拟流式响应
951
- const response = `这是对"${text}"的回答。`;
952
- for (let i = 0; i < response.length; i++) {
953
- targetMessage.content += response[i];
954
- await new Promise((resolve) => setTimeout(resolve, 50));
1397
+ /**
1398
+ * @description 添加错误消息到聊天记录
1399
+ */
1400
+ function addErrorMessage(error: ChatError) {
1401
+ const lastMessage = chatMessages.value[chatMessages.value.length - 1];
1402
+ if (lastMessage && lastMessage.role === 'assistant') {
1403
+ lastMessage.error = error.message;
1404
+ lastMessage.loading = false;
1405
+ } else {
1406
+ chatMessages.value.push({
1407
+ id: Date.now().toString(),
1408
+ role: 'assistant',
1409
+ content: `<span style="color: red;">${error.message}</span>`,
1410
+ timestamp: Date.now(),
1411
+ error: error.message,
1412
+ isThinking: false
1413
+ });
1414
+ }
1415
+ lastError.value = error;
955
1416
  }
956
1417
 
957
- targetMessage.loading = false;
958
- };
1418
+ // 创建 AbortController
1419
+ const controller = shallowRef(new AbortController());
1420
+
1421
+ /**
1422
+ * @description 发送消息
1423
+ * @param value
1424
+ * @param enableThink
1425
+ * @param enableNet
1426
+ * @param needCreateRecord
1427
+ */
1428
+ async function handleSend(
1429
+ value: string,
1430
+ enableThink?: boolean,
1431
+ enableNet?: boolean,
1432
+ needCreateRecord?: boolean
1433
+ ) {
1434
+ // 0. 清除之前的错误状态
1435
+ lastError.value = null;
1436
+
1437
+ // 1. 清空输入框
1438
+ userQuestion.value = '';
1439
+ // 2. 添加用户输入记录
1440
+ chatMessages.value.push({
1441
+ id: Date.now().toString(),
1442
+ role: 'user',
1443
+ content: value,
1444
+ timestamp: Date.now(),
1445
+ isThinking: enableThink,
1446
+ extraData: {
1447
+ question: value
1448
+ }
1449
+ });
1450
+
1451
+ // 3. 如果需要创建记录,立即创建
1452
+ if (needCreateRecord) {
1453
+ const recordId = newRecordId.value || 'record-' + Date.now().toString();
1454
+ newRecordId.value = null;
1455
+ chatRecords.value.push({
1456
+ id: recordId,
1457
+ name: value.slice(0, 30) + (value.length > 30 ? '...' : ''),
1458
+ content: value,
1459
+ type: 'chat',
1460
+ createTime: formatDate(new Date(), 'YYYY-MM-DD HH:mm:ss'),
1461
+ userId: uuid || 'default',
1462
+ extraData: {
1463
+ messages: chatMessages.value
1464
+ }
1465
+ });
1466
+ activeRecordId.value = recordId;
1467
+ }
959
1468
 
960
- // 取消生成
961
- const handleCancel = () => {
962
- generateLoading.value = false;
963
- senderLoading.value = false;
1469
+ // 4. 添加机器人输入记录
1470
+ senderLoading.value = true;
1471
+ generateLoading.value = true;
1472
+ if (streamMode.value) {
1473
+ try {
1474
+ const stream = await client.chat.completions.create(
1475
+ {
1476
+ model: chatModel.value,
1477
+ messages: chatMessages.value.map((item) => {
1478
+ return {
1479
+ role: item.role,
1480
+ content: item.content
1481
+ };
1482
+ }),
1483
+ stream: true,
1484
+ enable_thinking: !!enableThink
1485
+ } as ChatCompletionCreateParamsStreaming,
1486
+ {
1487
+ signal: controller.value.signal
1488
+ }
1489
+ );
1490
+ // 5. 载入响应数据,并关闭生成加载
1491
+ generateLoading.value = false;
1492
+
1493
+ const targetId = Date.now().toString();
1494
+ chatMessages.value.push({
1495
+ id: targetId,
1496
+ role: 'assistant',
1497
+ content: '',
1498
+ timestamp: Date.now(),
1499
+ isThinking: false,
1500
+ extraData: {
1501
+ model: chatModel.value,
1502
+ userQuestion: value
1503
+ }
1504
+ });
1505
+ const targetMessage = chatMessages.value.find((item) => item.id === targetId)!;
1506
+ for await (const chunk of stream) {
1507
+ targetMessage.loading = true;
1508
+ const delta = chunk.choices[0]?.delta as ReasoningDelta | undefined;
1509
+
1510
+ if (
1511
+ enableThink &&
1512
+ delta?.reasoning_content &&
1513
+ targetMessage.content.length === 0 &&
1514
+ !chunk.choices[0]?.delta.content
1515
+ ) {
1516
+ if (!targetMessage.thoughtProcess) {
1517
+ targetMessage.thoughtProcess = '';
1518
+ }
1519
+ targetMessage.isThinking = true;
1520
+ targetMessage.thoughtProcess += delta.reasoning_content || '';
1521
+ } else {
1522
+ targetMessage.isThinking = false;
1523
+ }
1524
+ targetMessage.content += chunk.choices[0]?.delta.content || '';
1525
+ targetMessage.timestamp = Date.now();
1526
+ }
1527
+ targetMessage.loading = false;
1528
+ } catch (error: unknown) {
1529
+ const chatError = createErrorMessage(error);
1530
+ const targetMessage = chatMessages.value[chatMessages.value.length - 1];
1531
+
1532
+ if (targetMessage && targetMessage.role === 'assistant') {
1533
+ targetMessage.loading = false;
1534
+ targetMessage.timestamp = Date.now();
1535
+ targetMessage.error = chatError.message;
1536
+ } else {
1537
+ addErrorMessage(chatError);
1538
+ }
964
1539
 
965
- const lastMessage = messages.value[messages.value.length - 1];
966
- if (lastMessage?.loading) {
967
- lastMessage.loading = false;
968
- lastMessage.isTerminated = true;
1540
+ console.error('[AI Chat] Stream request failed:', error);
1541
+ } finally {
1542
+ senderLoading.value = false;
1543
+ }
1544
+ } else {
1545
+ try {
1546
+ const response = await client.chat.completions.create(
1547
+ {
1548
+ model: chatModel.value,
1549
+ messages: chatMessages.value.map((item) => {
1550
+ return {
1551
+ role: item.role,
1552
+ content: item.content
1553
+ };
1554
+ }),
1555
+ stream: false
1556
+ },
1557
+ {
1558
+ signal: controller.value.signal
1559
+ }
1560
+ );
1561
+
1562
+ chatMessages.value.push({
1563
+ id: Date.now().toString(),
1564
+ role: 'assistant',
1565
+ content: response.choices[0]?.message.content || '请求失败,请稍后重试',
1566
+ timestamp: Date.now(),
1567
+ isThinking: false,
1568
+ extraData: {
1569
+ model: chatModel.value,
1570
+ userQuestion: value
1571
+ }
1572
+ });
1573
+ } catch (error: unknown) {
1574
+ const chatError = createErrorMessage(error);
1575
+ addErrorMessage(chatError);
1576
+ console.error('[AI Chat] Non-stream request failed:', error);
1577
+ } finally {
1578
+ // 4. 载入响应数据,并关闭生成加载
1579
+ senderLoading.value = false;
1580
+ generateLoading.value = false;
1581
+ }
1582
+ }
969
1583
  }
970
- };
971
1584
 
972
- // 处理气泡事件
973
- const handleBubbleEvent = (event: BubbleEvent, data: ChatMessage) => {
974
- switch (event) {
975
- case 'like':
976
- data.isLiked = !data.isLiked;
977
- data.isDisliked = false;
978
- break;
979
- case 'dislike':
980
- data.isDisliked = !data.isDisliked;
981
- data.isLiked = false;
982
- break;
983
- case 'copy':
984
- navigator.clipboard.writeText(data.content);
985
- break;
986
- case 'bookmark':
987
- data.isBookmarked = !data.isBookmarked;
988
- break;
1585
+ /**
1586
+ * @description 取消当前请求
1587
+ */
1588
+ function handleCancel() {
1589
+ // 中止当前请求
1590
+ controller.value.abort();
1591
+
1592
+ // 创建新的控制器供下次使用
1593
+ controller.value = new AbortController();
1594
+
1595
+ // 如果正在生成,标记最后一条消息为已取消
1596
+ if (generateLoading.value || senderLoading.value) {
1597
+ const lastMessage = chatMessages.value[chatMessages.value.length - 1];
1598
+ if (lastMessage && lastMessage.role === 'assistant') {
1599
+ lastMessage.loading = false;
1600
+ // 如果消息内容为空,添加取消提示
1601
+ if (!lastMessage.content && !lastMessage.error) {
1602
+ lastMessage.error = '生成已取消';
1603
+ }
1604
+ lastMessage.timestamp = Date.now();
1605
+ lastMessage.content = '生成已取消,请重试。';
1606
+ }
1607
+ }
1608
+
1609
+ // 重置加载状态
1610
+ generateLoading.value = false;
1611
+ senderLoading.value = false;
1612
+
1613
+ console.log('[AI Chat] Request cancelled by user');
989
1614
  }
990
- };
991
1615
 
992
- // 注意:记录创建逻辑已集成到 handleSend 函数中
993
- // 当 needCreateRecord 为 true 时,在发送消息的同时创建新记录
1616
+ onUnmounted(() => {
1617
+ controller.value.abort();
1618
+ });
994
1619
 
995
- // 切换记录
996
- const handleChangeRecord = (record?: ChatRecord) => {
997
- if (record) {
998
- messages.value = record.extraData?.messages || [];
999
- activeRecordId.value = record.id;
1000
- } else {
1001
- messages.value = [];
1620
+ /**
1621
+ * @description 重试最后一条失败的消息
1622
+ */
1623
+ function handleRetry() {
1624
+ if (chatMessages.value.length < 2) return;
1625
+
1626
+ const lastAssistantMsg = chatMessages.value[chatMessages.value.length - 1];
1627
+ const lastUserMsg = chatMessages.value[chatMessages.value.length - 2];
1628
+
1629
+ if (lastAssistantMsg?.error && lastUserMsg?.role === 'user') {
1630
+ // 移除错误的助手消息
1631
+ chatMessages.value.pop();
1632
+ // 重新发送用户消息
1633
+ const userContent = lastUserMsg.content;
1634
+ const isThinking = lastUserMsg.isThinking;
1635
+ handleSend(userContent, isThinking, false, false);
1636
+ }
1637
+ }
1638
+
1639
+ /**
1640
+ * @description 处理记录变更
1641
+ * @param record
1642
+ */
1643
+ function handleChangeRecord(record?: ChatRecord) {
1644
+ if (record) {
1645
+ activeRecordId.value = record?.id || null;
1646
+ newRecordId.value = null;
1647
+ }
1648
+ chatMessages.value = record?.extraData?.messages || [];
1649
+ }
1650
+
1651
+ function handleCreateRecord() {
1652
+ chatMessages.value = [];
1653
+ newRecordId.value = 'record-' + Date.now().toString();
1002
1654
  activeRecordId.value = null;
1003
1655
  }
1004
- };
1005
- </script>
1656
+ handleCreateRecord();
1657
+
1658
+ const { state: availableModels, isLoading: isLoadingModels } = useAsyncState(
1659
+ async () => {
1660
+ const response = await getModelList();
1661
+ return response.data.data.map((item) => ({
1662
+ label: item.id,
1663
+ value: item.id
1664
+ }));
1665
+ },
1666
+ [],
1667
+ {
1668
+ immediate: true
1669
+ }
1670
+ );
1006
1671
 
1007
- <style scoped>
1008
- .chat-app {
1009
- height: 100vh;
1010
- padding: 20px;
1011
- background: #f5f5f5;
1012
- }
1672
+ const showEditNameDialog = ref(false);
1673
+ const editRecord = ref<ChatRecord | null>(null);
1013
1674
 
1014
- .empty-state {
1015
- text-align: center;
1016
- padding: 60px 20px;
1017
- }
1675
+ const chatRecordActions: ChatRecordAction[] = [
1676
+ {
1677
+ key: 'edit',
1678
+ label: '编辑名称',
1679
+ icon: 'pi pi-pencil text-sm',
1680
+ handler: (record: ChatRecord) => {
1681
+ editRecord.value = record;
1682
+ showEditNameDialog.value = true;
1683
+ }
1684
+ },
1685
+ {
1686
+ key: 'delete',
1687
+ label: '删除',
1688
+ icon: 'pi pi-trash text-sm',
1689
+ handler: (record: ChatRecord) => {
1690
+ const index = chatRecords.value.findIndex((item) => item.id === record.id);
1691
+ if (index !== -1) {
1692
+ chatRecords.value.splice(index, 1);
1693
+ }
1694
+ // 如果删除的是当前激活的记录,清空消息列表
1695
+ if (activeRecordId.value === record.id) {
1696
+ chatMessages.value = [];
1697
+ activeRecordId.value = null;
1698
+ }
1699
+ }
1700
+ }
1701
+ ];
1702
+
1703
+ /**
1704
+ * @description 更新记录名称
1705
+ * @param record
1706
+ * @param newName
1707
+ */
1708
+ function updateRecordName(record: ChatRecord, newName: string) {
1709
+ const targetRecord = chatRecords.value.find((item) => item.id === record.id);
1710
+ if (targetRecord) {
1711
+ targetRecord.content = newName;
1712
+ }
1713
+ }
1018
1714
 
1019
- .welcome-title {
1020
- font-size: 32px;
1021
- font-weight: bold;
1022
- margin-bottom: 16px;
1023
- color: var(--kl-chat-primary-color);
1024
- }
1715
+ const bubbleEventActions: BubbleEventAction[] = [
1716
+ {
1717
+ key: 'info',
1718
+ icon: 'pi pi-info-circle',
1719
+ label: '信息'
1720
+ }
1721
+ ];
1025
1722
 
1026
- .welcome-desc {
1027
- font-size: 16px;
1028
- color: var(--kl-note-color);
1029
- line-height: 1.6;
1030
- }
1723
+ const showMessageDetailDialog = ref(false);
1724
+ const messageDetail = ref<ChatMessage | null>(null);
1031
1725
 
1032
- .brand-logo {
1033
- display: flex;
1034
- align-items: center;
1035
- gap: 8px;
1036
- font-size: 18px;
1037
- font-weight: bold;
1038
- }
1039
- </style>
1726
+ const showEditMessageDialog = ref(false);
1727
+ const editMessage = ref<ChatMessage | null>(null);
1728
+ function handleEditMessageContent(newContent: string) {
1729
+ if (editMessage.value) {
1730
+ editMessage.value.content = newContent;
1731
+ }
1732
+ }
1733
+
1734
+ function handleBubbleEvent(event: string, data: ChatMessage) {
1735
+ switch (event) {
1736
+ case 'like':
1737
+ data.isLiked = !data.isLiked;
1738
+ break;
1739
+ case 'dislike':
1740
+ data.isDisliked = !data.isDisliked;
1741
+ break;
1742
+ case 'delete':
1743
+ const index = chatMessages.value.findIndex((item) => item.id === data.id);
1744
+ if (index !== -1) {
1745
+ chatMessages.value.splice(index, 1);
1746
+ }
1747
+ break;
1748
+ case 'terminate':
1749
+ handleCancel();
1750
+ break;
1751
+ case 'reload':
1752
+ userQuestion.value = data.extraData?.question || '';
1753
+ break;
1754
+ case 'copy':
1755
+ navigator.clipboard.writeText(data.content || '');
1756
+ break;
1757
+ case 'edit':
1758
+ editMessage.value = data;
1759
+ showEditMessageDialog.value = true;
1760
+ break;
1761
+ case 'info':
1762
+ messageDetail.value = data;
1763
+ showMessageDetailDialog.value = true;
1764
+ break;
1765
+ default:
1766
+ break;
1767
+ }
1768
+ }
1769
+
1770
+ return {
1771
+ bubbleEventActions,
1772
+ chatRecordActions,
1773
+ isLoadingModels,
1774
+ chatMessages,
1775
+ chatRecords,
1776
+ showWorkspace,
1777
+ senderLoading,
1778
+ generateLoading,
1779
+ activeRecordId,
1780
+ streamMode,
1781
+ userQuestion,
1782
+ lastError,
1783
+ themeMode,
1784
+ handleCreateRecord,
1785
+ isNewRecord,
1786
+ chatModel,
1787
+ availableModels,
1788
+ changeThemeMode,
1789
+ changeModel,
1790
+ handleSend,
1791
+ handleCancel,
1792
+ handleRetry,
1793
+ handleChangeRecord,
1794
+ changeShowWorkspace,
1795
+ changeStreamMode,
1796
+
1797
+ showEditNameDialog,
1798
+ editRecord,
1799
+ updateRecordName,
1800
+
1801
+ handleBubbleEvent,
1802
+ showMessageDetailDialog,
1803
+ messageDetail,
1804
+ showEditMessageDialog,
1805
+ editMessage,
1806
+ handleEditMessageContent
1807
+ };
1808
+ };
1040
1809
  ```
1041
1810
 
1042
1811
  ## 📖 API 文档
@@ -1109,399 +1878,13 @@ const handleChangeRecord = (record?: ChatRecord) => {
1109
1878
  }
1110
1879
  ```
1111
1880
 
1112
- ## 使用示例
1113
-
1114
- ### 完整聊天应用
1115
-
1116
- ```vue
1117
- <script setup lang="ts">
1118
- import {
1119
- ChatContainer,
1120
- type BubbleEvent,
1121
- type ChatMessage,
1122
- type ChatRecord,
1123
- type ChatRecordAction
1124
- } from '@kernelift/ai-chat';
1125
- import '@kernelift/ai-chat/style.css';
1126
- import OpenAI from 'openai';
1127
- import { useStorage } from '@vueuse/core';
1128
- import type { ChatCompletionCreateParamsStreaming } from 'openai/resources';
1129
- import { onUnmounted, ref, shallowRef } from 'vue';
1130
-
1131
- // 当前显示的消息列表
1132
- const demoMessages = ref<ChatMessage[]>([]);
1133
- // 所有消息记录
1134
- const demoRecords = useStorage<ChatRecord[]>('demo-records', []);
1135
- // 显示工作区
1136
- const showWorkspace = ref(false);
1137
- // 发送中
1138
- const senderLoading = ref(false);
1139
- // 生成中
1140
- const generateLoading = ref(false);
1141
- // 当前激活的记录
1142
- const activeRecordId = ref<string | null>(null);
1143
-
1144
- //
1145
-
1146
- // 流式传输
1147
- const streamMode = ref<boolean>(true);
1148
- // 输入问题
1149
- const userQuestion = ref('');
1150
-
1151
- // const chatModel = ref('deepseek-ai/DeepSeek-V3.1-Terminus');
1152
-
1153
- const client = new OpenAI({
1154
- apiKey: 'sk-xxx',
1155
- baseURL: 'https://api.siliconflow.cn/v1',
1156
- // 危险,此处仅作为示范使用
1157
- dangerouslyAllowBrowser: true
1158
- });
1159
-
1160
- // 创建 AbortController
1161
- const controller = shallowRef(new AbortController());
1162
-
1163
- /**
1164
- * @description 发送消息
1165
- * @param value
1166
- * @param enableThink
1167
- * @param enableNet
1168
- */
1169
- async function handleSend(value: string, enableThink?: boolean) {
1170
- // 1. 清空输入框
1171
- userQuestion.value = '';
1172
- // 2. 添加用户输入记录
1173
- demoMessages.value.push({
1174
- id: Date.now().toString(),
1175
- role: 'user',
1176
- content: value,
1177
- timestamp: Date.now(),
1178
- isThinking: enableThink,
1179
- extraData: {
1180
- question: value
1181
- }
1182
- });
1183
- // 3. 添加机器人输入记录
1184
- senderLoading.value = true;
1185
- generateLoading.value = true;
1186
- if (streamMode.value) {
1187
- try {
1188
- const stream = await client.chat.completions.create(
1189
- {
1190
- model: 'deepseek-ai/DeepSeek-V3.1-Terminus',
1191
- messages: demoMessages.value.map((item) => {
1192
- return {
1193
- role: item.role,
1194
- content: item.content
1195
- };
1196
- }),
1197
- stream: true,
1198
- enable_thinking: !!enableThink
1199
- } as ChatCompletionCreateParamsStreaming,
1200
- {
1201
- signal: controller.value.signal
1202
- }
1203
- );
1204
- // 4. 载入响应数据,并关闭生成加载
1205
- generateLoading.value = false;
1206
-
1207
- const targetId = Date.now().toString();
1208
- demoMessages.value.push({
1209
- id: targetId,
1210
- role: 'assistant',
1211
- content: '',
1212
- timestamp: Date.now(),
1213
- isThinking: false
1214
- });
1215
- const targetMessage = demoMessages.value.find((item) => item.id === targetId)!;
1216
- for await (const chunk of stream) {
1217
- targetMessage.loading = true;
1218
- if (
1219
- enableThink &&
1220
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1221
- (chunk.choices[0]?.delta as any).reasoning_content &&
1222
- targetMessage.content.length === 0 &&
1223
- !chunk.choices[0]?.delta.content
1224
- ) {
1225
- if (!targetMessage.thoughtProcess) {
1226
- targetMessage.thoughtProcess = '';
1227
- }
1228
- targetMessage.isThinking = true;
1229
-
1230
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1231
- targetMessage.thoughtProcess += (chunk.choices[0]?.delta as any).reasoning_content || '';
1232
- } else {
1233
- targetMessage.isThinking = false;
1234
- }
1235
- targetMessage.content = targetMessage.content + (chunk.choices[0]?.delta.content || '');
1236
- targetMessage.timestamp = Date.now();
1237
- }
1238
- targetMessage.loading = false;
1239
- } catch {
1240
- // 请求失败处理
1241
- // TODO
1242
- // ElNotification.error('请求失败,请稍后重试');
1243
- const targetMessage = demoMessages.value[demoMessages.value.length - 1]!;
1244
- targetMessage.loading = false;
1245
- targetMessage.timestamp = Date.now();
1246
- targetMessage.error = '请求失败,请稍后重试';
1247
- } finally {
1248
- senderLoading.value = false;
1249
- }
1250
- } else {
1251
- try {
1252
- const response = await client.chat.completions.create(
1253
- {
1254
- model: 'deepseek-ai/DeepSeek-V3.1-Terminus',
1255
- messages: demoMessages.value.map((item) => {
1256
- return {
1257
- role: item.role,
1258
- content: item.content
1259
- };
1260
- }),
1261
- stream: false
1262
- },
1263
- {
1264
- signal: controller.value.signal
1265
- }
1266
- );
1267
-
1268
- demoMessages.value.push({
1269
- id: Date.now().toString(),
1270
- role: 'assistant',
1271
- content: response.choices[0]?.message.content || '请求失败,请稍后重试',
1272
- timestamp: Date.now(),
1273
- isThinking: false
1274
- });
1275
- } catch {
1276
- // TODO: 错误处理
1277
- // ElNotification.error('请求失败,请稍后重试');
1278
- } finally {
1279
- // 4. 载入响应数据,并关闭生成加载
1280
- senderLoading.value = false;
1281
- generateLoading.value = false;
1282
- }
1283
- }
1284
- }
1285
-
1286
- function handleCancel() {
1287
- controller.value.abort();
1288
- controller.value = new AbortController();
1289
- if (generateLoading.value) {
1290
- }
1291
- generateLoading.value = false;
1292
- senderLoading.value = false;
1293
- }
1294
-
1295
- onUnmounted(() => {
1296
- controller.value.abort();
1297
- });
1298
-
1299
- // 记录创建逻辑已整合到 handleSend 中
1300
- // 通过 needCreateRecord 参数判断是否需要创建新记录
1301
-
1302
- function handleChangeRecord(record?: ChatRecord) {
1303
- demoMessages.value = record?.extraData?.messages || [];
1304
- }
1305
-
1306
- function handleBubbleEvent(event: BubbleEvent, data: ChatMessage) {
1307
- switch (event) {
1308
- case 'like':
1309
- data.isLiked = !data.isLiked;
1310
- break;
1311
- case 'dislike':
1312
- data.isDisliked = !data.isDisliked;
1313
- break;
1314
- case 'bookmark':
1315
- data.isBookmarked = !data.isBookmarked;
1316
- break;
1317
- case 'terminate':
1318
- data.isTerminated = true;
1319
- break;
1320
- case 'copy':
1321
- navigator.clipboard.writeText(data.content);
1322
- // TODO: 添加复制成功提示
1323
- // ElMessage.success('复制成功');
1324
- }
1325
- }
1326
-
1327
- const recordButtons: ChatRecordAction[] = [
1328
- {
1329
- id: 'edit',
1330
- name: '编辑',
1331
- icon: 'edit',
1332
- action: () => {}
1333
- },
1334
- {
1335
- id: 'delete',
1336
- name: '删除',
1337
- icon: 'delete',
1338
- action: (record) => {
1339
- demoRecords.value = demoRecords.value.filter((item) => item.id !== record.id);
1340
- }
1341
- }
1342
- ];
1343
-
1344
- const themeMode = ref<'light' | 'dark'>('light');
1345
-
1346
- function handleScrollBottom() {
1347
- // TODO: 滚动到底部
1348
- console.log('滚动到底部');
1349
- }
1350
- </script>
1351
-
1352
- <template>
1353
- <div class="w-full h-full relative">
1354
- <ChatContainer
1355
- v-model="userQuestion"
1356
- v-model:loading="senderLoading"
1357
- v-model:messages="demoMessages"
1358
- v-model:record-id="activeRecordId"
1359
- :is-generate-loading="generateLoading"
1360
- :records="demoRecords"
1361
- :record-actions="recordButtons"
1362
- :show-workspace="showWorkspace"
1363
- :has-sender-tools="true"
1364
- :show-sender="true"
1365
- has-theme-mode
1366
- :markdown-class-name="themeMode === 'dark' ? 'prose-invert' : 'prose'"
1367
- :enable-think="true"
1368
- :enable-net="false"
1369
- :theme-mode="themeMode"
1370
- :input-height="80"
1371
- @send="handleSend"
1372
- @cancel="handleCancel"
1373
- @close-workspace="showWorkspace = false"
1374
- @change-record="handleChangeRecord"
1375
- @bubble-event="handleBubbleEvent"
1376
- @change-theme="(mode) => (themeMode = mode)"
1377
- @scroll-bottom="handleScrollBottom"
1378
- >
1379
- <template #sender-tools>
1380
- <div class="px-3 flex items-center h-full">
1381
- <div
1382
- class="border border-amber-600 rounded py-1 px-2 text-sm ml-auto text-amber-600 hover:brightness-125 hover:bg-amber-500/15 cursor-pointer"
1383
- @click="showWorkspace = !showWorkspace"
1384
- >
1385
- 会话空间
1386
- </div>
1387
- </div>
1388
- </template>
1389
- <template #empty>
1390
- <div class="text-center">
1391
- <div class="italic font-bold text-3xl mb-3">AI计量助手</div>
1392
- <div>
1393
- 你好!很高兴见到你!😊
1394
- <div>
1395
- 有什么我可以帮助你的吗?无论是回答问题、聊天还是其他任何需要,我都很乐意为你提供帮助!
1396
- </div>
1397
- </div>
1398
- </div>
1399
- </template>
1400
- <template #logo>
1401
- <div class="text-xl mb-2 font-bold">AI计量助手</div>
1402
- </template>
1403
- <template #header-logo>
1404
- <div class="text-lg font-bold ml-3">AI计量助手</div>
1405
- </template>
1406
-
1407
- <template #workspace="{ record }">
1408
- <div class="p-3 relative overflow-auto w-full h-full workspace-area">
1409
- 在工作区展示记录的一些详细信息或额外内容
1410
- <div class="text-base mt-8 whitespace-pre-line bg-gray-200 p-3">
1411
- {{ record }}
1412
- </div>
1413
- </div>
1414
- </template>
1415
-
1416
- <template #record-dropdown>
1417
- <div class="text-gray-300 italic absolute top-0 right-0">记录下拉菜单</div>
1418
- </template>
1419
-
1420
- <template #bubble-header="{ data }">
1421
- <div v-if="data.id === '1765436189938'" class="text-gray-300 italic">气泡头部</div>
1422
- </template>
1423
- <template #bubble-footer="{ data }">
1424
- <div v-if="data.id === '1765436189938'" class="text-gray-300 italic">
1425
- 气泡底部会覆盖掉操作按钮
1426
- </div>
1427
- </template>
1428
- <template #bubble-event="{ data }">
1429
- <div v-if="data.id === '1765436289340'" class="ml-auto">可以有很多其他按钮</div>
1430
- </template>
1431
- <template #bubble-content-header="{ data }">
1432
- <div v-if="data.id === '1765436189938'" class="text-gray-300 italic">
1433
- 我这里可以插入头部
1434
- </div>
1435
- </template>
1436
- <template #bubble-content-footer="{ data }">
1437
- <div v-if="data.id === '1765436189938'" class="text-gray-300 italic">
1438
- 我这里可以插入底部
1439
- </div>
1440
- </template>
1441
-
1442
- <template #bubble-thinking-header="{ data }">
1443
- <div v-if="data.id === '1765436189938'" class="text-gray-300 italic">
1444
- 在思考区域搞点事情
1445
- </div>
1446
- </template>
1447
-
1448
- <template #sender-footer-tools>
1449
- <div class="text-gray-300 italic">这里可以插入一些按钮元素</div>
1450
- </template>
1451
-
1452
- <template #sender-button>
1453
- <div class="border border-gray-300 bg-amber-600">发送</div>
1454
- </template>
1455
- </ChatContainer>
1456
- </div>
1457
- </template>
1458
-
1459
- <style lang="scss">
1460
- .workspace-area {
1461
- --scrollbar-width: 8px;
1462
- --scrollbar-border-radius: 4px;
1463
- --scrollbar-track-color: transparent;
1464
- --scrollbar-thumb-color: rgba(var(--kl-chat-primary-rgb), 0.3);
1465
- --scrollbar-thumb-hover-color: rgba(var(--kl-chat-primary-rgb), 0.6);
1466
- --scrollbar-thumb-active-color: rgba(var(--kl-chat-primary-rgb), 0.8);
1467
-
1468
- &::-webkit-scrollbar {
1469
- width: var(--scrollbar-width);
1470
- height: var(--scrollbar-width);
1471
- opacity: 0;
1472
- transition: opacity 0.3s ease;
1473
- }
1474
-
1475
- &::-webkit-scrollbar-track {
1476
- background: var(--scrollbar-track-color);
1477
- border-radius: var(--scrollbar-border-radius);
1478
- margin: 2px;
1479
- }
1480
-
1481
- &::-webkit-scrollbar-thumb {
1482
- background: var(--scrollbar-thumb-color);
1483
- border-radius: var(--scrollbar-border-radius);
1484
- transition: all 0.3s ease;
1485
- cursor: pointer;
1486
-
1487
- &:hover {
1488
- background: var(--scrollbar-thumb-hover-color);
1489
- }
1490
-
1491
- &:active {
1492
- background: var(--scrollbar-thumb-active-color);
1493
- }
1494
- }
1495
- }
1496
- </style>
1497
- ```
1498
-
1499
1881
  ### Props 属性
1500
1882
 
1501
1883
  | 属性名 | 类型 | 默认值 | 说明 |
1502
1884
  | ----------------------- | ------------------------ | ----------- | -------------------------------------------------- |
1503
1885
  | `records` | `ChatRecord[]` | `[]` | 聊天记录列表 |
1504
1886
  | `recordActions` | `ChatRecordAction[]` | `[]` | 记录操作按钮配置 |
1887
+ | `bubbleExtEvents` | `BubbleEventAction[]` | `[]` | 气泡扩展事件配置 |
1505
1888
  | `hasHeader` | `boolean` | `true` | 是否显示头部 |
1506
1889
  | `headerHeight` | `number` | `38` | 头部高度 (px) |
1507
1890
  | `hasThemeMode` | `boolean` | `false` | 是否支持主题切换 |
@@ -1520,7 +1903,9 @@ function handleScrollBottom() {
1520
1903
  | `themeMode` | `'light' \| 'dark'` | `'light'` | 主题模式 |
1521
1904
  | `enableNet` | `boolean` | `undefined` | 联网搜索启用状态 |
1522
1905
  | `enableThink` | `boolean` | `undefined` | 深度思考启用状态 |
1523
- | `inputHeight` | `number` | `140` | 输入框高度 |
1906
+ | `disabledCreateRecord` | `boolean` | `false` | 是否禁用新建聊天记录 |
1907
+ | `inputHeight` | `number` | `140` | 输入框最大高度 (px) |
1908
+ | `defaultInputHeight` | `number` | `62` | 输入框初始默认高度 (px) |
1524
1909
  | `onCopy` | `(code: string) => void` | `undefined` | 复制代码回调 |
1525
1910
  | `i18n` | `Record<string, any>` | `zhCN` | 国际化配置 |
1526
1911
  | `autoScroll` | `boolean` | `true` | 是否自动滚动到底部 |
@@ -1542,49 +1927,51 @@ function handleScrollBottom() {
1542
1927
 
1543
1928
  ### Events 事件
1544
1929
 
1545
- | 事件名 | 参数 | 说明 |
1546
- | -------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------- |
1547
- | `send` | `(text: string, enableThink?: boolean, enableNet?: boolean, needCreateRecord?: boolean)` | 发送消息,needCreateRecord 表示是否需要创建新记录 |
1548
- | `cancel` | - | 取消生成 |
1549
- | `clear` | - | 清空聊天 |
1550
- | `change-record` | `(record?: ChatRecord)` | 切换记录 |
1551
- | `change-collapse` | `(collapse: boolean)` | 折叠状态改变 |
1552
- | `change-theme` | `(theme: 'light' \| 'dark')` | 主题切换 |
1553
- | `change-aside-width` | `(width: number)` | 侧边栏宽度改变 |
1554
- | `click-logo` | - | 点击Logo |
1555
- | `bubble-event` | `(event: BubbleEvent \| string, message: ChatMessage)` | 气泡交互事件(内置事件 + 自定义 ext-events) |
1556
- | `close-workspace` | - | 关闭工作区 |
1557
- | `scroll-bottom` | - | 滚动到底部 |
1930
+ | 事件名 | 参数 | 说明 |
1931
+ | ------------------------ | ---------------------------------------------------------------------------------------- | ------------------------------------------------- |
1932
+ | `send` | `(text: string, enableThink?: boolean, enableNet?: boolean, needCreateRecord?: boolean)` | 发送消息,needCreateRecord 表示是否需要创建新记录 |
1933
+ | `cancel` | - | 取消生成 |
1934
+ | `clear` | - | 清空聊天 |
1935
+ | `change-record` | `(record?: ChatRecord)` | 切换记录 |
1936
+ | `change-collapse` | `(collapse: boolean)` | 折叠状态改变 |
1937
+ | `change-theme` | `(theme: 'light' \| 'dark')` | 主题切换 |
1938
+ | `change-aside-width` | `(width: number)` | 侧边栏宽度改变 |
1939
+ | `change-workspace-width` | `(widthPercent: number)` | 工作区宽度占比改变 |
1940
+ | `click-logo` | - | 点击Logo |
1941
+ | `bubble-event` | `(event: BubbleEvent \| string, message: ChatMessage)` | 气泡交互事件(内置事件 + 自定义 ext-events) |
1942
+ | `close-workspace` | - | 关闭工作区 |
1943
+ | `scroll-bottom` | - | 滚动到底部 |
1558
1944
 
1559
1945
  ### Slots 插槽
1560
1946
 
1561
- | 插槽名 | 参数 | 说明 |
1562
- | ------------------------ | ------------------------------------------------------------------------------------------------ | -------------- |
1563
- | `left-aside` | `{ mobile: boolean }` | 左侧边栏 |
1564
- | `aside` | `{ record: ChatRecord \| undefined, mobile: boolean }` | 主侧边栏 |
1565
- | `logo` | `{ mobile: boolean }` | Logo区域 |
1566
- | `new-chat-button` | `{ mobile: boolean, execute: Function }` | 新建聊天按钮 |
1567
- | `record-dropdown` | `{ mobile: boolean }` | 记录下拉菜单 |
1568
- | `header` | `{ record: ChatRecord \| undefined, mobile: boolean }` | 头部区域 |
1569
- | `header-logo` | `{ mobile: boolean }` | 头部Logo |
1570
- | `collapsed-header-extra` | `{ mobile: boolean }` | 折叠头部额外区 |
1571
- | `header-extra` | `{ mobile: boolean }` | 头部额外区 |
1572
- | `bubble-header` | `{ data: ChatMessage, mobile: boolean }` | 气泡头部 |
1573
- | `bubble-footer` | `{ data: ChatMessage, mobile: boolean }` | 气泡底部 |
1574
- | `bubble-event` | `{ data: ChatMessage, mobile: boolean }` | 气泡操作区 |
1575
- | `bubble-content-header` | `{ data: ChatMessage, mobile: boolean }` | 气泡内容头部 |
1576
- | `bubble-content-footer` | `{ data: ChatMessage, mobile: boolean }` | 气泡内容底部 |
1577
- | `bubble-thinking-header` | `{ data: ChatMessage, mobile: boolean }` | 思考过程头部 |
1578
- | `bubble-loading-content` | `{ mobile: boolean }` | 加载内容 |
1579
- | `empty` | `{ mobile: boolean }` | 空状态 |
1580
- | `sender-tools` | `{ mobile: boolean }` | 发送工具区 |
1581
- | `sender-footer-tools` | `{ value: string, loading: boolean, enableNet: boolean, enableThink: boolean, mobile: boolean }` | 发送器底部工具 |
1582
- | `footer` | `{ mobile: boolean }` | 底部区域 |
1583
- | `workspace` | `{ record: ChatRecord \| undefined, mobile: boolean }` | 工作区 |
1584
- | `send-button` | `{ state: object, execute: Function, mobile: boolean }` | 发送按钮 |
1585
- | `think-button` | `{ state: object, execute: Function, mobile: boolean }` | 思考按钮 |
1586
- | `net-button` | `{ state: object, execute: Function, mobile: boolean }` | 联网按钮 |
1587
- | `sender-textarea` | `{ state: object, execute: Function, mobile: boolean, height: number }` | 输入框 |
1947
+ | 插槽名 | 参数 | 说明 |
1948
+ | ------------------------ | -------------------------------------------------------------------------------------------------------- | -------------- |
1949
+ | `left-aside` | `{ mobile: boolean }` | 左侧边栏 |
1950
+ | `aside` | `{ record: ChatRecord \| undefined, mobile: boolean }` | 主侧边栏 |
1951
+ | `record-footer` | `{ record: ChatRecord \| undefined, mobile: boolean }` | 记录底部区域 |
1952
+ | `logo` | `{ mobile: boolean }` | Logo区域 |
1953
+ | `new-chat-button` | `{ mobile: boolean, execute: Function, disabled: boolean }` | 新建聊天按钮 |
1954
+ | `record-dropdown` | `{ mobile: boolean }` | 记录下拉菜单 |
1955
+ | `header` | `{ record: ChatRecord \| undefined, mobile: boolean }` | 头部区域 |
1956
+ | `header-logo` | `{ mobile: boolean }` | 头部Logo |
1957
+ | `collapsed-header-extra` | `{ mobile: boolean }` | 折叠头部额外区 |
1958
+ | `header-extra` | `{ mobile: boolean }` | 头部额外区 |
1959
+ | `bubble-header` | `{ data: ChatMessage, mobile: boolean }` | 气泡头部 |
1960
+ | `bubble-footer` | `{ data: ChatMessage, mobile: boolean }` | 气泡底部 |
1961
+ | `bubble-event` | `{ data: ChatMessage, mobile: boolean }` | 气泡操作区 |
1962
+ | `bubble-content-header` | `{ data: ChatMessage, mobile: boolean }` | 气泡内容头部 |
1963
+ | `bubble-content-footer` | `{ data: ChatMessage, mobile: boolean }` | 气泡内容底部 |
1964
+ | `bubble-thinking-header` | `{ data: ChatMessage, mobile: boolean }` | 思考过程头部 |
1965
+ | `bubble-loading-content` | `{ mobile: boolean }` | 加载内容 |
1966
+ | `empty` | `{ mobile: boolean }` | 空状态 |
1967
+ | `sender-tools` | `{ mobile: boolean }` | 发送工具区 |
1968
+ | `sender-footer-tools` | `{ value: string, loading: boolean, enableNet: boolean, enableThink: boolean, mobile: boolean }` | 发送器底部工具 |
1969
+ | `footer` | `{ mobile: boolean }` | 底部区域 |
1970
+ | `workspace` | `{ record: ChatRecord \| undefined, mobile: boolean }` | 工作区 |
1971
+ | `send-button` | `{ state: { loading: boolean, inputText: string }, execute: Function, mobile: boolean }` | 发送按钮 |
1972
+ | `think-button` | `{ state: { hasThinking: boolean, enableThink: boolean }, execute: Function, mobile: boolean }` | 思考按钮 |
1973
+ | `net-button` | `{ state: { hasNetSearch: boolean, enableNet: boolean }, execute: Function, mobile: boolean }` | 联网按钮 |
1974
+ | `sender-textarea` | `{ state: { loading: boolean, inputText: string }, execute: Function, mobile: boolean, height: number }` | 输入框 |
1588
1975
 
1589
1976
  ### Exposed 方法
1590
1977