@hlw-zbzs/ai 1.0.1 → 1.0.3

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.
@@ -1,474 +0,0 @@
1
- <template>
2
- <div class="chat-wrapper">
3
- <!-- 顶部预设话术栏 -->
4
- <div class="prompt-bar">
5
- <span class="prompt-label">常用话术:</span>
6
- <span class="prompt-chip" @click="setPrompt('帮我写一段幽默风趣的直播间求点赞话术')">
7
- 求点赞
8
- </span>
9
- <span class="prompt-chip" @click="setPrompt('观众送出了嘉年华,写一段热情霸气的感谢词')">
10
- 感谢大哥
11
- </span>
12
- <span class="prompt-chip" @click="setPrompt('有观众在弹幕质疑商品价格偏高,给出高情商回复话术')">
13
- 高情商解围
14
- </span>
15
- <span class="prompt-chip" @click="setPrompt('直播间准备开始抽奖,帮我写一段炒热气氛的话术')">
16
- 抽奖倒计时
17
- </span>
18
- </div>
19
-
20
- <!-- 对话气泡滚动区域 -->
21
- <div class="chat-area">
22
- <div v-if="dialogue.length === 0" class="chat-empty">
23
- <n-icon :size="40" class="empty-icon"><HardwareChipOutline /></n-icon>
24
- <span class="empty-title">AI 对话演练沙箱</span>
25
- <span class="empty-desc">选择服务商后,在下方输入话术需求或点击上方预设,实时体验大模型流式输出与思考推理</span>
26
- </div>
27
-
28
- <div v-else class="chat-list">
29
- <div
30
- v-for="(msg, index) in dialogue"
31
- :key="index"
32
- class="chat-row"
33
- :class="msg.role"
34
- >
35
- <div class="role-tag" :class="msg.role">
36
- {{ msg.role === 'user' ? '主播' : 'AI' }}
37
- </div>
38
-
39
- <div class="msg-bubble">
40
- <!-- 深度思考过程折叠块 (DeepSeek R1 等推理模型) -->
41
- <div v-if="msg.think" class="think-box">
42
- <div class="think-header">
43
- <n-icon :size="12"><CodeSlashOutline /></n-icon>
44
- <span>思考过程</span>
45
- </div>
46
- <div class="think-content">{{ msg.think }}</div>
47
- </div>
48
-
49
- <!-- 消息正文与复制操作 -->
50
- <div class="msg-text">
51
- <div class="text-content">
52
- <span v-if="msg.text">{{ msg.text }}</span>
53
- <span v-else-if="loading && index === dialogue.length - 1" class="loading-hint">
54
- 正在思考与生成回复...
55
- </span>
56
- </div>
57
-
58
- <div v-if="msg.text" class="bubble-actions">
59
- <button
60
- class="copy-btn"
61
- title="复制话术内容"
62
- @click.stop="onCopy(msg.text)"
63
- >
64
- <n-icon :size="12"><CopyOutline /></n-icon>
65
- <span>复制话术</span>
66
- </button>
67
- </div>
68
- </div>
69
- </div>
70
- </div>
71
- </div>
72
- </div>
73
-
74
- <!-- 底部输入操作栏 -->
75
- <div class="chat-input-bar">
76
- <n-input
77
- v-model:value="input"
78
- type="textarea"
79
- placeholder="输入提问内容 (快捷键 Enter 发送,Shift + Enter 换行)..."
80
- :autosize="{ minRows: 2.5, maxRows: 5 }"
81
- :disabled="loading"
82
- size="medium"
83
- @keydown="onKeyDown"
84
- />
85
-
86
- <div class="input-bottom-row">
87
- <span class="shortcut-tip">Enter 发送 / Shift + Enter 换行</span>
88
-
89
- <div class="action-buttons">
90
- <hlw-button
91
- size="medium"
92
- secondary
93
- :icon="TrashOutline"
94
- :disabled="dialogue.length === 0 || loading"
95
- @click="onClear"
96
- >
97
- 清空记录
98
- </hlw-button>
99
-
100
- <hlw-button
101
- v-if="loading"
102
- size="medium"
103
- type="warning"
104
- secondary
105
- :icon="StopCircleOutline"
106
- @click="onAbort"
107
- >
108
- 停止生成
109
- </hlw-button>
110
-
111
- <hlw-button
112
- v-else
113
- size="medium"
114
- type="primary"
115
- :icon="PaperPlaneOutline"
116
- :disabled="!input.trim()"
117
- @click="onSend"
118
- >
119
- 发送测试
120
- </hlw-button>
121
- </div>
122
- </div>
123
- </div>
124
- </div>
125
- </template>
126
-
127
- <script setup lang="ts">
128
- import { ref } from 'vue'
129
- import {
130
- TrashOutline,
131
- PaperPlaneOutline,
132
- StopCircleOutline,
133
- HardwareChipOutline,
134
- CodeSlashOutline,
135
- CopyOutline
136
- } from '@vicons/ionicons5'
137
- import { NInput, NIcon } from 'naive-ui'
138
- import { toast, HlwButton } from '@hlw-zbzs/vue'
139
- import { useAi } from '../core'
140
- import type { AiMsg } from '../types'
141
-
142
- const { loading, stream, abort } = useAi()
143
-
144
- const input = ref('')
145
- const dialogue = ref<AiMsg[]>([])
146
-
147
- function setPrompt(prompt: string) {
148
- input.value = prompt
149
- }
150
-
151
- async function onCopy(content: string) {
152
- if (!content) return
153
- try {
154
- if (navigator?.clipboard?.writeText) {
155
- await navigator.clipboard.writeText(content)
156
- } else {
157
- const textarea = document.createElement('textarea')
158
- textarea.value = content
159
- textarea.style.position = 'fixed'
160
- textarea.style.opacity = '0'
161
- document.body.appendChild(textarea)
162
- textarea.select()
163
- document.execCommand('copy')
164
- document.body.removeChild(textarea)
165
- }
166
- toast('已复制话术到剪贴板', 'success')
167
- } catch (err: any) {
168
- toast('复制失败,请手动复制', 'error')
169
- }
170
- }
171
-
172
- function onKeyDown(event: KeyboardEvent) {
173
- if (event.key === 'Enter' && !event.shiftKey) {
174
- event.preventDefault()
175
- if (input.value.trim() && !loading.value) {
176
- onSend()
177
- }
178
- }
179
- }
180
-
181
- async function onSend() {
182
- const userText = input.value.trim()
183
- if (!userText || loading.value) return
184
-
185
- input.value = ''
186
- dialogue.value.push({ role: 'user', text: userText })
187
-
188
- const aiMsg: AiMsg = { role: 'assistant', text: '', think: '' }
189
- dialogue.value.push(aiMsg)
190
-
191
- try {
192
- await stream(
193
- dialogue.value.slice(0, -1),
194
- {
195
- onToken: (delta) => {
196
- aiMsg.text += delta
197
- },
198
- onThink: (delta) => {
199
- if (!aiMsg.think) aiMsg.think = ''
200
- aiMsg.think += delta
201
- },
202
- onError: (err) => {
203
- toast(`生成错误: ${err.message}`, 'error')
204
- }
205
- }
206
- )
207
- } catch (err: any) {
208
- aiMsg.text = `[请求失败]: ${err.message || '网络异常'}`
209
- }
210
- }
211
-
212
- function onAbort() {
213
- abort()
214
- toast('已中止当前生成', 'info')
215
- }
216
-
217
- function onClear() {
218
- dialogue.value = []
219
- }
220
- </script>
221
-
222
- <style scoped lang="scss">
223
- .chat-wrapper {
224
- width: 100%;
225
- height: 100%;
226
- display: flex;
227
- flex-direction: column;
228
- background: #ffffff;
229
-
230
- .prompt-bar {
231
- height: 40px;
232
- padding: 0 16px;
233
- border-bottom: 1px solid #f1f5f9;
234
- display: flex;
235
- align-items: center;
236
- gap: 6px;
237
- overflow-x: auto;
238
- flex-shrink: 0;
239
-
240
- .prompt-label {
241
- font-size: 13px;
242
- font-weight: 400;
243
- color: #64748b;
244
- white-space: nowrap;
245
- }
246
-
247
- .prompt-chip {
248
- font-size: 12px;
249
- font-weight: 400;
250
- color: #2563eb;
251
- background: #eff6ff;
252
- border: 1px solid #dbeafe;
253
- padding: 3px 10px;
254
- border-radius: 4px;
255
- cursor: pointer;
256
- white-space: nowrap;
257
- user-select: none;
258
- transition: all 0.15s ease;
259
-
260
- &:hover {
261
- background: #dbeafe;
262
- }
263
- }
264
- }
265
-
266
- .chat-area {
267
- flex: 1;
268
- min-height: 0;
269
- overflow-y: auto;
270
- padding: 16px;
271
-
272
- .chat-empty {
273
- height: 100%;
274
- display: flex;
275
- flex-direction: column;
276
- align-items: center;
277
- justify-content: center;
278
- gap: 8px;
279
- color: #94a3b8;
280
- text-align: center;
281
-
282
- .empty-icon {
283
- color: #cbd5e1;
284
- }
285
-
286
- .empty-title {
287
- font-size: 15px;
288
- font-weight: 400;
289
- color: #475569;
290
- }
291
-
292
- .empty-desc {
293
- font-size: 13px;
294
- font-weight: 400;
295
- color: #94a3b8;
296
- max-width: 340px;
297
- line-height: 1.5;
298
- }
299
- }
300
-
301
- .chat-list {
302
- display: flex;
303
- flex-direction: column;
304
- gap: 14px;
305
-
306
- .chat-row {
307
- display: flex;
308
- gap: 10px;
309
- align-items: flex-start;
310
-
311
- .role-tag {
312
- width: 32px;
313
- height: 32px;
314
- border-radius: 6px;
315
- display: flex;
316
- align-items: center;
317
- justify-content: center;
318
- font-size: 12px;
319
- font-weight: 400;
320
- flex-shrink: 0;
321
-
322
- &.user {
323
- background: #2563eb;
324
- color: #ffffff;
325
- }
326
-
327
- &.assistant {
328
- background: #f1f5f9;
329
- color: #334155;
330
- border: 1px solid #e2e8f0;
331
- }
332
- }
333
-
334
- .msg-bubble {
335
- max-width: 85%;
336
- display: flex;
337
- flex-direction: column;
338
- gap: 6px;
339
-
340
- .think-box {
341
- background: #f8fafc;
342
- border-left: 3px solid #64748b;
343
- padding: 8px 12px;
344
- font-size: 12px;
345
- font-weight: 400;
346
- color: #64748b;
347
- border-radius: 0 4px 4px 0;
348
- font-family: monospace;
349
-
350
- .think-header {
351
- display: flex;
352
- align-items: center;
353
- gap: 4px;
354
- font-weight: 400;
355
- color: #475569;
356
- margin-bottom: 3px;
357
- font-size: 12px;
358
- }
359
-
360
- .think-content {
361
- white-space: pre-wrap;
362
- line-height: 1.5;
363
- }
364
- }
365
-
366
- .msg-text {
367
- background: #f8fafc;
368
- border: 1px solid #f1f5f9;
369
- color: #0f172a;
370
- padding: 10px 14px;
371
- border-radius: 8px;
372
- font-size: 13px;
373
- font-weight: 400;
374
- line-height: 1.6;
375
- white-space: pre-wrap;
376
- word-break: break-word;
377
-
378
- .bubble-actions {
379
- display: flex;
380
- justify-content: flex-end;
381
- margin-top: 6px;
382
- border-top: 1px dashed #e2e8f0;
383
- padding-top: 5px;
384
-
385
- .copy-btn {
386
- display: inline-flex;
387
- align-items: center;
388
- gap: 3px;
389
- background: #ffffff;
390
- border: 1px solid #cbd5e1;
391
- border-radius: 4px;
392
- padding: 2px 8px;
393
- font-size: 12px;
394
- font-weight: 400;
395
- color: #475569;
396
- cursor: pointer;
397
- user-select: none;
398
- transition: all 0.15s ease;
399
-
400
- &:hover {
401
- color: #2563eb;
402
- border-color: #93c5fd;
403
- background: #eff6ff;
404
- }
405
- }
406
- }
407
-
408
- .loading-hint {
409
- color: #94a3b8;
410
- font-style: italic;
411
- }
412
- }
413
- }
414
-
415
- &.user {
416
- flex-direction: row-reverse;
417
-
418
- .msg-bubble {
419
- align-items: flex-end;
420
-
421
- .msg-text {
422
- background: #2563eb;
423
- border: none;
424
- color: #ffffff;
425
-
426
- .bubble-actions {
427
- border-top: 1px dashed rgba(255, 255, 255, 0.3);
428
-
429
- .copy-btn {
430
- background: rgba(255, 255, 255, 0.15);
431
- border-color: rgba(255, 255, 255, 0.4);
432
- color: #ffffff;
433
-
434
- &:hover {
435
- background: rgba(255, 255, 255, 0.25);
436
- }
437
- }
438
- }
439
- }
440
- }
441
- }
442
- }
443
- }
444
- }
445
-
446
- .chat-input-bar {
447
- padding: 12px 16px;
448
- background: #ffffff;
449
- border-top: 1px solid #e2e8f0;
450
- display: flex;
451
- flex-direction: column;
452
- gap: 8px;
453
- flex-shrink: 0;
454
-
455
- .input-bottom-row {
456
- display: flex;
457
- align-items: center;
458
- justify-content: space-between;
459
-
460
- .shortcut-tip {
461
- font-size: 12px;
462
- font-weight: 400;
463
- color: #94a3b8;
464
- }
465
-
466
- .action-buttons {
467
- display: flex;
468
- align-items: center;
469
- gap: 8px;
470
- }
471
- }
472
- }
473
- }
474
- </style>
@@ -1,175 +0,0 @@
1
- <template>
2
- <div class="engine-section">
3
- <div class="section-header">
4
- <span class="section-title">选择大模型服务商</span>
5
- <span class="section-hint">点击一键切换引擎</span>
6
- </div>
7
-
8
- <div class="engine-grid">
9
- <div
10
- v-for="item in list"
11
- :key="item.type"
12
- class="engine-card"
13
- :class="{ active: engine === item.type }"
14
- @click="onSelect(item)"
15
- >
16
- <div class="card-head">
17
- <span class="card-name">{{ item.name }}</span>
18
- <span v-if="item.free" class="free-badge">免密</span>
19
- <span v-else-if="item.url" class="key-link" title="点击打开官网获取 Key" @click.stop="openPortal(item)">
20
- <span>获取Key</span>
21
- <n-icon :size="11"><OpenOutline /></n-icon>
22
- </span>
23
- </div>
24
- <span class="card-desc">{{ item.tag }}</span>
25
- </div>
26
- </div>
27
- </div>
28
- </template>
29
-
30
- <script setup lang="ts">
31
- import { OpenOutline } from '@vicons/ionicons5'
32
- import { NIcon } from 'naive-ui'
33
- import { tools } from '@hlw-zbzs/core'
34
- import { toast } from '@hlw-zbzs/vue'
35
- import { useAi } from '../core'
36
- import type { AiEngine, AiInfo } from '../types'
37
-
38
- const emit = defineEmits<{
39
- (event: 'change', type: AiEngine): void
40
- }>()
41
-
42
- const { engine, list, select } = useAi()
43
-
44
- function openPortal(item: AiInfo) {
45
- if (!item?.url) return
46
- if (tools?.openExternal) {
47
- tools.openExternal(item.url)
48
- } else {
49
- window.open(item.url, '_blank')
50
- }
51
- toast(`已为你打开【${item.name}】开发者控制台`, 'info')
52
- }
53
-
54
- function onSelect(item: AiInfo) {
55
- select(item.type)
56
- emit('change', item.type)
57
- }
58
- </script>
59
-
60
- <style scoped lang="scss">
61
- .engine-section {
62
- display: flex;
63
- flex-direction: column;
64
- gap: 10px;
65
- padding: 16px 16px 14px 16px;
66
- border-bottom: 1px solid #f1f5f9;
67
-
68
- .section-header {
69
- display: flex;
70
- align-items: center;
71
- justify-content: space-between;
72
- border-bottom: 1px solid #f1f5f9;
73
- padding-bottom: 6px;
74
-
75
- .section-title {
76
- font-size: 14px;
77
- font-weight: 400;
78
- color: #1e293b;
79
- }
80
-
81
- .section-hint {
82
- font-size: 12px;
83
- font-weight: 400;
84
- color: #94a3b8;
85
- }
86
- }
87
-
88
- .engine-grid {
89
- display: grid;
90
- grid-template-columns: repeat(3, minmax(0, 1fr));
91
- gap: 10px;
92
-
93
- .engine-card {
94
- background: #f8fafc;
95
- border: 1px solid #e2e8f0;
96
- border-radius: 6px;
97
- padding: 9px 12px;
98
- cursor: pointer;
99
- user-select: none;
100
- transition: all 0.15s ease;
101
- display: flex;
102
- flex-direction: column;
103
- gap: 4px;
104
- min-width: 0;
105
-
106
- .card-head {
107
- display: flex;
108
- align-items: center;
109
- justify-content: space-between;
110
-
111
- .card-name {
112
- font-size: 13px;
113
- font-weight: 400;
114
- color: #334155;
115
- }
116
-
117
- .free-badge {
118
- font-size: 11px;
119
- font-weight: 400;
120
- color: #059669;
121
- background: #ecfdf5;
122
- padding: 0 4px;
123
- border-radius: 3px;
124
- }
125
-
126
- .key-link {
127
- display: inline-flex;
128
- align-items: center;
129
- gap: 2px;
130
- font-size: 11px;
131
- font-weight: 400;
132
- color: #2563eb;
133
- background: #eff6ff;
134
- padding: 1px 5px;
135
- border-radius: 3px;
136
- cursor: pointer;
137
- transition: all 0.15s ease;
138
-
139
- &:hover {
140
- background: #dbeafe;
141
- color: #1d4ed8;
142
- }
143
- }
144
- }
145
-
146
- .card-desc {
147
- font-size: 11px;
148
- font-weight: 400;
149
- color: #94a3b8;
150
- white-space: nowrap;
151
- overflow: hidden;
152
- text-overflow: ellipsis;
153
- }
154
-
155
- &:hover {
156
- border-color: #93c5fd;
157
- background: #f1f5f9;
158
- }
159
-
160
- &.active {
161
- border-color: #3b82f6;
162
- background: #eff6ff;
163
-
164
- .card-head .card-name {
165
- color: #2563eb;
166
- }
167
-
168
- .card-desc {
169
- color: #3b82f6;
170
- }
171
- }
172
- }
173
- }
174
- }
175
- </style>