Skip to content

Commit 60e3438

Browse files
committed
[1.7.0-feature] 仿chatgpt交互 todo:打字机效果待优化
1 parent 9a362a8 commit 60e3438

21 files changed

+736
-63
lines changed

app/build.gradle

+9
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,15 @@ dependencies {
147147
implementation 'com.github.promeg:tinypinyin-lexicons-android-cncity:2.0.3'
148148
implementation fileTree(dir: 'libs', include: ['*.jar'])
149149

150+
/**
151+
* markdown渲染
152+
*/
153+
def markwon_version = "4.6.2"
154+
implementation "io.noties.markwon:core:$markwon_version"
155+
implementation "io.noties.markwon:ext-latex:$markwon_version"
156+
implementation "io.noties.markwon:html:$markwon_version"
157+
implementation "io.noties.markwon:inline-parser:$markwon_version"
158+
150159
api project(':lib_protobuf')
151160

152161
if (compileMVPager2WithSource == "1") {

app/src/main/java/org/ninetripods/mq/study/kotlin/ktx/CommonExt.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ fun Context.log(message: String) {
2626
}
2727

2828
fun log(message: String) {
29-
Log.e("TTT", message)
29+
Log.e("Tag", message)
3030
}
3131

3232
fun Number.dp2px(): Int {
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,122 @@
11
package org.ninetripods.mq.study.activity
22

33
import android.os.Bundle
4+
import android.view.View
45
import androidx.appcompat.app.AppCompatActivity
56
import androidx.recyclerview.widget.LinearLayoutManager
67
import androidx.recyclerview.widget.RecyclerView
78
import org.ninetripods.mq.study.R
89
import org.ninetripods.mq.study.base.adapter.BaseAdapter
10+
import org.ninetripods.mq.study.base.iInterface.ItemClick
911
import org.ninetripods.mq.study.chatgpt.ChatVHolderFactory
12+
import org.ninetripods.mq.study.chatgpt.GuideItemModel
13+
import org.ninetripods.mq.study.chatgpt.GuideModel
1014
import org.ninetripods.mq.study.chatgpt.MessageModel
1115
import org.ninetripods.mq.study.kotlin.ktx.id
1216

1317
/**
1418
* 仿ChatGPT交互页面
1519
*/
16-
class ChatGptActivity : AppCompatActivity() {
20+
class ChatGptActivity : AppCompatActivity(), ItemClick<MessageModel> {
21+
22+
companion object {
23+
const val GUIDE_TEXT_1 = "今日天气如何?"
24+
const val GUIDE_TEXT_2 = "以春天为题写一首诗。"
25+
const val GUIDE_TEXT_3 = "明天的彩票号码是多少?"
26+
const val REPLY_TEXT_1 =
27+
"今天夜间到明天白天:晴间多云,西风一到二级,最高温度:25℃(摄氏度),最低温度:15℃(摄氏度)。空气质量:优。紫外线指数:强。 "
28+
const val REPLY_TEXT_2 = "春风拂面花香浓,\n" +
29+
"轻歌曼舞自心中。\n" +
30+
"莺啼燕语声声绕,\n" +
31+
"桃红柳绿画境容。\n" +
32+
"\n" +
33+
"山间溪畔寻幽趣,\n" +
34+
"芳草如茵踏绿途。\n" +
35+
"细雨濯清新心扉,\n" +
36+
"梨花雪片点春图。\n" +
37+
"\n" +
38+
"春水涟漪波澜动,\n" +
39+
"微风拂面心弦动。\n" +
40+
"花开花落轮回处,\n" +
41+
"岁月循环春永在。\n" +
42+
"\n" +
43+
"花开花谢芬芳在,\n" +
44+
"岁月循环春长在。\n" +
45+
"春风拂面花香浓,\n" +
46+
"轻歌曼舞自心中。"
47+
const val REPLY_TEXT_3 =
48+
"很抱歉,作为一个语言模型AI,我无法预测未来或者提供未来的信息,包括彩票号码。彩票号码是由随机算法产生的,没有任何规律可循,因此无法预测其结果。购买彩票是一种娱乐方式,但请记得理性购买,不要过度投入金钱。祝您好运!"
49+
}
1750

1851
private val mRv: RecyclerView by id(R.id.rv_view)
19-
private val chatAdapter by lazy { BaseAdapter<MessageModel>(ChatVHolderFactory()) }
52+
private val chatAdapter by lazy { BaseAdapter<MessageModel>(ChatVHolderFactory(), this) }
2053

2154
override fun onCreate(savedInstanceState: Bundle?) {
2255
super.onCreate(savedInstanceState)
2356
setContentView(R.layout.activity_layout_rv)
57+
mRv.layoutManager = LinearLayoutManager(this).apply { stackFromEnd = true }
58+
mRv.adapter = chatAdapter
2459
setRvInfo()
2560
}
2661

2762
private fun setRvInfo() {
2863
val list = mutableListOf<MessageModel>()
29-
list.add(MessageModel("1", "今天天气如何", type = ChatVHolderFactory.TYPE_ASK_TXT))
30-
list.add(MessageModel("1", "今天天气很棒", type = ChatVHolderFactory.TYPE_REPLY_TXT))
31-
list.add(MessageModel("1", "今天天气如何", type = ChatVHolderFactory.TYPE_REPLY_SPAN))
32-
for (i in 0..50) {
33-
list.add(MessageModel("1", "今天天气如何", type = ChatVHolderFactory.TYPE_ASK_TXT))
34-
}
64+
list.add(MessageModel(content = "hi,there!", type = ChatVHolderFactory.TYPE_REPLY_TXT, autoFillAnim = false))
65+
66+
list.add(
67+
MessageModel(
68+
type = ChatVHolderFactory.TYPE_REPLY_GUIDE,
69+
innerModel = GuideModel(
70+
sceneTypeName = "您可以像下面这样问我:",
71+
sceneList = mutableListOf<GuideItemModel>().apply {
72+
add(GuideItemModel(sceneName = "今日天气如何?"))
73+
add(GuideItemModel(sceneName = "以春天为题写一首诗。"))
74+
add(GuideItemModel(sceneName = "明天的彩票号码是多少?"))
75+
})
76+
)
77+
)
78+
79+
// list.add(MessageModel(content = "天气预报", type = ChatVHolderFactory.TYPE_ASK_TXT))
80+
// list.add(MessageModel(content = "天气情况如下:", type = ChatVHolderFactory.TYPE_REPLY_TXT))
81+
// list.add(MessageModel(type = ChatVHolderFactory.TYPE_REPLY_SPAN))
82+
// for (i in 0..20) {
83+
// list.add(MessageModel(content = "天气预报", type = ChatVHolderFactory.TYPE_ASK_TXT))
84+
// }
3585
chatAdapter.submitList(list)
36-
mRv.layoutManager = LinearLayoutManager(this)
37-
mRv.adapter = chatAdapter
86+
}
87+
88+
override fun itemClick(itemView: View, model: MessageModel?, position: Int) {
89+
when (itemView.id) {
90+
R.id.sample_item -> {
91+
val guideItemModel: GuideItemModel? =
92+
(model?.innerModel as? GuideModel)?.sceneList?.get(position)
93+
guideItemModel?.let {
94+
chatAdapter.addModel(
95+
MessageModel(
96+
content = it.sceneName,
97+
type = ChatVHolderFactory.TYPE_ASK_TXT
98+
)
99+
)
100+
val replyTxt = when (it.sceneName) {
101+
GUIDE_TEXT_1 -> REPLY_TEXT_1
102+
GUIDE_TEXT_2 -> REPLY_TEXT_2
103+
GUIDE_TEXT_3 -> REPLY_TEXT_3
104+
else -> REPLY_TEXT_1
105+
}
106+
chatAdapter.addModel(
107+
MessageModel(
108+
content = replyTxt,
109+
type = ChatVHolderFactory.TYPE_REPLY_TXT
110+
)
111+
)
112+
scrollToBottom()
113+
}
114+
}
115+
}
116+
}
117+
118+
private fun scrollToBottom() {
119+
val position = chatAdapter.itemCount - 1
120+
mRv.scrollToPosition(position)
38121
}
39122
}

app/src/main/kotlin/org/ninetripods/mq/study/base/adapter/BaseAdapter.kt

+14-6
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,17 @@ import android.view.ViewGroup
44
import androidx.recyclerview.widget.DiffUtil
55
import androidx.recyclerview.widget.ListAdapter
66
import androidx.recyclerview.widget.RecyclerView
7+
import org.ninetripods.mq.study.base.iInterface.ItemClick
78
import org.ninetripods.mq.study.chatgpt.ChatDiffUtil
89

910
/**
1011
* Created by mq on 2023/7/20
1112
* RecyclerView.Adapter基类
1213
*/
13-
open class BaseAdapter<T : Any>(private val vhFactory: IVHFactory) :
14+
open class BaseAdapter<T : Any>(
15+
private val vhFactory: IVHFactory,
16+
private val itemClick: ItemClick<T>? = null,
17+
) :
1418
RecyclerView.Adapter<BaseVHolder<T>>() {
1519
private val models = mutableListOf<T>()
1620

@@ -21,7 +25,10 @@ open class BaseAdapter<T : Any>(private val vhFactory: IVHFactory) :
2125
}
2226

2327
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseVHolder<T> {
24-
return vhFactory.getVH(parent.context, parent, viewType) as BaseVHolder<T>
28+
val holder = vhFactory.getVH(parent.context, parent, viewType) as BaseVHolder<T>
29+
//绑定点击事件
30+
holder.bindItemClick(itemClick)
31+
return holder
2532
}
2633

2734
override fun getItemCount(): Int = models.size
@@ -30,10 +37,6 @@ open class BaseAdapter<T : Any>(private val vhFactory: IVHFactory) :
3037
holder.onBindViewHolder(models[position], position)
3138
}
3239

33-
fun submit(item: T) {
34-
35-
}
36-
3740
fun submitList(newList: List<T>) {
3841
//传入新旧数据进行比对
3942
val diffUtil = ChatDiffUtil(models, newList)
@@ -45,6 +48,11 @@ open class BaseAdapter<T : Any>(private val vhFactory: IVHFactory) :
4548
//将数据传给adapter,最终通过adapter.notifyItemXXX更新数据
4649
diffResult.dispatchUpdatesTo(this)
4750
}
51+
52+
fun addModel(model: T) {
53+
models.add(model)
54+
notifyItemInserted(itemCount)
55+
}
4856
}
4957

5058
/**

app/src/main/kotlin/org/ninetripods/mq/study/base/adapter/BaseVHolder.kt

+21
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,35 @@ import android.view.LayoutInflater
55
import android.view.View
66
import android.view.ViewGroup
77
import androidx.recyclerview.widget.RecyclerView
8+
import org.ninetripods.mq.study.base.iInterface.ItemClick
9+
import org.ninetripods.mq.study.base.iInterface.ItemClickListener
810

911
/**
1012
* Created by mq on 2023/7/19
1113
*/
1214
abstract class BaseVHolder<T>(context: Context, parent: ViewGroup, resource: Int) :
1315
RecyclerView.ViewHolder(LayoutInflater.from(context).inflate(resource, parent, false)) {
1416

17+
protected val onItemClickListener by lazy { ItemClickListener<T>() }
18+
19+
fun bindItemClick(itemClick: ItemClick<T>? = null) {
20+
itemClick?.let {
21+
onItemClickListener.itemClick = it
22+
onBindVHClick(onItemClickListener)
23+
}
24+
}
25+
26+
/**
27+
* 在子ViewHolder中实现
28+
*/
29+
open fun onBindVHClick(clickListener: ItemClickListener<T>) {}
30+
1531
fun onBindViewHolder(item: T, position: Int) {
32+
//补充Model及位置position
33+
onItemClickListener.itemClick?.let {
34+
onItemClickListener.model = item
35+
onItemClickListener.position = position
36+
}
1637
onBindView(item, position)
1738
}
1839

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package org.ninetripods.mq.study.base.iInterface
2+
3+
import android.view.View
4+
5+
/**
6+
* Created by mq on 2023/7/27
7+
*/
8+
interface ItemClick<T> {
9+
fun itemClick(itemView: View, model: T?, position: Int)
10+
}
11+
12+
class ItemClickListener<T> : View.OnClickListener {
13+
var itemClick: ItemClick<T>? = null
14+
var position: Int = -1
15+
var model: T? = null
16+
17+
override fun onClick(v: View?) {
18+
v?.let { itemClick?.itemClick(v, model, position) }
19+
}
20+
21+
}

app/src/main/kotlin/org/ninetripods/mq/study/chatgpt/ChatDiffUtil.kt

+7-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.ninetripods.mq.study.chatgpt
22

33
import androidx.recyclerview.widget.DiffUtil
4+
import org.ninetripods.mq.study.kotlin.ktx.log
45

56
class ChatDiffUtil(private val oldModels: List<Any>, private val newModels: List<Any>) :
67
DiffUtil.Callback() {
@@ -20,15 +21,19 @@ class ChatDiffUtil(private val oldModels: List<Any>, private val newModels: List
2021
* 例如,如果你的项目有唯一的id,这个方法应该检查它们的id是否相等。
2122
*/
2223
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
23-
return oldModels[oldItemPosition]::class.java == newModels[newItemPosition]::class.java
24+
val areItemsSame = oldModels[oldItemPosition]::class.java == newModels[newItemPosition]::class.java
25+
log("areItemsSame:$areItemsSame")
26+
return areItemsSame
2427
}
2528

2629
/**
2730
* 比较两个Item是否有相同的内容(用于判断Item的内容是否发生了改变),
2831
* 该方法只有当areItemsTheSame (int, int)返回true时才会被调用。
2932
*/
3033
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
31-
return oldModels[oldItemPosition] == newModels[newItemPosition]
34+
val areContentSame = oldModels[oldItemPosition] == newModels[newItemPosition]
35+
log("areContentSame:$areContentSame, $oldItemPosition, $newItemPosition")
36+
return areContentSame
3237
}
3338

3439
/**

app/src/main/kotlin/org/ninetripods/mq/study/chatgpt/ChatVHolderFactory.kt

+6-3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import android.view.ViewGroup
55
import org.ninetripods.mq.study.base.adapter.BaseVHolder
66
import org.ninetripods.mq.study.base.adapter.IVHFactory
77
import org.ninetripods.mq.study.chatgpt.viewholder.ChatAskHolder
8+
import org.ninetripods.mq.study.chatgpt.viewholder.ChatReplyGuideHolder
89
import org.ninetripods.mq.study.chatgpt.viewholder.ChatReplyImgTextHolder
910
import org.ninetripods.mq.study.chatgpt.viewholder.ChatReplyTxHolder
1011

@@ -13,16 +14,18 @@ import org.ninetripods.mq.study.chatgpt.viewholder.ChatReplyTxHolder
1314
*/
1415
class ChatVHolderFactory : IVHFactory {
1516
companion object {
16-
const val TYPE_ASK_TXT = 1
17-
const val TYPE_REPLY_TXT = 2
18-
const val TYPE_REPLY_SPAN = 3
17+
const val TYPE_ASK_TXT = 1 //type1
18+
const val TYPE_REPLY_TXT = 2 //type2
19+
const val TYPE_REPLY_SPAN = 3 //type3
20+
const val TYPE_REPLY_GUIDE = 4//type4
1921
}
2022

2123
override fun getVH(context: Context, parent: ViewGroup, viewType: Int): BaseVHolder<*> {
2224
return when (viewType) {
2325
TYPE_ASK_TXT -> ChatAskHolder(context, parent)
2426
TYPE_REPLY_TXT -> ChatReplyTxHolder(context, parent)
2527
TYPE_REPLY_SPAN -> ChatReplyImgTextHolder(context, parent)
28+
TYPE_REPLY_GUIDE -> ChatReplyGuideHolder(context, parent)
2629
else -> throw IllegalStateException("unSupport type")
2730
}
2831
}

app/src/main/kotlin/org/ninetripods/mq/study/chatgpt/MessageModel.kt

+25-2
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,40 @@ import org.ninetripods.mq.study.base.adapter.IMultiType
66
* Created by mq on 2023/7/20
77
*/
88
data class MessageModel(
9-
var msgId: String,
10-
var content: String,
9+
var msgId: String = "",
10+
var content: String = "",
11+
var innerModel: IItemModel? = null,
1112
//封禁状态 正常 = 1 全屏蔽 = 2 部分屏蔽 = 3 无需审核 = 4 用户被封禁 = 5
1213
var banStatus: Int = 1,
1314
//传输中 = 0, 传输成功 = 1, 传输失败 = 2
1415
var status: Int = 0,
1516
var type: Int = ChatVHolderFactory.TYPE_ASK_TXT,
17+
var autoFillAnim: Boolean = true, //是否需要填充动画
1618
) : IMultiType {
1719
override fun getItemViewType(): Int = type
1820
}
1921

22+
interface IItemModel
23+
24+
data class GuideModel(
25+
var sceneTypeId: String = "",
26+
var sceneTypeName: String = "",
27+
var isGuide: String = "",
28+
var sceneList: List<GuideItemModel>? = null,
29+
) : IItemModel
30+
31+
data class GuideItemModel(
32+
var sceneId: Int = 0,
33+
var describe: Boolean = false,
34+
var sceneName: String = "",
35+
var icon: String = "",
36+
var ask: String = "",
37+
var reply: String = "",
38+
39+
//呈现样式: 0,默认,1是button样式
40+
var Style: Int = 0,
41+
)
42+
2043
data class CardItemModel(
2144
var action: Int = 0,
2245
var sceneId: Int = 0,

app/src/main/kotlin/org/ninetripods/mq/study/chatgpt/decoration/GirdItemDecoration.kt

+3-2
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,16 @@ import android.view.View
55
import androidx.recyclerview.widget.GridLayoutManager
66
import androidx.recyclerview.widget.RecyclerView
77

8-
class GirdItemDecoration(val rowSpace: Int, val columnSpace: Int) : RecyclerView.ItemDecoration() {
8+
class GirdItemDecoration(private val rowSpace: Int, private val columnSpace: Int) :
9+
RecyclerView.ItemDecoration() {
910

1011
override fun getItemOffsets(
1112
outRect: Rect,
1213
view: View,
1314
parent: RecyclerView,
1415
state: RecyclerView.State,
1516
) {
16-
var layoutManager = parent.layoutManager
17+
val layoutManager = parent.layoutManager
1718
if (layoutManager is GridLayoutManager) {
1819
val position = parent.getChildLayoutPosition(view)
1920
val spanCount = layoutManager.spanCount

0 commit comments

Comments
 (0)