Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions ui/src/components/chat-plus/api/chat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { get, postStream, del, put, post } from '@/request/chat/index'

const prefix = (window.MaxKB?.prefix || '/chat') + '/api'

export const open = () =>
get('/open', {})

export const chat = (chatId: string, data: any) =>
postStream(`${prefix}/chat_message/${chatId}`, data)

export const history = (page: number, size: number) =>
get(`/historical_conversation/${page}/${size}`)

export const records = (chatId: string, page: number, size: number) =>
get(`/historical_conversation_record/${chatId}/${page}/${size}`)

export const recordDetail = (chatId: string, recordId: string) =>
get(`/historical_conversation/${chatId}/record/${recordId}`)

export const vote = (chatId: string, recordId: string, voteStatus: string, reason?: string) =>
put(`/vote/chat/${chatId}/chat_record/${recordId}`, {
vote_status: voteStatus,
...(reason !== undefined && { vote_reason: reason }),
})

export const deleteChat = (chatId: string) =>
del(`/historical_conversation/${chatId}`)

export const clearChat = () =>
del('/historical_conversation/clear')

export const modifyChat = (chatId: string, data: any) =>
put(`/historical_conversation/${chatId}`, data)

export const uploadFile = (file: File, sourceId: string, sourceType: string) => {
const fd = new FormData()
fd.append('file', file)
fd.append('source_id', sourceId)
fd.append('source_type', sourceType)
return post('/oss/file', fd)
}
44 changes: 44 additions & 0 deletions ui/src/components/chat-plus/api/debug.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { get, postStream, del, put, post } from '@/request/index'
import useStore from '@/stores'

const prefix = (window.MaxKB?.prefix || '/admin') + '/api'

export const open = (applicationId: string) => {
const { user } = useStore()
return get(`/workspace/${user.getWorkspaceId()}/application/${applicationId}/open`, {})
}

export const chat = (chatId: string, data: any) =>
postStream(`${prefix}/chat_message/${chatId}`, data)

export const history = (page: number, size: number) =>
get(`/historical_conversation/${page}/${size}`)

export const records = (chatId: string, page: number, size: number) =>
get(`/historical_conversation_record/${chatId}/${page}/${size}`)

export const recordDetail = (chatId: string, recordId: string) =>
get(`/historical_conversation/${chatId}/record/${recordId}`)

export const vote = (chatId: string, recordId: string, voteStatus: string, reason?: string) =>
put(`/vote/chat/${chatId}/chat_record/${recordId}`, {
vote_status: voteStatus,
...(reason !== undefined && { vote_reason: reason }),
})

export const deleteChat = (chatId: string) =>
del(`/historical_conversation/${chatId}`)

export const clearChat = () =>
del('/historical_conversation/clear')

export const modifyChat = (chatId: string, data: any) =>
put(`/historical_conversation/${chatId}`, data)

export const uploadFile = (file: File, sourceId: string, sourceType: string) => {
const fd = new FormData()
fd.append('file', file)
fd.append('source_id', sourceId)
fd.append('source_type', sourceType)
return post('/oss/file', fd)
}
8 changes: 8 additions & 0 deletions ui/src/components/chat-plus/api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import * as chat from './chat'
import * as debug from './debug'

export type ChatType = 'CHAT' | 'DEBUG'

const apis = { CHAT: chat, DEBUG: debug } as const

export const useApi = (type: ChatType) => apis[type]
37 changes: 37 additions & 0 deletions ui/src/components/chat-plus/bus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
type Handler = (...args: any[]) => void

class ChatBus {
private events = new Map<string, Set<Handler>>()

on(event: string, handler: Handler) {
if (!this.events.has(event)) {
this.events.set(event, new Set())
}
this.events.get(event)!.add(handler)
return () => this.off(event, handler)
}

off(event: string, handler: Handler) {
this.events.get(event)?.delete(handler)
}

emit(event: string, ...args: any[]) {
this.events.get(event)?.forEach((handler) => handler(...args))
}

clear() {
this.events.clear()
}
}

