Skip to content
体验新版
项目
组织
正在加载...
登录
切换导航
打开侧边栏
DCloud
uni-ai-chat
提交
96d615d7
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看板
提交
96d615d7
编写于
4月 24, 2023
作者:
DCloud_JSON
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
适配pc端
上级
f732c299
变更
6
隐藏空白更改
内联
并排
Showing
6 changed file
with
299 addition
and
82 deletion
+299
-82
App.vue
App.vue
+31
-2
manifest.json
manifest.json
+2
-2
pages.json
pages.json
+1
-1
pages/chat/chat.vue
pages/chat/chat.vue
+251
-69
redme.md
redme.md
+14
-8
static/remove.png
static/remove.png
+0
-0
未找到文件。
App.vue
浏览文件 @
96d615d7
<
script
>
export
default
{
onLaunch
:
function
()
{
console
.
log
(
'
App Launch
'
)
console
.
log
(
'
App Launch
'
)
let
version
=
uni
.
getSystemInfoSync
().
uniRuntimeVersion
function
toNum
(
a
){
const
c
=
a
.
toString
().
split
(
'
.
'
);
const
num_place
=
[
""
,
"
0
"
,
"
00
"
,
"
000
"
,
"
0000
"
],
r
=
num_place
.
reverse
();
for
(
let
i
=
0
;
i
<
c
.
length
;
i
++
){
const
len
=
c
[
i
].
length
;
c
[
i
]
=
r
[
len
]
+
c
[
i
];
}
return
c
.
join
(
''
);
}
if
(
toNum
(
version
)
<
toNum
(
"
3.8.0
"
)){
uni
.
showModal
({
content
:
'
本示例的HBuilderX版本不得低于3.8.0,请升级
'
,
showCancel
:
false
});
}
},
onShow
:
function
()
{
console
.
log
(
'
App Show
'
)
...
...
@@ -12,5 +29,17 @@
}
</
script
>
<
style
>
<
style
lang=
"scss"
>
/*每个页面公共css */
/* #ifdef H5 */
@media
screen
and
(
min-width
:
650px
){
/* pc宽屏 隐藏会话页面头部 && 全局底部导航 以下兼容了Vue2和3两种模式的样式*/
uni-page
[
data-page
=
"pages/chat/chat"
]
uni-page-head
,
{
display
:
none
!
important
;
background-color
:
red
!
important
;
}
/* #endif */
}
</
style
>
manifest.json
浏览文件 @
96d615d7
...
...
@@ -68,10 +68,10 @@
"uniStatistics"
:
{
"enable"
:
false
},
"vueVersion"
:
"
2
"
,
"vueVersion"
:
"
3
"
,
"h5"
:
{
"unipush"
:
{
"enable"
:
tru
e
"enable"
:
fals
e
}
}
}
pages.json
浏览文件 @
96d615d7
...
...
@@ -4,7 +4,7 @@
"path"
:
"pages/chat/chat"
,
"style"
:
{
"navigationBarTitleText"
:
"uni-ai"
,
"navigationBarTitleText"
:
"uni-ai
-chat
"
,
"enablePullDownRefresh"
:
false
}
}
...
...
pages/chat/chat.vue
浏览文件 @
96d615d7
<
template
>
<view
class=
"page"
>
<view
class=
"container"
>
<view
class=
"page"
>
<view
class=
"container"
>
<view
v-if=
"isWidescreen"
class=
"header"
>
uni-im-chat
</view>
<text
class=
"noData"
v-if=
"msgList.length === 0"
>
没有对话记录
</text>
<scroll-view
:scroll-into-view=
"scrollIntoView"
scroll-y=
"true"
class=
"msg-list"
:enable-flex=
"true"
>
<view
v-for=
"(msg,index) in msgList"
class=
"msg-item"
:key=
"index"
>
<uni-dateformat
class=
"create_time"
:date=
"msg.create_time"
format=
"MM/dd hh:mm:ss"
></uni-dateformat>
<view
v-for=
"(msg,index) in msgList"
class=
"msg-item"
:key=
"index"
>
<view
class=
"create_time-box"
>
<uni-dateformat
class=
"create_time"
:date=
"msg.create_time"
format=
"MM/dd hh:mm:ss"
></uni-dateformat>
</view>
<view
:class=
"
{reverse:!msg.isAi}">
<view
class=
"userInfo"
>
<image
class=
"avatar"
:src=
"msg.isAi?'../../static/uni-ai.png':'../../static/avatar.png'"
mode=
"widthFix"
></image>
</view>
<view
style=
"flex-direction: column;"
>
<view
class=
"content"
>
<uni-ai-msg
:md=
"msg.content"
:show-cursor=
"index == msgList.length-1 && msg.isAi && sseIndex"
></uni-ai-msg>
<!--
<text
:selectable=
"true"
>
{{
msg
.
content
}}
</text>
-->
</view>
<view
class=
"content"
>
<uni-ai-msg
:md=
"msg.content"
:show-cursor=
"index == msgList.length-1 && msg.isAi && sseIndex"
></uni-ai-msg>
</view>
<uni-icons
v-if=
"index == msgList.length-1 && !msg.isAi && msg.state != 100 && msgStateIcon(msg)"
@
click=
"msg.state == -100 ? retriesSendMsg() : ''"
:color=
"msg.state===0?'#999':'#d22'"
...
...
@@ -24,23 +24,45 @@
<view
id=
"last-msg-item"
></view>
</scroll-view>
<view
class=
"foot-box"
>
<view
class=
"trash"
>
<uni-icons
@
click=
"clear"
type=
"trash"
size=
"24"
color=
"#888"
></uni-icons>
</view>
<view
class=
"textarea-box"
>
<textarea
v-model=
"content"
class=
"textarea"
:auto-height=
"true"
:disabled=
"inputBoxDisabled"
:placeholder=
"placeholderText"
:maxlength=
"-1"
placeholder-class=
"input-placeholder"
></textarea>
<view
class=
"foot-box"
>
<view
class=
"menu"
v-if=
"isWidescreen"
>
<view
class=
"trash menu-item"
>
<image
@
click=
"clear"
src=
"@/static/remove.png"
mode=
"heightFix"
></image>
</view>
<view
class=
"set-stream menu-item"
>
<view
class=
"title"
>
<text>
流式响应
</text>
<uni-icons
@
click=
"toStreamMD"
class=
"help"
type=
"help"
></uni-icons>
<text>
:
</text>
</view>
<switch
:checked=
"stream"
@
change=
"changeStream"
/>
</view>
</view>
<view
class=
"send-btn-box"
>
<button
@
click=
"beforeSendMsg"
:disabled=
"inputBoxDisabled || !content"
class=
"send"
>
发送
</button>
<view
class=
"foot-box-content"
>
<view
v-if=
"!isWidescreen"
class=
"trash"
>
<uni-icons
@
click=
"clear"
type=
"trash"
size=
"24"
color=
"#888"
></uni-icons>
</view>
<view
class=
"textarea-box"
>
<textarea
v-model=
"content"
class=
"textarea"
:auto-height=
"!isWidescreen"
:disabled=
"inputBoxDisabled"
:placeholder=
"placeholderText"
:maxlength=
"-1"
placeholder-class=
"input-placeholder"
></textarea>
</view>
<view
class=
"send-btn-box"
>
<text
v-if=
"isWidescreen"
class=
"send-btn-tip"
>
↵ 发送 / shift + ↵ 换行
</text>
<button
@
click=
"beforeSendMsg"
:disabled=
"inputBoxDisabled || !content"
class=
"send"
type=
"primary"
>
发送
</button>
</view>
</view>
</view>
<view
v-if=
"!isWidescreen"
id=
"set-stream"
>
<view
class=
"title"
>
<text>
流式响应
</text>
<uni-icons
@
click=
"toStreamMD"
class=
"help"
type=
"help"
></uni-icons>
<text>
:
</text>
</view>
<switch
:checked=
"stream"
@
change=
"changeStream"
/>
</view>
</view>
<label
class=
"set-stream"
>
<text
style=
"font-size: 12px;"
>
流式响应:
</text>
<switch
style=
"transform: scale(0.7);"
:checked=
"stream"
@
change=
"changeStream"
/>
</label>
</view>
</
template
>
...
...
@@ -52,7 +74,8 @@
msgList
:
[],
content
:
""
,
sseIndex
:
0
,
stream
:
false
stream
:
false
,
isWidescreen
:
false
}
},
computed
:
{
...
...
@@ -113,7 +136,7 @@
//按下了shift ctrl alt windows键
adjunctKeydown
=
true
;
}
if
(
e
.
keyCode
==
13
&&
adjunctKeydown
)
{
if
(
e
.
keyCode
==
13
&&
!
adjunctKeydown
)
{
this
.
beforeSendMsg
();
}
};
...
...
@@ -124,13 +147,59 @@
}
};
}
// #endif
// #endif
// 添加惰性函数,检查是否开通push
this
.
changeStream
.
check
=
async
()
=>
{
uni
.
getPushClientId
({
fail
:()
=>
{
this
.
stream
=
false
uni
.
showModal
({
content
:
'
你暂未开通uni-push。不支持此功能
'
,
showCancel
:
false
,
confirmText
:
"
查看详情
"
,
complete
()
{
let
url
=
"
https://uniapp.dcloud.net.cn/uniCloud/uni-ai-chat.html#%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9
"
// #ifndef H5
uni
.
setClipboardData
({
data
:
url
,
showToast
:
false
,
success
()
{
uni
.
showToast
({
title
:
'
已复制文档链接,请到浏览器粘贴浏览
'
,
icon
:
'
none
'
,
duration
:
5000
});
}
})
// #endif
// #ifdef H5
window
.
open
(
url
)
// #endif
}
});
console
.
log
(
'
你暂未开通uni-push。不支持此功能。详情:https://uniapp.dcloud.net.cn/uniCloud/uni-ai-chat.html#%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9
'
);
},
success
:()
=>
{
this
.
changeStream
.
check
=
()
=>
{}
}
})
}
uni
.
createMediaQueryObserver
(
this
).
observe
({
minWidth
:
650
,
},
matches
=>
{
this
.
isWidescreen
=
matches
;
})
},
methods
:
{
// updateLastMsg(){
// },
changeStream
(
e
){
this
.
changeStream
.
check
()
// console.log('e',e.detail.value);
this
.
stream
=
e
.
detail
.
value
},
...
...
@@ -302,6 +371,25 @@
}
});
},
toStreamMD
(){
let
url
=
"
https://uniapp.dcloud.net.cn/uniCloud/uni-ai.html#chat-completion-stream
"
// #ifndef H5
uni
.
setClipboardData
({
data
:
url
,
showToast
:
false
,
success
()
{
uni
.
showToast
({
title
:
'
已复制文档链接,请到浏览器粘贴浏览
'
,
icon
:
'
none
'
,
duration
:
5000
});
}
})
// #endif
// #ifdef H5
window
.
open
(
url
)
// #endif
}
}
}
...
...
@@ -312,13 +400,10 @@
view
,
textarea
,
button
,
.page
,
/*
#ifdef
H5
*/
.page
*
/*
#endif
*/
.page
{
display
:
flex
;
box-sizing
:
border-box
;
box-sizing
:
border-box
;
}
/* #endif */
...
...
@@ -329,38 +414,34 @@
.page
,
.container
{
background-color
:
#efefef
;
width
:
750rpx
;
flex
:
1
;
flex
:
1
;
/* #ifdef H5 */
height
:
calc
(
100vh
-
44px
);
/* #endif */
/* #ifndef H5 */
height
:
100vh
;
/* #endif */
flex-direction
:
column
;
align-items
:
center
;
}
align-items
:
center
;
justify-content
:
center
;
}
/* #ifndef APP-NVUE */
.container
{
background-color
:
#FAFAFA
;
}
.container
,
.container
*
{
max-width
:
550px
;
background-color
:
#FAFAFA
;
}
/* #endif */
.foot-box
{
width
:
750rpx
;
display
:
flex
;
justify-content
:
space-around
;
align-items
:
self-start
;
flex-direction
:
column
;
padding
:
10px
0px
;
background-color
:
#FFF
;
}
.foot-box-content
{
justify-content
:
space-around
;
}
.textarea-box
{
.textarea-box
{
padding
:
8px
10px
;
background-color
:
#f9f9f9
;
border-radius
:
5px
;
...
...
@@ -368,11 +449,11 @@
.textarea-box
.textarea
{
max-height
:
100px
;
width
:
460rpx
;
font-size
:
14px
;
/* #ifndef APP-NVUE */
overflow
:
auto
;
/* #endif */
/* #endif */
width
:
450rpx
;
}
/* #ifndef APP-NVUE */
...
...
@@ -388,7 +469,6 @@
.trash
,
.send
{
/* border: 1px solid #000; */
width
:
50px
;
height
:
30px
;
justify-content
:
center
;
...
...
@@ -397,12 +477,11 @@
}
.trash
{
width
:
30rpx
;
margin-left
:
2
0rpx
;
width
:
30rpx
;
margin-left
:
1
0rpx
;
}
.send
{
background-color
:
#2eaf4d
;
color
:
#FFF
;
border-radius
:
4px
;
display
:
flex
;
...
...
@@ -463,7 +542,9 @@
}
.content
{
max-width
:
500rpx
;
/* #ifndef APP-NVUE */
max-width
:
550rpx
;
/* #endif */
background-color
:
#FFF
;
border-radius
:
5px
;
padding
:
12px
10px
;
...
...
@@ -507,33 +588,134 @@
justify-content
:
center
;
}
.set-stream
{
position
:
fixed
;
left
:
0px
;
top
:
0
;
#set-stream
{
z-index
:
999
;
padding
:
0
5px
;
justify-content
:
center
;
align-items
:
center
;
position
:
fixed
;
bottom
:
50px
;
right
:
0
;
}
#set-stream
.title
{
font-size
:
12px
;
position
:
relative
;
left
:
10rpx
;
}
#set-stream
switch
{
transform
:
scale
(
0
.5
);
}
/* #ifdef H5 */
@media
screen
and
(
min-width
:
600px
){
.textarea-box
{
flex
:
1
@media
screen
and
(
min-width
:
650px
){
.foot-box
{
border-top
:
solid
1px
#dde0e2
;
}
.page
{
width
:
100vw
;
flex-direction
:
row
;
}
.page
*
{
max-width
:
950px
;
}
.container
,
{
box-shadow
:
0
0
5px
#e0e1e7
;
margin-top
:
44px
;
border-radius
:
10px
;
overflow
:
hidden
;
}
.container
.header
{
height
:
44px
;
line-height
:
44px
;
border-bottom
:
1px
solid
#F0F0F0
;
width
:
100vw
;
justify-content
:
center
;
font-weight
:
500
;
}
.content
{
background-color
:
#f9f9f9
;
}
.foot-box
,
.foot-box-content
,
.msg-list
,
.msg-item
,
//
.create_time
,
.noData
,
.textarea-box
,
.textarea
,
textarea-box
{
width
:
100%
!
important
;
}
.trash
,
.send-btn-box
{
flex-shrink
:
0
;
width
:
100px
;
.create_time-box
{
margin-top
:
15px
;
justify-content
:
center
;
padding
:
0
;
margin
:
0
;
}
.trash
{
width
:
60px
;
.create_time
{
display
:
flex
;
}
.textarea-box
,
.textarea
,
textarea
,
textarea-box
{
height
:
120px
;
}
.container
,
.foot-box
,
.textarea-box
{
background-color
:
#FFF
;
}
.foot-box-content
{
flex-direction
:
column
;
justify-content
:
center
;
align-items
:
flex-end
;
padding-bottom
:
0
;
}
.menu
{
padding
:
0
10px
;
}
.menu-item
{
height
:
20px
;
justify-content
:
center
;
align-items
:
center
;
align-content
:
center
;
display
:
flex
;
margin-right
:
10px
;
cursor
:
pointer
;
}
.trash
{
opacity
:
0
.8
;
}
.trash
image
{
height
:
15px
;
}
.set-stream
.title
{
font-size
:
12px
;
}
.set-stream
switch
{
transform
:
scale
(
0
.6
);
position
:
relative
;
left
:
-5px
;
}
.textarea-box
,
.textarea-box
*
{
// border: 1px solid #000;
}
.textarea-box
.textarea
{
width
:
100%
;
.send-btn-box
.send-btn-tip
{
color
:
#919396
;
margin-right
:
8px
;
font-size
:
12px
;
line-height
:
28px
;
}
}
/* #endif */
...
...
redme.md
浏览文件 @
96d615d7
## 简介
> 仅支持3.8.0及以上版本的HBuilderX
uni-ai-chat是基于
[
uni-ai
](
https://uniapp.dcloud.net.cn/uniCloud/uni-ai.html
)
的云端一体AI项目模板
视频效果:
...
...
@@ -6,6 +9,16 @@ uni-ai-chat是基于[uni-ai](https://uniapp.dcloud.net.cn/uniCloud/uni-ai.html)
包含一个前端页面(路径:
`/pages/chat/chat.vue`
)和一个云对象(路径:
`uniCloud/cloudfunctions/uni-ai-chat/index.obj.js`
)
## 响应流程图
<img
width=
"400px"
src=
"https://web-assets.dcloud.net.cn/unidoc/zh/uni-ai-chat/20230424211201.jpg"
>
-
普通响应的流程较为简单,全流程基于 HTTP 网络请求。其缺陷是,当访问 AI 聊天接口时,如果生成的回复内容过大,响应时间会很长,导致前端用户需要等待很长时间才能收到结果。详情请参考:
[
stream的优势
](
https://uniapp.dcloud.net.cn/uniCloud/uni-ai.html#chat-completion-stream
)
。
-
流式响应的使用需客户端先通过
`new uniCloud.SSEChannel()`
创建 SSE 通道,并获得
`channel`
值(详情请参考:
[
https://uniapp.dcloud.net.cn/uniCloud/sse-channel.html#create-sse-channel
](
https://uniapp.dcloud.net.cn/uniCloud/sse-channel.html#create-sse-channel
)
)。在客户端向 uniCloud 云对象发起请求时,需要将该
`channel`
值作为参数一同携带;然后 uniCloud 云对象与 uni-ai 建立 流式响应
[
(stream)
](
https://uniapp.dcloud.net.cn/uniCloud/uni-ai.html#chat-completion-stream
)
通讯,云对象监听 uni-ai 返回的分片数据,在接收到数据后再通过 sse-channel (
[
反序列化消息通道
](
https://uniapp.dcloud.net.cn/uniCloud/sse-channel.html#cloud-deserialize-channel
)
)向客户端推送消息。
### 注意事项 @heed
-
使用
`sse-channel`
需要先
[
开通uni-push
](
https://uniapp.dcloud.net.cn/unipush-v2.html#%E7%AC%AC%E4%B8%80%E6%AD%A5-%E5%BC%80%E9%80%9A
)
-
目前uni-push2.0不支持本地调试(后续版本会支持),需要在HBuilderX控制台,更改
`连接本地云函数`
为
`连接云端云函数`
。
## 体验步骤
...
...
@@ -14,11 +27,4 @@ uni-ai-chat是基于[uni-ai](https://uniapp.dcloud.net.cn/uniCloud/uni-ai.html)
3.
打开
`uni-ai-chat`
插件下载地址:
[
https://ext.dcloud.net.cn/plugin?name=uni-ai-chat
](
https://ext.dcloud.net.cn/plugin?name=uni-ai-chat
)
4.
点击
`使用HBuilderX导入示例项目`
5.
对项目根目录uniCloud点右键选择“云服务空间初始化向导”界面按提示部署项目
6.
在uni-app项目点右键,关联之前创建的服务空间。
## 注意事项
uni-ai-chat支持
[
流式响应
](
https://uniapp.dcloud.net.cn/uniCloud/uni-ai.html#%E6%B5%81%E5%BC%8F%E5%93%8D%E5%BA%94-chat-completion-stream
)
模式,该模式会接收uni-ai的流式响应的数据,通过sse-channel
[
(云函数(云对象)请求中的中间状态通知通道)
](
https://uniapp.dcloud.net.cn/uniCloud/sse-channel.html
)
向客户端推送消息,
而使用
`sse-channel`
需要先
[
开通uni-push
](
https://uniapp.dcloud.net.cn/unipush-v2.html#%E7%AC%AC%E4%B8%80%E6%AD%A5-%E5%BC%80%E9%80%9A
)
,目前uni-push2.0不支持本地调试(后续版本会支持),需要在HBuilderX控制台,更改
`连接本地云函数`
为
`连接云端云函数`
。
6.
在uni-app项目点右键,关联之前创建的服务空间。
\ No newline at end of file
static/remove.png
0 → 100644
浏览文件 @
96d615d7
2.6 KB
编辑
预览
Markdown
is supported
0%
请重试
或
添加新附件
.
添加附件
取消
You are about to add
0
people
to the discussion. Proceed with caution.
先完成此消息的编辑!
取消
想要评论请
注册
或
登录