Skip to content

Commit 2bff26a

Browse files
committed
feat: Chat vote
1 parent 6d1bbd9 commit 2bff26a

File tree

10 files changed

+199
-26
lines changed

10 files changed

+199
-26
lines changed

apps/application/serializers/application.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -245,8 +245,9 @@ def is_valid(self, *, user_id=None, raise_exception=False):
245245
'knowledge_id_list': self.data.get('knowledge_id_list')}).is_valid()
246246

247247
@staticmethod
248-
def to_application_model(user_id: str, application: Dict):
248+
def to_application_model(user_id: str, workspace_id: str, application: Dict):
249249
return Application(id=uuid.uuid7(), name=application.get('name'), desc=application.get('desc'),
250+
workspace_id=workspace_id,
250251
prologue=application.get('prologue'),
251252
dialogue_number=application.get('dialogue_number', 0),
252253
user_id=user_id, model_id=application.get('model_id'),
@@ -321,7 +322,7 @@ def get_query_set(self, instance: Dict, workspace_manage: bool, is_x_pack_ee: bo
321322
'folder_query_set': folder_query_set,
322323
'application_query_set': application_query_set,
323324
'application_custom_sql': application_custom_sql_query_set
324-
} if (workspace_manage and is_x_pack_ee) else {'folder_query_set': folder_query_set,
325+
} if (workspace_manage and not is_x_pack_ee) else {'folder_query_set': folder_query_set,
325326
'application_query_set': application_query_set,
326327
'user_query_set': QuerySet(
327328
workspace_user_role_mapping_model).filter(
@@ -442,8 +443,10 @@ def to_application_knowledge_mapping(application_id: str, dataset_id: str):
442443
def insert_simple(self, instance: Dict):
443444
self.is_valid(raise_exception=True)
444445
user_id = self.data.get('user_id')
446+
workspace_id = self.data.get("workspace_id")
445447
ApplicationCreateSerializer.SimplateRequest(data=instance).is_valid(user_id=user_id, raise_exception=True)
446-
application_model = ApplicationCreateSerializer.SimplateRequest.to_application_model(user_id, instance)
448+
application_model = ApplicationCreateSerializer.SimplateRequest.to_application_model(user_id, workspace_id,
449+
instance)
447450
dataset_id_list = instance.get('knowledge_id_list', [])
448451
application_knowledge_mapping_model_list = [
449452
self.to_application_knowledge_mapping(application_model.id, dataset_id) for

apps/chat/api/vote_api.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# coding=utf-8
2+
"""
3+
@project: MaxKB
4+
@Author:虎虎
5+
@file: vote_api.py
6+
@date:2025/6/23 17:35
7+
@desc:
8+
"""
9+
from django.utils.translation import gettext_lazy as _
10+
from drf_spectacular.types import OpenApiTypes
11+
from drf_spectacular.utils import OpenApiParameter
12+
13+
from chat.serializers.chat_record import VoteRequest
14+
from common.mixins.api_mixin import APIMixin
15+
from common.result import DefaultResultSerializer
16+
17+
18+
class VoteAPI(APIMixin):
19+
@staticmethod
20+
def get_request():
21+
return VoteRequest
22+
23+
@staticmethod
24+
def get_parameters():
25+
return [OpenApiParameter(
26+
name="chat_id",
27+
description=_("Chat ID"),
28+
type=OpenApiTypes.STR,
29+
location='path',
30+
required=True,
31+
),
32+
OpenApiParameter(
33+
name="chat_record_id",
34+
description=_("Chat Record ID"),
35+
type=OpenApiTypes.STR,
36+
location='path',
37+
required=True,
38+
)
39+
]
40+
41+
@staticmethod
42+
def get_response():
43+
return DefaultResultSerializer
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# coding=utf-8
2+
"""
3+
@project: MaxKB
4+
@Author:虎虎
5+
@file: chat_record.py
6+
@date:2025/6/23 11:16
7+
@desc:
8+
"""
9+
from typing import Dict
10+
11+
from django.db import transaction
12+
from django.db.models import QuerySet
13+
from django.utils.translation import gettext_lazy as _, gettext
14+
from rest_framework import serializers
15+
16+
from application.models import VoteChoices, ChatRecord
17+
from common.exception.app_exception import AppApiException
18+
from common.utils.lock import try_lock, un_lock
19+
20+
21+
class VoteRequest(serializers.Serializer):
22+
vote_status = serializers.ChoiceField(choices=VoteChoices.choices,
23+
label=_("Bidding Status"))
24+
25+
26+
class VoteSerializer(serializers.Serializer):
27+
chat_id = serializers.UUIDField(required=True, label=_("Conversation ID"))
28+
29+
chat_record_id = serializers.UUIDField(required=True,
30+
label=_("Conversation record id"))
31+
32+
@transaction.atomic
33+
def vote(self, instance: Dict, with_valid=True):
34+
if with_valid:
35+
self.is_valid(raise_exception=True)
36+
VoteRequest(data=instance).is_valid(raise_exception=True)
37+
if not try_lock(self.data.get('chat_record_id')):
38+
raise AppApiException(500,
39+
gettext(
40+
"Voting on the current session minutes, please do not send repeated requests"))
41+
try:
42+
chat_record_details_model = QuerySet(ChatRecord).get(id=self.data.get('chat_record_id'),
43+
chat_id=self.data.get('chat_id'))
44+
if chat_record_details_model is None:
45+
raise AppApiException(500, gettext("Non-existent conversation chat_record_id"))
46+
vote_status = instance.get("vote_status")
47+
if chat_record_details_model.vote_status == VoteChoices.UN_VOTE:
48+
if vote_status == VoteChoices.STAR:
49+
# 点赞
50+
chat_record_details_model.vote_status = VoteChoices.STAR
51+
52+
if vote_status == VoteChoices.TRAMPLE:
53+
# 点踩
54+
chat_record_details_model.vote_status = VoteChoices.TRAMPLE
55+
chat_record_details_model.save()
56+
else:
57+
if vote_status == VoteChoices.UN_VOTE:
58+
# 取消点赞
59+
chat_record_details_model.vote_status = VoteChoices.UN_VOTE
60+
chat_record_details_model.save()
61+
else:
62+
raise AppApiException(500, gettext("Already voted, please cancel first and then vote again"))
63+
finally:
64+
un_lock(self.data.get('chat_record_id'))
65+
return True

apps/chat/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@
1212
path('chat_message/<str:chat_id>', views.ChatView.as_view()),
1313
path('open', views.OpenView.as_view()),
1414
path('captcha', views.CaptchaView.as_view(), name='captcha'),
15+
path('vote/chat/<str:chat_id>/chat_record/<str:chat_record_id>', views.VoteView.as_view(), name='vote'),
1516
]

apps/chat/views/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@
88
"""
99
from .chat_embed import *
1010
from .chat import *
11+
from .chat_record import *

apps/chat/views/chat.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,9 +128,9 @@ def get(self, request: Request):
128128

129129
class CaptchaView(APIView):
130130
@extend_schema(methods=['GET'],
131-
summary=_("Get captcha"),
132-
description=_("Get captcha"),
133-
operation_id=_("Get captcha"), # type: ignore
131+
summary=_("Get Chat captcha"),
132+
description=_("Get Chat captcha"),
133+
operation_id=_("Get Chat captcha"), # type: ignore
134134
tags=[_("User Management")], # type: ignore
135135
responses=CaptchaAPI.get_response())
136136
def get(self, request: Request):

apps/chat/views/chat_record.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# coding=utf-8
2+
"""
3+
@project: MaxKB
4+
@Author:虎虎
5+
@file: chat_record.py
6+
@date:2025/6/23 10:42
7+
@desc:
8+
"""
9+
from drf_spectacular.utils import extend_schema
10+
from rest_framework.request import Request
11+
from rest_framework.views import APIView
12+
from django.utils.translation import gettext_lazy as _
13+
14+
from chat.api.vote_api import VoteAPI
15+
from chat.serializers.chat_record import VoteSerializer
16+
from common import result
17+
from common.auth import TokenAuth
18+
19+
20+
class VoteView(APIView):
21+
authentication_classes = [TokenAuth]
22+
23+
@extend_schema(
24+
methods=['PUT'],
25+
description=_("Like, Dislike"),
26+
summary=_("Like, Dislike"),
27+
operation_id=_("Like, Dislike"), # type: ignore
28+
parameters=VoteAPI.get_parameters(),
29+
request=VoteAPI.get_request(),
30+
responses=VoteAPI.get_response(),
31+
tags=[_('Chat')] # type: ignore
32+
)
33+
def put(self, request: Request, chat_id: str, chat_record_id: str):
34+
return result.success(VoteSerializer(
35+
data={'chat_id': chat_id,
36+
'chat_record_id': chat_record_id
37+
}).vote(request.data))

apps/knowledge/serializers/knowledge.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,9 +196,9 @@ def list(self):
196196
if not root:
197197
raise serializers.ValidationError(_('Folder not found'))
198198
workspace_manage = is_workspace_manage(self.data.get('user_id'), self.data.get('workspace_id'))
199-
199+
is_x_pack_ee = self.is_x_pack_ee()
200200
return native_search(
201-
self.get_query_set(),
201+
self.get_query_set(workspace_manage, is_x_pack_ee),
202202
select_string=get_file_content(
203203
os.path.join(
204204
PROJECT_DIR,

ui/src/api/chat/chat.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,29 @@ const getAuthSetting: (auth_type: string, loading?: Ref<boolean>) => Promise<Res
159159
) => {
160160
return get(`/chat_user/${auth_type}/detail`, undefined, loading)
161161
}
162+
/**
163+
* 点赞点踩
164+
* @param chat_id 对话id
165+
* @param chat_record_id 对话记录id
166+
* @param vote_status 点赞状态
167+
* @param loading 加载器
168+
* @returns
169+
*/
170+
const vote: (
171+
chat_id: string,
172+
chat_record_id: string,
173+
vote_status: string,
174+
loading?: Ref<boolean>,
175+
) => Promise<Result<boolean>> = (chat_id, chat_record_id, vote_status, loading) => {
176+
return put(
177+
`/vote/chat/${chat_id}/chat_record/${chat_record_id}`,
178+
{
179+
vote_status,
180+
},
181+
undefined,
182+
loading,
183+
)
184+
}
162185
export default {
163186
open,
164187
chat,
@@ -176,4 +199,5 @@ export default {
176199
ldapLogin,
177200
getAuthSetting,
178201
passwordAuthentication,
202+
vote,
179203
}

ui/src/components/ai-chat/component/operation-button/ChatOperationButton.vue

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ import { nextTick, onMounted, ref, onBeforeUnmount } from 'vue'
103103
import { useRoute } from 'vue-router'
104104
import { copyClick } from '@/utils/clipboard'
105105
import applicationApi from '@/api/application/application'
106+
import chatAPI from '@/api/chat/chat'
106107
import { datetimeFormat } from '@/utils/time'
107108
import { MsgError } from '@/utils/message'
108109
import bus from '@/bus'
@@ -118,7 +119,7 @@ const copy = (data: any) => {
118119
}
119120
const route = useRoute()
120121
const {
121-
params: { id }
122+
params: { id },
122123
} = route as any
123124
124125
const props = withDefaults(
@@ -134,8 +135,8 @@ const props = withDefaults(
134135
}>(),
135136
{
136137
data: () => ({}),
137-
type: 'ai-chat'
138-
}
138+
type: 'ai-chat',
139+
},
139140
)
140141
141142
const emit = defineEmits(['update:data', 'regeneration'])
@@ -152,12 +153,10 @@ function regeneration() {
152153
}
153154
154155
function voteHandle(val: string) {
155-
applicationApi
156-
.putChatVote(props.applicationId, props.chatId, props.data.record_id, val, loading)
157-
.then(() => {
158-
buttonData.value['vote_status'] = val
159-
emit('update:data', buttonData.value)
160-
})
156+
chatAPI.vote(props.chatId, props.data.record_id, val, loading).then(() => {
157+
buttonData.value['vote_status'] = val
158+
emit('update:data', buttonData.value)
159+
})
161160
}
162161
163162
function markdownToPlainText(md: string) {
@@ -203,9 +202,9 @@ function smartSplit(
203202
0: 10,
204203
1: 25,
205204
3: 50,
206-
5: 100
205+
5: 100,
207206
},
208-
is_end = false
207+
is_end = false,
209208
) {
210209
// 匹配中文逗号/句号,且后面至少还有20个字符(含任何字符,包括换行)
211210
const regex = /([。?\n])|(<audio[^>]*><\/audio>)/g
@@ -261,7 +260,7 @@ enum AudioStatus {
261260
/**
262261
* 错误
263262
*/
264-
ERROR = 'ERROR'
263+
ERROR = 'ERROR',
265264
}
266265
class AudioManage {
267266
textList: Array<string>
@@ -318,7 +317,7 @@ class AudioManage {
318317
.postTextToSpeech(
319318
(props.applicationId as string) || (id as string),
320319
{ text: text },
321-
loading
320+
loading,
322321
)
323322
.then(async (res: any) => {
324323
if (res.type === 'application/json') {
@@ -347,7 +346,7 @@ class AudioManage {
347346
this.audioList.push(audioElement)
348347
} else {
349348
const speechSynthesisUtterance: SpeechSynthesisUtterance = new SpeechSynthesisUtterance(
350-
text
349+
text,
351350
)
352351
speechSynthesisUtterance.onend = () => {
353352
this.statusList[index] = AudioStatus.END
@@ -381,7 +380,7 @@ class AudioManage {
381380
.postTextToSpeech(
382381
(props.applicationId as string) || (id as string),
383382
{ text: text },
384-
loading
383+
loading,
385384
)
386385
.then(async (res: any) => {
387386
if (res.type === 'application/json') {
@@ -432,7 +431,7 @@ class AudioManage {
432431
433432
// 需要播放的内容
434433
const index = this.statusList.findIndex((status) =>
435-
[AudioStatus.MOUNTED, AudioStatus.READY].includes(status)
434+
[AudioStatus.MOUNTED, AudioStatus.READY].includes(status),
436435
)
437436
if (index < 0 || this.statusList[index] === AudioStatus.MOUNTED) {
438437
return
@@ -502,9 +501,9 @@ class AudioManage {
502501
{
503502
0: 20,
504503
1: 50,
505-
5: 100
504+
5: 100,
506505
},
507-
is_end
506+
is_end,
508507
)
509508
510509
return split

0 commit comments

Comments
 (0)