export const chatBus = new ChatBus()

export const ChatEvents = {
OPEN_CONVERSATION: 'open:conversation',
NEW_CONVERSATION: 'new:conversation',
DELETE_CONVERSATION: 'delete:conversation',
RENAME_CONVERSATION: 'rename:conversation',
SEND_MESSAGE: 'send:message',
STOP_GENERATING: 'stop:generating',
REFRESH_LIST: 'refresh:list',
} as const
143 changes: 143 additions & 0 deletions ui/src/components/chat-plus/component/answer-content/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
<template>
<div class="item-content lighter">
<div v-for="(answer_text, index) in answer_text_list" :key="index" class="mb-8 flex">
<div class="avatar mr-8" v-if="showAvatar">
<img v-if="application.avatar" :src="application.avatar" height="28px" width="28px" />
<LogoIcon v-else height="28px" width="28px" />
</div>
<div
class="content w-full"
@mouseup="openControl"
:style="{ 'padding-right': showUserAvatar ? 'var(--padding-left)' : '0' }"
>
<template v-if="answer_text.length > 0">
<ContentItem
v-for="(answer, aIndex) in answer_text"
:key="aIndex"
:content="answer"
:send-message="chatMessage"
/>
</template>
<ContentItem
v-else-if="
(chatRecord.write_ed === undefined || chatRecord.write_ed === true) &&
answer_text_list.flat().map((item) => item.content).join('').trim().length === 0
"
:content="{ content: $t('aiChat.tip.answerMessage') }"
/>
<p v-else-if="chatRecord.is_stop" style="margin: 0.5rem 0">
{{ $t('aiChat.tip.stopAnswer') }}
</p>
<p v-else style="margin: 0.5rem 0">
{{ $t('aiChat.tip.answerLoading') }} <span class="dotting"></span>
</p>
<KnowledgeSourceComponent
:data="chatRecord"
:application="application"
:type="type"
:appType="application.type"
:executionIsRightPanel="props.executionIsRightPanel"
@open-execution-detail="emit('openExecutionDetail')"
@openParagraph="emit('openParagraph')"
@openParagraphDocument="(val: string) => emit('openParagraphDocument', val)"
v-if="showSource(chatRecord) && index === chatRecord.answer_text_list.length - 1"
/>
</div>
</div>

<div
class="content"
:style="{
'padding-left': showAvatar ? 'var(--padding-left)' : '0',
'padding-right': showUserAvatar ? 'var(--padding-left)' : '0',
}"
v-if="!selection"
>
<OperationButton
:type="type"
:application="application"
:chatRecord="chatRecord"
@update:chatRecord="(event: any) => emit('update:chatRecord', event)"
:loading="loading"
:start-chat="startChat"
:stop-chat="stopChat"
:regenerationChart="regenerationChart"
/>
</div>
</div>
</template>

<script setup lang="ts">
import { computed } from 'vue'
import KnowledgeSourceComponent from '@/components/ai-chat/component/knowledge-source-component/index.vue'
import OperationButton from '@/components/ai-chat/component/operation-button/index.vue'
import ContentItem from './items/index.vue'
import { type chatType } from '@/api/type/application'
import bus from '@/bus'

const props = defineProps<{
chatRecord: chatType
application: any
loading: boolean
sendMessage: (question: string, other_params_data?: any, chat?: chatType) => Promise<boolean>
chatManagement: any
type: 'log' | 'ai-chat' | 'debug-ai-chat' | 'share'
executionIsRightPanel?: boolean
selection?: boolean
}>()

const emit = defineEmits([
'update:chatRecord',
'openExecutionDetail',
'openParagraph',
'openParagraphDocument',
])

const showAvatar = computed(() => props.application.show_avatar == undefined ? true : props.application.show_avatar)
const showUserAvatar = computed(() => props.application.show_user_avatar == undefined ? true : props.application.show_user_avatar)

