Skip to content
体验新版
项目
组织
正在加载...
登录
切换导航
打开侧边栏
DCloud
uni-ai-chat
提交
26aa2ef8
U
uni-ai-chat
项目概览
DCloud
/
uni-ai-chat
通知
893
Star
11
Fork
8
代码
文件
提交
分支
Tags
贡献者
分支图
Diff
Issue
2
列表
看板
标记
里程碑
合并请求
0
DevOps
流水线
流水线任务
计划
Wiki
0
Wiki
分析
仓库
DevOps
项目成员
Pages
U
uni-ai-chat
项目概览
项目概览
详情
发布
仓库
仓库
文件
提交
分支
标签
贡献者
分支图
比较
Issue
2
Issue
2
列表
看板
标记
里程碑
合并请求
0
合并请求
0
Pages
DevOps
DevOps
流水线
流水线任务
计划
分析
分析
仓库分析
DevOps
Wiki
0
Wiki
成员
成员
收起侧边栏
关闭侧边栏
动态
分支图
创建新Issue
流水线任务
提交
Issue看板
提交
26aa2ef8
编写于
6月 05, 2023
作者:
DCloud_JSON
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
1.0.19
上级
a2fb542e
变更
4
隐藏空白更改
内联
并排
Showing
4 changed file
with
151 addition
and
65 deletion
+151
-65
changelog.md
changelog.md
+6
-0
components/uni-ai-msg/uni-ai-msg.vue
components/uni-ai-msg/uni-ai-msg.vue
+81
-42
package.json
package.json
+1
-1
pages/chat/chat.vue
pages/chat/chat.vue
+63
-22
未找到文件。
changelog.md
浏览文件 @
26aa2ef8
## 1.0.19(2023-06-05)
-
新增 支持停止响应(终止接收ai返回的答案)
-
新增 支持一键换答案(让ai重新回答一遍,得到新的答案)
-
新增 支持一键复制答案全文
-
优化 为提升用户体验;在收到ai的回答之前,不可编辑并发送新问题 改成 可以先编辑但不能发送
-
修复 由
`1.0.12`
重构部分逻辑引起的,发送失败后点击重复报
`this.retriesSendMsg is not a function"`
的问题
## 1.0.18(2023-06-02)
## 1.0.18(2023-06-02)
-
新增 支持一键复制代码块
-
新增 支持一键复制代码块
-
修复 部分情况下偶发,ai回复的内容会串到用户的提问内容中的问题
-
修复 部分情况下偶发,ai回复的内容会串到用户的提问内容中的问题
...
...
components/uni-ai-msg/uni-ai-msg.vue
浏览文件 @
26aa2ef8
...
@@ -10,19 +10,20 @@
...
@@ -10,19 +10,20 @@
<view
class=
"content"
>
<view
class=
"content"
>
<view
class=
"rich-text-box"
:class=
"
{'show-cursor':showCursor}" ref="rich-text-box">
<view
class=
"rich-text-box"
:class=
"
{'show-cursor':showCursor}" ref="rich-text-box">
<rich-text
v-if=
"nodes&&nodes.length"
space=
"nbsp"
:nodes=
"nodes"
@
itemclick=
"trOnclick"
></rich-text>
<rich-text
v-if=
"nodes&&nodes.length"
space=
"nbsp"
:nodes=
"nodes"
@
itemclick=
"trOnclick"
></rich-text>
</view>
<!-- #ifdef H5 -->
<view
v-if=
"isLastMsg && adpid && msg.insufficientScore"
>
<view
class=
"copy-box"
:style=
"
{left,top}">
<text
style=
"color: red;"
>
<text
class=
"copy"
@
click=
"copy"
>
复制
</text>
默认不启用广告组件(被注释),如需使用,请"去掉注释"(“重新运行”后生效)
</view>
位置:/components/uni-ai-msg/uni-ai-msg.vue 第28行,或全局搜索 uni-ad-rewarded-video
<!-- #endif -->
</text>
</view>
<!--
<uni-ad-rewarded-video
:adpid=
"adpid"
@
onAdClose=
"onAdClose"
></uni-ad-rewarded-video>
-->
<view
v-if=
"isLastMsg && adpid && msg.insufficientScore"
>
</view>
<text
style=
"color: red;"
>
<view
v-if=
"msg.isAi"
class=
"controller"
>
默认不启用广告组件(被注释),如需使用,请"去掉注释"(“重新运行”后生效)
<text
v-if=
"isLastMsg"
title=
"换一个答案"
@
click=
"changeAnswer"
class=
"retry-icon"
>
⟳
</text>
位置:/components/uni-ai-msg/uni-ai-msg.vue 第28行,或全局搜索 uni-ad-rewarded-video
<view
@
click=
"copy"
title=
"复制"
class=
"copy-icon"
>
</text>
<view
class=
"copy-icon-a"
></view>
<!--
<uni-ad-rewarded-video
:adpid=
"adpid"
@
onAdClose=
"onAdClose"
></uni-ad-rewarded-video>
-->
<view
class=
"copy-icon-b"
></view>
</view>
</view>
</view>
</view>
</view>
<uni-icons
v-if=
"isLastMsg && !msg.isAi && msg.state != 100 && msgStateIcon(msg)"
<uni-icons
v-if=
"isLastMsg && !msg.isAi && msg.state != 100 && msgStateIcon(msg)"
...
@@ -119,31 +120,6 @@
...
@@ -119,31 +120,6 @@
};
};
},
},
mounted
()
{
mounted
()
{
// #ifdef H5
// web端限制不选中文字时出现系统右键菜单
let
richTextBox
=
this
.
$refs
[
'
rich-text-box
'
]
if
(
richTextBox
)
{
// 监听鼠标右键事件
richTextBox
.
$el
.
addEventListener
(
'
contextmenu
'
,
(
e
)
=>
{
// 判断是否选中了文字内容,如果没有则限制系统默认行为(禁止弹出右键菜单)
if
(
!
document
.
getSelection
().
toString
())
{
// console.log(e);
// 设置悬浮的复制按钮的坐标值
this
.
top
=
e
.
y
+
'
px
'
this
.
left
=
e
.
x
+
'
px
'
// console.log(e.x);
// console.log(e.y);
// 禁止系统默认行为(禁止弹出右键菜单)
e
.
preventDefault
()
}
})
}
// 监听全局点击事件,隐藏悬浮的复制按钮的坐标
document
.
addEventListener
(
'
click
'
,
()
=>
{
this
.
left
=
"
-100px
"
})
// #endif
},
},
created
()
{
created
()
{
this
.
msg
=
msgList
[
this
.
msgIndex
]
this
.
msg
=
msgList
[
this
.
msgIndex
]
...
@@ -201,7 +177,7 @@
...
@@ -201,7 +177,7 @@
switch
(
msg
.
state
)
{
switch
(
msg
.
state
)
{
case
0
:
case
0
:
// 发送中
// 发送中
return
'
spinner-cycle
'
return
''
break
;
break
;
case
-
100
:
case
-
100
:
// 发送失败
// 发送失败
...
@@ -235,13 +211,25 @@
...
@@ -235,13 +211,25 @@
}
}
})
})
}
}
},
changeAnswer
(){
this
.
$emit
(
'
changeAnswer
'
)
},
retriesSendMsg
(){
this
.
$emit
(
'
retriesSendMsg
'
)
},
},
// #ifdef H5
// #ifdef H5
// 复制文本内容到系统剪切板
// 复制文本内容到系统剪切板
copy
()
{
copy
()
{
uni
.
setClipboardData
({
uni
.
setClipboardData
({
data
:
this
.
md
,
data
:
this
.
md
,
showToast
:
false
,
showToast
:
false
,
success
()
{
uni
.
showToast
({
title
:
'
复制成功
'
,
icon
:
'
none
'
});
}
})
})
// 设置悬浮的复制按钮的坐标值,使其隐藏
// 设置悬浮的复制按钮的坐标值,使其隐藏
this
.
left
=
"
-100px
"
this
.
left
=
"
-100px
"
...
@@ -301,9 +289,10 @@
...
@@ -301,9 +289,10 @@
justify-content
:
center
;
justify-content
:
center
;
}
}
.content
{
.content
{
position
:
relative
;
/* #ifndef APP-NVUE */
/* #ifndef APP-NVUE */
max-width
:
85%
;
max-width
:
calc
(
85%
-
15px
)
;
/* #endif */
/* #endif */
background-color
:
#FFF
;
background-color
:
#FFF
;
border-radius
:
5px
;
border-radius
:
5px
;
...
@@ -314,6 +303,56 @@
...
@@ -314,6 +303,56 @@
user-select
:
text
;
user-select
:
text
;
cursor
:
text
;
cursor
:
text
;
/* #endif */
/* #endif */
}
.controller
{
position
:
absolute
;
right
:
-25px
;
bottom
:
0
;
width
:
20px
;
flex-direction
:
column
;
height
:
40px
;
justify-content
:
flex-end
;
}
.retry-icon
{
font-size
:
26px
;
color
:
#d4d4d4
;
height
:
25px
;
line-height
:
15px
;
margin-bottom
:
5px
;
}
.retry-icon
:hover
{
color
:
#BBB
;
}
.retry-icon
,
.copy-icon
{
cursor
:
pointer
;
}
.copy-icon
{
position
:
relative
;
height
:
25px
;
}
.copy-icon-a
,
.copy-icon-b
{
position
:
absolute
;
border
:
1
.5px
solid
#d4d4d4
;
width
:
10px
;
height
:
12px
;
background-color
:
#FFF
;
left
:
2px
;
top
:
2px
;
border-radius
:
3px
;
}
.copy-icon
:hover
.copy-icon-a
,
.copy-icon
:hover
.copy-icon-b
,
{
border-color
:
#bbb
;
}
.copy-icon-b
{
top
:
5px
;
left
:
5px
;
}
}
/* #ifndef APP-NVUE */
/* #ifndef APP-NVUE */
...
...
package.json
浏览文件 @
26aa2ef8
{
{
"id"
:
"uni-ai-chat"
,
"id"
:
"uni-ai-chat"
,
"name"
:
"uni-ai-chat"
,
"name"
:
"uni-ai-chat"
,
"version"
:
"1.0.1
8
"
,
"version"
:
"1.0.1
9
"
,
"description"
:
"基于uni-ai的聊天示例项目,支持流式、支持前文总结,云端一体"
,
"description"
:
"基于uni-ai的聊天示例项目,支持流式、支持前文总结,云端一体"
,
"main"
:
"main.js"
,
"main"
:
"main.js"
,
"scripts"
:
{
"scripts"
:
{
...
...
pages/chat/chat.vue
浏览文件 @
26aa2ef8
...
@@ -4,16 +4,17 @@
...
@@ -4,16 +4,17 @@
<view
v-if=
"isWidescreen"
class=
"header"
>
uni-ai-chat
</view>
<view
v-if=
"isWidescreen"
class=
"header"
>
uni-ai-chat
</view>
<text
class=
"noData"
v-if=
"msgLength === 0"
>
没有对话记录
</text>
<text
class=
"noData"
v-if=
"msgLength === 0"
>
没有对话记录
</text>
<scroll-view
:scroll-into-view=
"scrollIntoView"
scroll-y=
"true"
class=
"msg-list"
:enable-flex=
"true"
>
<scroll-view
:scroll-into-view=
"scrollIntoView"
scroll-y=
"true"
class=
"msg-list"
:enable-flex=
"true"
>
<uni-ai-msg
ref=
"msg"
v-for=
"(msgIndex,index) in msgLength"
:key=
"index"
:msgIndex=
"index"
<uni-ai-msg
ref=
"msg"
v-for=
"(msgIndex,index) in msgLength"
:key=
"index"
:msgIndex=
"index"
@
retriesSendMsg=
"retriesSendMsg"
@
changeAnswer=
"changeAnswer"
:show-cursor=
"index == msgLength - 1 && msgLength%2 === 0 && sseIndex"
:isLastMsg=
"index == msgLength - 1"
></uni-ai-msg>
:show-cursor=
"index == msgLength - 1 && msgLength%2 === 0 && sseIndex"
:isLastMsg=
"index == msgLength - 1"
></uni-ai-msg>
<view
class=
"tip-ai-ing"
v-if=
"msgLength && msgLength%2 !== 0"
>
<view
class=
"tip-ai-ing"
v-if=
"msgLength && msgLength%2 !== 0"
>
<text>
uni-ai正在思考中...
</text>
<text>
uni-ai正在思考中...
</text>
<view
v-if=
"NODE_ENV == 'development' && !enableStream"
>
<view
v-if=
"NODE_ENV == 'development' && !enableStream"
>
如需提速,请开通
<uni-link
class=
"uni-link"
href=
"https://uniapp.dcloud.net.cn/uniCloud/uni-ai-chat.html"
如需提速,请开通
<uni-link
class=
"uni-link"
href=
"https://uniapp.dcloud.net.cn/uniCloud/uni-ai-chat.html"
text=
"[流式响应]"
></uni-link>
text=
"[流式响应]"
></uni-link>
</view>
</view>
</view>
</view>
<view
id=
"last-msg-item"
></view>
<view
@
click=
"closeSseChannel"
class=
"stop-responding"
v-if=
"enableStream && sseIndex"
>
▣ 停止响应
</view>
<view
id=
"last-msg-item"
style=
"height: 1px;"
></view>
</scroll-view>
</scroll-view>
<view
class=
"foot-box"
>
<view
class=
"foot-box"
>
...
@@ -29,10 +30,10 @@
...
@@ -29,10 +30,10 @@
</view>
</view>
<view
class=
"textarea-box"
@
click=
"focus = true"
>
<view
class=
"textarea-box"
@
click=
"focus = true"
>
<textarea
v-model=
"content"
:cursor-spacing=
"15"
class=
"textarea"
:auto-height=
"!isWidescreen"
<textarea
v-model=
"content"
:cursor-spacing=
"15"
class=
"textarea"
:auto-height=
"!isWidescreen"
:
disabled=
"inputBoxDisabled"
:
placeholder=
"placeholderText"
:maxlength=
"-1"
:focus=
"focus"
@
blur=
"focus = false"
:placeholder=
"placeholderText"
:maxlength=
"-1"
:focus=
"focus"
@
blur=
"focus = false"
placeholder-class=
"input-placeholder"
></textarea>
placeholder-class=
"input-placeholder"
></textarea>
</view>
</view>
<view
class=
"send-btn-box"
>
<view
class=
"send-btn-box"
:title=
"(msgLength && msgLength%2 !== 0) ? 'ai正在回复中不能发送':''"
>
<text
v-if=
"isWidescreen"
class=
"send-btn-tip"
>
↵ 发送 / shift + ↵ 换行
</text>
<text
v-if=
"isWidescreen"
class=
"send-btn-tip"
>
↵ 发送 / shift + ↵ 换行
</text>
<button
@
click=
"beforeSendMsg"
:disabled=
"inputBoxDisabled || !content"
class=
"send"
<button
@
click=
"beforeSendMsg"
:disabled=
"inputBoxDisabled || !content"
class=
"send"
type=
"primary"
>
发送
</button>
type=
"primary"
>
发送
</button>
...
@@ -97,16 +98,11 @@
...
@@ -97,16 +98,11 @@
},
},
// 输入框占位符文本
// 输入框占位符文本
placeholderText
()
{
placeholderText
()
{
// 如果输入框被禁用,则显示“uni-ai正在回复中”
// #ifdef H5
if
(
this
.
inputBoxDisabled
)
{
// 如果屏幕宽度大于960,则显示“请输入内容,ctrl + enter 发送”,否则显示“请输入要发给uni-ai的内容”
return
'
uni-ai正在回复中
'
return
window
.
innerWidth
>
960
?
'
请输入内容,ctrl + enter 发送
'
:
'
请输入要发给uni-ai的内容
'
}
else
{
// #endif
// #ifdef H5
return
'
请输入要发给uni-ai的内容
'
// 如果屏幕宽度大于960,则显示“请输入内容,ctrl + enter 发送”,否则显示“请输入要发给uni-ai的内容”
return
window
.
innerWidth
>
960
?
'
请输入内容,ctrl + enter 发送
'
:
'
请输入要发给uni-ai的内容
'
// #endif
return
'
请输入要发给uni-ai的内容
'
}
},
},
// 获取当前环境
// 获取当前环境
NODE_ENV
()
{
NODE_ENV
()
{
...
@@ -327,8 +323,23 @@
...
@@ -327,8 +323,23 @@
})
})
// 发送消息
// 发送消息
this
.
send
()
this
.
send
()
},
async
changeAnswer
(){
this
.
msgList
.
pop
()
// 防止 偶发答案涉及敏感,重复回答时。提问内容 被卡掉无法重新问
this
.
updateLastMsg
({
illegal
:
false
})
this
.
send
()
},
},
async
beforeSendMsg
()
{
async
beforeSendMsg
()
{
if
(
this
.
inputBoxDisabled
){
return
uni
.
showToast
({
title
:
'
ai正在回复中不能发送
'
,
icon
:
'
none
'
});
}
// 如果开启了广告位需要登录
// 如果开启了广告位需要登录
if
(
this
.
adpid
)
{
if
(
this
.
adpid
)
{
// 获取本地缓存的token
// 获取本地缓存的token
...
@@ -603,7 +614,17 @@
...
@@ -603,7 +614,17 @@
showCancel
:
false
showCancel
:
false
});
});
})
})
},
},
closeSseChannel
(){
if
(
sseChannel
){
sseChannel
.
close
()
sseChannel
=
false
}
// 将skip_callback设置为true,以便下一次请求可以正常回调
skip_callback
=
true
// 将流式响应计数值归零
this
.
sseIndex
=
0
},
// 滚动窗口以显示最新的一条消息
// 滚动窗口以显示最新的一条消息
showLastMsg
()
{
showLastMsg
()
{
// 等待DOM更新
// 等待DOM更新
...
@@ -658,9 +679,29 @@
...
@@ -658,9 +679,29 @@
box-sizing
:
border-box
;
box-sizing
:
border-box
;
}
}
/* #endif */
/* #endif */
.stop-responding
{
font-size
:
14px
;
border-radius
:
3px
;
margin-bottom
:
15px
;
background-color
:
#f0a020
;
color
:
#FFF
;
width
:
90px
;
height
:
30px
;
line-height
:
30px
;
margin
:
0
auto
;
justify-content
:
center
;
margin-bottom
:
15px
;
/* #ifdef H5 */
cursor
:
pointer
;
/* #endif */
}
.stop-responding
:hover
{
box-shadow
:
0
0
10px
#aaa
;
}
/* #ifndef APP-NVUE */
/* #ifndef APP-NVUE */
page
,
page
,
/*
#endif
*/
/*
#endif
*/
...
...
编辑
预览
Markdown
is supported
0%
请重试
或
添加新附件
.
添加附件
取消
You are about to add
0
people
to the discussion. Proceed with caution.
先完成此消息的编辑!
取消
想要评论请
注册
或
登录