@kernelift/ai-chat 2.7.0 → 2.7.1
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/CHANGELOG.md +6 -0
- package/README.md +1008 -621
- package/dist/ai-chat.css +1 -1
- package/dist/index.d.ts +56 -4
- package/dist/index.js +20 -17
- package/package.json +1 -1
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="
|
|
874
|
+
<div class="w-full h-full relative">
|
|
798
875
|
<ChatContainer
|
|
799
|
-
v-model="
|
|
876
|
+
v-model="userQuestion"
|
|
800
877
|
v-model:loading="senderLoading"
|
|
801
|
-
v-model:messages="
|
|
802
|
-
|
|
803
|
-
:records="records"
|
|
804
|
-
:theme-mode="themeMode"
|
|
878
|
+
v-model:messages="chatMessages"
|
|
879
|
+
:records="chatRecords"
|
|
805
880
|
:is-generate-loading="generateLoading"
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
has-
|
|
809
|
-
|
|
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="
|
|
819
|
-
<
|
|
820
|
-
<div class="
|
|
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
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
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
|
-
<
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
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
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
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
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
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
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
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
|
-
|
|
907
|
-
|
|
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
|
-
|
|
911
|
-
|
|
1326
|
+
/**
|
|
1327
|
+
* @description 切换模型
|
|
1328
|
+
* @param newModel
|
|
1329
|
+
*/
|
|
1330
|
+
function changeModel(newModel: string) {
|
|
1331
|
+
chatModel.value = newModel;
|
|
1332
|
+
}
|
|
912
1333
|
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
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
|
-
|
|
925
|
-
|
|
926
|
-
|
|
1341
|
+
/**
|
|
1342
|
+
* @description 切换流式传输模式
|
|
1343
|
+
* @param isStream
|
|
1344
|
+
*/
|
|
1345
|
+
function changeStreamMode(isStream: boolean) {
|
|
1346
|
+
streamMode.value = isStream;
|
|
1347
|
+
}
|
|
927
1348
|
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
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
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
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
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
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
|
-
|
|
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
|
-
|
|
962
|
-
|
|
963
|
-
|
|
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
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
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
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
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
|
-
|
|
993
|
-
|
|
1616
|
+
onUnmounted(() => {
|
|
1617
|
+
controller.value.abort();
|
|
1618
|
+
});
|
|
994
1619
|
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1008
|
-
|
|
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
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
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
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
}
|
|
1715
|
+
const bubbleEventActions: BubbleEventAction[] = [
|
|
1716
|
+
{
|
|
1717
|
+
key: 'info',
|
|
1718
|
+
icon: 'pi pi-info-circle',
|
|
1719
|
+
label: '信息'
|
|
1720
|
+
}
|
|
1721
|
+
];
|
|
1025
1722
|
|
|
1026
|
-
|
|
1027
|
-
|
|
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
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
}
|
|
1039
|
-
|
|
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
|
-
| `
|
|
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`
|
|
1548
|
-
| `cancel`
|
|
1549
|
-
| `clear`
|
|
1550
|
-
| `change-record`
|
|
1551
|
-
| `change-collapse`
|
|
1552
|
-
| `change-theme`
|
|
1553
|
-
| `change-aside-width`
|
|
1554
|
-
| `
|
|
1555
|
-
| `
|
|
1556
|
-
| `
|
|
1557
|
-
| `
|
|
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
|
-
| `
|
|
1566
|
-
| `
|
|
1567
|
-
| `
|
|
1568
|
-
| `
|
|
1569
|
-
| `header
|
|
1570
|
-
| `
|
|
1571
|
-
| `header-extra`
|
|
1572
|
-
| `
|
|
1573
|
-
| `bubble-
|
|
1574
|
-
| `bubble-
|
|
1575
|
-
| `bubble-
|
|
1576
|
-
| `bubble-content-
|
|
1577
|
-
| `bubble-
|
|
1578
|
-
| `bubble-
|
|
1579
|
-
| `
|
|
1580
|
-
| `
|
|
1581
|
-
| `sender-
|
|
1582
|
-
| `footer`
|
|
1583
|
-
| `
|
|
1584
|
-
| `
|
|
1585
|
-
| `
|
|
1586
|
-
| `
|
|
1587
|
-
| `
|
|
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
|
|