const chatMessage = (question: string, type: 'old' | 'new', other_params_data?: any) => {
if (type === 'old') {
props.chatRecord.answer_text_list.push([])
props.sendMessage(question, other_params_data, props.chatRecord).then(() => {
props.chatManagement.open(props.chatRecord.id)
props.chatManagement.write(props.chatRecord.id)
})
} else {
props.sendMessage(question, other_params_data)
}
}

const openControl = (event: any) => {
if (props.type !== 'log') bus.emit('open-control', event)
}

const answer_text_list = computed(() => {
return props.chatRecord.answer_text_list.map((item: any) => {
if (typeof item == 'string') return [{ content: item }]
if (item instanceof Array) return item
return [item]
})
})

function showSource(row: any) {
if (props.type === 'log') return true
if (row.write_ed && 500 !== row.status) return true
return false
}

const regenerationChart = (chat: chatType) => {
const container = props.chatRecord?.upload_meta || props.chatRecord.execution_details?.find((d: any) => d.type === 'start-node')
props.sendMessage(chat.problem_text, {
re_chat: true,
image_list: container?.image_list || [],
document_list: container?.document_list || [],
audio_list: container?.audio_list || [],
video_list: container?.video_list || [],
other_list: container?.other_list || [],
})
}

const stopChat = (chat: chatType) => { props.chatManagement.stop(chat.id) }
const startChat = (chat: chatType) => { props.chatManagement.write(chat.id) }
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<template>
<div class="content-failure mb-8">
<span class="failure-icon">⚠️</span>
<span>{{ content.content }}</span>
</div>
</template>

<script setup lang="ts">
defineProps<{ content: any }>()
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<template>
<div class="form-block mb-8">
<DynamicsForm
:disabled="is_submit"
label-position="top"
require-asterisk-position="right"
ref="dynamicsFormRef"
:render_data="form_field_list"
label-suffix=":"
v-model="form_data"
:model="form_data"
/>
<el-button :type="is_submit ? 'info' : 'primary'" :disabled="is_submit" @click="submit">
{{ is_submit ? '已提交' : '提交' }}
</el-button>
</div>
</template>

<script setup lang="ts">
import { computed, ref, inject } from 'vue'
import DynamicsForm from '@/components/dynamics-form/index.vue'

const chat = inject<any>('chat')

const props = defineProps<{
content: any
}>()

const _submit = ref(false)
const form_field_list = computed(() => props.content.form_field_list || [])
const is_submit = computed(() => _submit.value || !!props.content.is_submit)
const _form_data = ref<any>({})
const form_data = computed({
get: () => (props.content.is_submit ? props.content.form_data : _form_data.value),
set: (v) => {
_form_data.value = v
},
})
const dynamicsFormRef = ref()

const submit = () => {
dynamicsFormRef.value?.validate().then(() => {
_submit.value = true
if (chat?.sendMessage) {
chat.sendMessage('', 'old', {
position: props.content.position,
runtime_node_id: props.content.id,
chat_record_id: props.content.chat_record_id,
node_data: form_data.value,
})
}
})
}
</script>
<style lang="scss" scoped>
.form-block {
background: #ffffff;
border: 1px solid var(--el-border-color-lighter, #e4e7ed);
border-radius: 8px;
padding: 16px;
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<template>
<component v-if="typeRenderers[content.type]" :is="typeRenderers[content.type]" :content="content" v-bind="$attrs" />
<TextItem v-else :content="content" v-bind="$attrs" />
</template>

<script setup lang="ts">
import TextItem from './text.vue'
import ReasoningItem from './reasoning.vue'
import ToolItem from './tool.vue'
import FormItem from './form.vue'
import FailureItem from './failure.vue'

const typeRenderers: Record<string, any> = {
TEXT: TextItem,
REASONING: ReasoningItem,
TOOL: ToolItem,
FORM: FormItem,
FAILURE: FailureItem,
}

defineProps<{ content: any }>()
</script>
Loading
Loading