@supermsy/smart-chat-widget 1.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.
@@ -0,0 +1,237 @@
1
+ <template>
2
+ <div class="chat-window">
3
+ <ChatHeader
4
+ @minimize="$emit('minimize')"
5
+ @close="handleClose" />
6
+
7
+ <ChatMessageList
8
+ :messages="messages"
9
+ :user-info="userInfo" />
10
+
11
+ <ChatInput
12
+ :disabled="isStreaming"
13
+ :enable-transfer="enableTransfer"
14
+ @send="$emit('send-message', $event)"
15
+ @transfer="$emit('transfer')" />
16
+
17
+ <div
18
+ v-if="showConfirmModal"
19
+ class="modal-overlay"
20
+ @click="showConfirmModal = false">
21
+ <div
22
+ class="confirm-modal"
23
+ @click.stop>
24
+ <div class="modal-icon">
25
+ <svg
26
+ viewBox="0 0 24 24"
27
+ fill="none"
28
+ stroke="#f59e0b"
29
+ stroke-width="2">
30
+ <circle
31
+ cx="12"
32
+ cy="12"
33
+ r="10" />
34
+ <line
35
+ x1="12"
36
+ y1="8"
37
+ x2="12"
38
+ y2="12" />
39
+ <line
40
+ x1="12"
41
+ y1="16"
42
+ x2="12.01"
43
+ y2="16" />
44
+ </svg>
45
+ </div>
46
+ <div class="modal-title">确认关闭</div>
47
+ <div class="modal-content">确定要关闭对话窗口吗?</div>
48
+ <div class="modal-actions">
49
+ <button
50
+ class="btn-cancel"
51
+ @click="showConfirmModal = false">
52
+ 取消
53
+ </button>
54
+ <button
55
+ class="btn-confirm"
56
+ @click="confirmClose">
57
+ 确定
58
+ </button>
59
+ </div>
60
+ </div>
61
+ </div>
62
+ </div>
63
+ </template>
64
+
65
+ <script setup>
66
+ import { ref } from 'vue';
67
+ import ChatHeader from './ChatHeader.vue';
68
+ import ChatMessageList from './ChatMessageList.vue';
69
+ import ChatInput from './ChatInput.vue';
70
+
71
+ const emit = defineEmits(['minimize', 'close', 'send-message', 'transfer']);
72
+
73
+ const showConfirmModal = ref(false);
74
+
75
+ const handleClose = () => {
76
+ showConfirmModal.value = true;
77
+ };
78
+
79
+ const confirmClose = () => {
80
+ showConfirmModal.value = false;
81
+ emit('close');
82
+ };
83
+
84
+ defineProps({
85
+ messages: {
86
+ type: Array,
87
+ default: () => []
88
+ },
89
+ isStreaming: {
90
+ type: Boolean,
91
+ default: false
92
+ },
93
+ userInfo: {
94
+ type: Object,
95
+ default: () => ({ id: '', name: '', avatar: '' })
96
+ },
97
+ enableTransfer: {
98
+ type: Boolean,
99
+ default: true
100
+ }
101
+ });
102
+ </script>
103
+
104
+ <style scoped>
105
+ .chat-window {
106
+ width: 400px;
107
+ height: 560px;
108
+ background: #ffffff;
109
+ border-radius: 16px;
110
+ box-shadow: 0 10px 15px rgba(0, 0, 0, 0.08);
111
+ display: flex;
112
+ flex-direction: column;
113
+ overflow: hidden;
114
+ animation: slideUp 0.3s ease;
115
+ position: relative;
116
+ }
117
+
118
+ @keyframes slideUp {
119
+ from {
120
+ opacity: 0;
121
+ transform: translateY(20px);
122
+ }
123
+ to {
124
+ opacity: 1;
125
+ transform: translateY(0);
126
+ }
127
+ }
128
+
129
+ .modal-overlay {
130
+ position: absolute;
131
+ top: 0;
132
+ left: 0;
133
+ right: 0;
134
+ bottom: 0;
135
+ background: rgba(0, 0, 0, 0.5);
136
+ display: flex;
137
+ align-items: center;
138
+ justify-content: center;
139
+ z-index: 100;
140
+ animation: fadeIn 0.2s ease;
141
+ }
142
+
143
+ @keyframes fadeIn {
144
+ from {
145
+ opacity: 0;
146
+ }
147
+ to {
148
+ opacity: 1;
149
+ }
150
+ }
151
+
152
+ .confirm-modal {
153
+ background: white;
154
+ border-radius: 12px;
155
+ padding: 24px;
156
+ width: 280px;
157
+ text-align: center;
158
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
159
+ animation: modalSlideUp 0.2s ease;
160
+ }
161
+
162
+ @keyframes modalSlideUp {
163
+ from {
164
+ opacity: 0;
165
+ transform: translateY(10px);
166
+ }
167
+ to {
168
+ opacity: 1;
169
+ transform: translateY(0);
170
+ }
171
+ }
172
+
173
+ .modal-icon {
174
+ width: 48px;
175
+ height: 48px;
176
+ margin: 0 auto 16px;
177
+ background: #fef3c7;
178
+ border-radius: 50%;
179
+ display: flex;
180
+ align-items: center;
181
+ justify-content: center;
182
+ }
183
+
184
+ .modal-icon svg {
185
+ width: 24px;
186
+ height: 24px;
187
+ }
188
+
189
+ .modal-title {
190
+ font-size: 18px;
191
+ font-weight: 600;
192
+ color: #1f2937;
193
+ margin-bottom: 8px;
194
+ }
195
+
196
+ .modal-content {
197
+ font-size: 14px;
198
+ color: #6b7280;
199
+ margin-bottom: 20px;
200
+ }
201
+
202
+ .modal-actions {
203
+ display: flex;
204
+ gap: 12px;
205
+ justify-content: flex-end;
206
+ }
207
+
208
+ .btn-cancel,
209
+ .btn-confirm {
210
+ padding: 8px 20px;
211
+ border-radius: 6px;
212
+ font-size: 14px;
213
+ font-weight: 500;
214
+ cursor: pointer;
215
+ transition: all 0.2s;
216
+ }
217
+
218
+ .btn-cancel {
219
+ border: 1px solid #d1d5db;
220
+ background: white;
221
+ color: #374151;
222
+ }
223
+
224
+ .btn-cancel:hover {
225
+ background: #f9fafb;
226
+ }
227
+
228
+ .btn-confirm {
229
+ border: none;
230
+ background: #8b5cf6;
231
+ color: white;
232
+ }
233
+
234
+ .btn-confirm:hover {
235
+ background: #7c3aed;
236
+ }
237
+ </style>
@@ -0,0 +1,82 @@
1
+ <template>
2
+ <div class="faq-section">
3
+ <div class="faq-header">
4
+ <span class="title">常见问题</span>
5
+ <span class="refresh-btn" @click="$emit('refresh')">
6
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 14px;">
7
+ <polyline points="23 4 23 10 17 10"/>
8
+ <polyline points="1 20 1 14 7 14"/>
9
+ <path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
10
+ </svg>
11
+ 换一批
12
+ </span>
13
+ </div>
14
+ <div class="faq-list">
15
+ <div v-for="(faq, index) in faqList" :key="index" class="faq-item" @click="$emit('select', faq)">
16
+ {{ faq.question }}
17
+ </div>
18
+ </div>
19
+ </div>
20
+ </template>
21
+
22
+ <script setup>
23
+ defineProps({
24
+ faqList: {
25
+ type: Array,
26
+ default: () => []
27
+ }
28
+ })
29
+
30
+ defineEmits(['select', 'refresh'])
31
+ </script>
32
+
33
+ <style scoped>
34
+ .faq-section {
35
+ padding: 12px 16px;
36
+ background: #FFFFFF;
37
+ border-top: 1px solid #E5E7EB;
38
+ }
39
+
40
+ .faq-header {
41
+ display: flex;
42
+ align-items: center;
43
+ justify-content: space-between;
44
+ margin-bottom: 8px;
45
+ }
46
+
47
+ .title {
48
+ font-size: 13px;
49
+ font-weight: 600;
50
+ color: #1F2937;
51
+ }
52
+
53
+ .refresh-btn {
54
+ font-size: 12px;
55
+ color: #8B5CF6;
56
+ cursor: pointer;
57
+ display: flex;
58
+ align-items: center;
59
+ gap: 4px;
60
+ }
61
+
62
+ .faq-list {
63
+ display: flex;
64
+ flex-direction: column;
65
+ gap: 6px;
66
+ }
67
+
68
+ .faq-item {
69
+ padding: 8px 12px;
70
+ background: #F9FAFB;
71
+ border-radius: 8px;
72
+ font-size: 13px;
73
+ color: #1F2937;
74
+ cursor: pointer;
75
+ transition: all 0.2s;
76
+ }
77
+
78
+ .faq-item:hover {
79
+ background: rgba(139, 92, 246, 0.05);
80
+ color: #8B5CF6;
81
+ }
82
+ </style>
@@ -0,0 +1,220 @@
1
+ <template>
2
+ <div class="smart-chat-widget">
3
+ <ChatToggleButton
4
+ v-if="!isOpen"
5
+ @click="toggleWindow" />
6
+
7
+ <ChatWindow
8
+ v-if="isOpen"
9
+ :messages="messages"
10
+ :is-streaming="isStreaming"
11
+ :user-info="userInfo"
12
+ :enable-transfer="enableTransfer"
13
+ @minimize="toggleWindow"
14
+ @close="toggleWindow"
15
+ @send-message="handleSendMessage"
16
+ @transfer="handleTransfer" />
17
+ </div>
18
+ </template>
19
+
20
+ <script setup>
21
+ import { ref, onMounted } from 'vue';
22
+ import ChatToggleButton from './ChatToggleButton.vue';
23
+ import ChatWindow from './ChatWindow.vue';
24
+ import { sendMessageWithStream } from '@/api/chat';
25
+
26
+ const props = defineProps({
27
+ // 智能体ID(可省略,使用默认值)
28
+ agentId: {
29
+ type: String,
30
+ default: 'a29b7cb5-96f3-4126-bbcb-a584659b3f76'
31
+ },
32
+ // 用户信息(可省略)
33
+ userInfo: {
34
+ type: Object,
35
+ default: () => ({ id: '', name: '', avatar: '' })
36
+ },
37
+ // 初始问候语
38
+ greeting: {
39
+ type: String,
40
+ default: '您好,请问有什么可以帮助您的?'
41
+ },
42
+ // 是否显示转人工按钮
43
+ enableTransfer: {
44
+ type: Boolean,
45
+ default: true
46
+ },
47
+ // API基础路径(可省略,自动检测)
48
+ apiBaseUrl: {
49
+ type: String,
50
+ default: ''
51
+ },
52
+ // 客户端ID(可省略,自动生成)
53
+ clientId: {
54
+ type: String,
55
+ default: ''
56
+ },
57
+ // 窗口标题
58
+ title: {
59
+ type: String,
60
+ default: '智能助手'
61
+ },
62
+ // 输入框占位符
63
+ placeholder: {
64
+ type: String,
65
+ default: '请问有什么可以帮助您的?'
66
+ }
67
+ });
68
+
69
+ const emit = defineEmits([
70
+ 'message-sent',
71
+ 'message-received',
72
+ 'transfer-customer',
73
+ 'window-opened',
74
+ 'window-closed'
75
+ ]);
76
+
77
+ const isOpen = ref(false);
78
+ const messages = ref([]);
79
+ const isStreaming = ref(false);
80
+ const conversationId = ref('');
81
+
82
+ // 自动检测 API 基础路径
83
+ const getApiBaseUrl = () => {
84
+ if (props.apiBaseUrl) return props.apiBaseUrl;
85
+ // 尝试从环境变量获取
86
+ if (import.meta.env && import.meta.env.VITE_CHAT_API_URL) {
87
+ return import.meta.env.VITE_CHAT_API_URL;
88
+ }
89
+ // 默认使用独立路径,避免与其他项目冲突
90
+ return '/smart-chat-api';
91
+ };
92
+
93
+ onMounted(() => {
94
+ messages.value.push({
95
+ id: Date.now().toString(),
96
+ type: 'bot',
97
+ content: props.greeting,
98
+ timestamp: new Date().toLocaleString(),
99
+ isFinished: true
100
+ });
101
+ });
102
+
103
+ const toggleWindow = () => {
104
+ isOpen.value = !isOpen.value;
105
+ if (isOpen.value) {
106
+ emit('window-opened');
107
+ } else {
108
+ emit('window-closed');
109
+ }
110
+ };
111
+
112
+ const handleSendMessage = async (content) => {
113
+ console.log('[SmartChatWidget] 收到消息:', content);
114
+
115
+ if (!content.trim()) return;
116
+
117
+ const userMessage = {
118
+ id: Date.now().toString(),
119
+ type: 'user',
120
+ content: content.trim(),
121
+ timestamp: new Date().toLocaleString(),
122
+ isFinished: true
123
+ };
124
+ messages.value.push(userMessage);
125
+ console.log('[SmartChatWidget] 添加用户消息:', messages.value);
126
+ emit('message-sent', { message: userMessage, timestamp: Date.now() });
127
+
128
+ isStreaming.value = true;
129
+ console.log('[SmartChatWidget] 开始流式请求');
130
+
131
+ try {
132
+ const botMessageIndex = messages.value.length;
133
+ const botMessage = ref({
134
+ id: Date.now().toString() + '-bot',
135
+ type: 'bot',
136
+ content: '',
137
+ timestamp: new Date().toLocaleString(),
138
+ isFinished: false
139
+ });
140
+ messages.value.push(botMessage.value);
141
+ console.log('[SmartChatWidget] 添加机器人消息模板:', botMessage.value);
142
+
143
+ await sendMessageWithStream(
144
+ {
145
+ agentId: props.agentId,
146
+ prompt: content.trim(),
147
+ clientId: props.clientId || generateClientId()
148
+ },
149
+ getApiBaseUrl(),
150
+ (streamData) => {
151
+ console.log('[SmartChatWidget] 收到流式数据:', streamData);
152
+
153
+ if (streamData.conversationId) {
154
+ conversationId.value = streamData.conversationId;
155
+ console.log('[SmartChatWidget] 更新conversationId:', conversationId.value);
156
+ }
157
+ if (streamData.content) {
158
+ botMessage.value.content += streamData.content;
159
+ messages.value[botMessageIndex] = { ...botMessage.value };
160
+ console.log('[SmartChatWidget] 更新消息内容:', botMessage.value.content);
161
+ }
162
+ if (streamData.finish) {
163
+ botMessage.value.isFinished = true;
164
+ messages.value[botMessageIndex] = { ...botMessage.value };
165
+ isStreaming.value = false;
166
+ console.log('[SmartChatWidget] 消息完成:', botMessage.value);
167
+ emit('message-received', { message: botMessage.value, timestamp: Date.now() });
168
+ }
169
+ },
170
+ (error) => {
171
+ console.error('[SmartChatWidget] 流式消息错误:', error);
172
+ isStreaming.value = false;
173
+ botMessage.value.isFinished = true;
174
+ messages.value[botMessageIndex] = { ...botMessage.value };
175
+ }
176
+ );
177
+
178
+ if (!botMessage.value.isFinished) {
179
+ botMessage.value.isFinished = true;
180
+ messages.value[botMessageIndex] = { ...botMessage.value };
181
+ isStreaming.value = false;
182
+ console.log('[SmartChatWidget] 消息完成(流式结束):', botMessage.value);
183
+ emit('message-received', { message: botMessage.value, timestamp: Date.now() });
184
+ }
185
+ } catch (error) {
186
+ console.error('[SmartChatWidget] 发送消息失败:', error);
187
+ isStreaming.value = false;
188
+ messages.value.push({
189
+ id: Date.now().toString() + '-error',
190
+ type: 'bot',
191
+ content: '抱歉,服务暂时不可用,请稍后重试',
192
+ timestamp: new Date().toLocaleString(),
193
+ isFinished: true
194
+ });
195
+ }
196
+ };
197
+
198
+ const generateClientId = () => {
199
+ return 'client-' + Date.now().toString(36) + Math.random().toString(36).substr(2, 9);
200
+ };
201
+
202
+ const handleTransfer = () => {
203
+ emit('transfer-customer', {
204
+ userId: props.userInfo.id,
205
+ conversationId: conversationId.value,
206
+ context: messages.value
207
+ .map((m) => `${m.type === 'user' ? '用户' : '助手'}: ${m.content}`)
208
+ .join('\n')
209
+ });
210
+ };
211
+ </script>
212
+
213
+ <style scoped>
214
+ .smart-chat-widget {
215
+ position: fixed;
216
+ right: 24px;
217
+ bottom: 24px;
218
+ z-index: 9999;
219
+ }
220
+ </style>
package/src/index.js ADDED
@@ -0,0 +1,12 @@
1
+ import SmartChatWidget from './components/SmartChatWidget.vue'
2
+ import './styles/index.scss'
3
+
4
+ const SmartChatWidgetPlugin = {
5
+ install(app) {
6
+ app.component('SmartChatWidget', SmartChatWidget)
7
+ }
8
+ }
9
+
10
+ export { SmartChatWidget, SmartChatWidgetPlugin }
11
+
12
+ export default SmartChatWidgetPlugin
package/src/main.js ADDED
@@ -0,0 +1,5 @@
1
+ import { createApp } from 'vue'
2
+ import App from './App.vue'
3
+
4
+ const app = createApp(App)
5
+ app.mount('#app')
@@ -0,0 +1,8 @@
1
+ /* 智能聊天组件全局样式 */
2
+ .smart-chat-widget * {
3
+ box-sizing: border-box;
4
+ }
5
+
6
+ .smart-chat-widget {
7
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
8
+ }