feat: add chat log step detail

This commit is contained in:
ulleo
2026-02-12 17:51:02 +08:00
committed by ulleo
parent 40925e9649
commit 2f5736686f
13 changed files with 712 additions and 17 deletions
+10 -3
View File
@@ -300,8 +300,10 @@ export class ChatLogHistoryItem {
duration?: number | undefined
total_tokens?: number | undefined
operate?: string | undefined
operate_key?: string | undefined
local_operation?: boolean | undefined
error?: boolean | undefined
message?: any
constructor()
constructor(
@@ -311,7 +313,8 @@ export class ChatLogHistoryItem {
total_tokens: number | undefined,
operate: string | undefined,
local_operation: boolean | undefined,
error: boolean | undefined
error: boolean | undefined,
message: any | undefined
)
constructor(
start_time?: Date | string,
@@ -320,15 +323,18 @@ export class ChatLogHistoryItem {
total_tokens?: number | undefined,
operate?: string | undefined,
local_operation?: boolean | undefined,
error?: boolean | undefined
error?: boolean | undefined,
message?: any | undefined
) {
this.start_time = getDate(start_time)
this.finish_time = getDate(finish_time)
this.duration = duration
this.total_tokens = total_tokens
this.operate_key = operate
this.operate = t('chat.log.' + operate)
this.local_operation = !!local_operation
this.error = !!error
this.message = message
}
}
@@ -373,7 +379,8 @@ const toChatLogHistoryItem = (data?: any): any | undefined => {
data.total_tokens,
data.operate,
data.local_operation,
data.error
data.error,
data.message
)
}
+10 -1
View File
@@ -767,7 +767,16 @@
"FILTER_CUSTOM_PROMPT": "Match Custom Prompts",
"EXECUTE_SQL": "Execute SQL",
"GENERATE_PICTURE": "Generate Picture"
}
},
"log_system": "Conversation Template",
"log_question": "Current Question",
"log_answer": "AI Response",
"log_history": "History",
"find_term_title": "Matched {0} terms",
"find_sql_sample_title": "Matched {0} SQL examples",
"find_custom_prompt_title": "Matched {0} custom prompts",
"query_count_title": "Found {0} data entries",
"generate_picture_success": "Image generated"
},
"about": {
"title": "About",
+10 -1
View File
@@ -767,7 +767,16 @@
"FILTER_CUSTOM_PROMPT": "사용자 정의 프롬프트 매칭",
"EXECUTE_SQL": "SQL 실행",
"GENERATE_PICTURE": "이미지 생성"
}
},
"log_system": "대화 템플릿",
"log_question": "현재 질문",
"log_answer": "AI 응답",
"log_history": "기록",
"find_term_title": "{0}개의 용어 매칭됨",
"find_sql_sample_title": "{0}개의 SQL 예시 매칭됨",
"find_custom_prompt_title": "{0}개의 사용자 정의 프롬프트 매칭됨",
"query_count_title": "{0}개의 데이터 항목 발견됨",
"generate_picture_success": "이미지가 생성되었습니다"
},
"about": {
"title": "정보",
+10 -1
View File
@@ -767,7 +767,16 @@
"FILTER_CUSTOM_PROMPT": "匹配自定义提示词",
"EXECUTE_SQL": "执行 SQL",
"GENERATE_PICTURE": "生成图片"
}
},
"log_system": "对话模板",
"log_question": "本次提问",
"log_answer": "AI 回答",
"log_history": "历史记录",
"find_term_title":"匹配到 {0} 个术语",
"find_sql_sample_title":"匹配到 {0} 个SQL示例",
"find_custom_prompt_title":"匹配到 {0} 个自定义提示词",
"query_count_title":"查询到 {0} 条数据",
"generate_picture_success":"已生成图片"
},
"about": {
"title": "关于",
+21 -11
View File
@@ -8,6 +8,13 @@ import icon_alarm_clock_colorful from '@/assets/svg/icon_alarm-clock_colorful.sv
import { chatApi, type ChatLogHistory } from '@/api/chat.ts'
import { useI18n } from 'vue-i18n'
import { debounce } from 'lodash-es'
import LogTerm from './execution-component/LogTerm.vue'
import LogSQLSample from './execution-component/LogSQLSample.vue'
import LogCustomPrompt from './execution-component/LogCustomPrompt.vue'
import LogDataQuery from './execution-component/LogDataQuery.vue'
import LogChooseTable from './execution-component/LogChooseTable.vue'
import LogGeneratePicture from './execution-component/LogGeneratePicture.vue'
import LogWithAi from '@/views/chat/execution-component/LogWithAi.vue'
const { t } = useI18n()
const logHistory = ref<ChatLogHistory>({})
@@ -76,13 +83,8 @@ defineExpose({
<div class="title">{{ t('parameter.execution_details') }}</div>
<div class="list">
<div
v-for="(ele, index) in logHistory.steps"
:key="ele.duration"
class="list-item"
@click="handleExpand(index)"
>
<div class="header">
<div v-for="(ele, index) in logHistory.steps" :key="ele.duration" class="list-item">
<div class="header" @click="handleExpand(index)">
<div class="name">
<el-icon class="shrink" :class="expandIds.includes(index) && 'expand'" size="10">
<icon_expand_right_filled></icon_expand_right_filled>
@@ -104,7 +106,15 @@ defineExpose({
</el-icon>
</div>
</div>
<div class="content"></div>
<div v-if="expandIds.includes(index)" class="content">
<LogTerm v-if="ele.operate_key === 'FILTER_TERMS'" :item="ele" />
<LogSQLSample v-else-if="ele.operate_key === 'FILTER_SQL_EXAMPLE'" :item="ele" />
<LogCustomPrompt v-else-if="ele.operate_key === 'FILTER_CUSTOM_PROMPT'" :item="ele" />
<LogChooseTable v-else-if="ele.operate_key === 'CHOOSE_TABLE'" :item="ele" />
<LogDataQuery v-else-if="ele.operate_key === 'EXECUTE_SQL'" :item="ele" />
<LogGeneratePicture v-else-if="ele.operate_key === 'GENERATE_PICTURE'" :item="ele" />
<LogWithAi v-else :item="ele" />
</div>
</div>
</div>
</el-drawer>
@@ -175,13 +185,13 @@ defineExpose({
}
100% {
height: 504px;
min-height: 54px;
}
}
&:has(.expand) {
height: 504px;
animation: expand 0.5s;
min-height: 54px;
//animation: expand 0.5s;
}
.shrink {
@@ -0,0 +1,14 @@
<script setup lang="ts"></script>
<template>
<div class="base-container"><slot></slot></div>
</template>
<style scoped lang="less">
.base-container {
background: #f5f6f7;
border-radius: 12px;
padding: 12px;
margin-top: 16px;
}
</style>
@@ -0,0 +1,73 @@
<script setup lang="ts">
import BaseContent from './BaseContent.vue'
import { type ChatLogHistoryItem } from '@/api/chat.ts'
import { computed } from 'vue'
const props = withDefaults(
defineProps<{
item?: ChatLogHistoryItem
error?: string
}>(),
{
item: undefined,
error: '',
}
)
const schema = computed(() => {
return (props.item?.message as string) ?? ''
})
</script>
<template>
<BaseContent class="base-container">
<template v-if="item.error">
{{ error }}
</template>
<template v-else>
<div v-dompurify-html="schema" class="inner-title"></div>
</template>
</BaseContent>
</template>
<style scoped lang="less">
.inner-title {
color: #646a73;
font-size: 12px;
line-height: 20px;
font-weight: 500;
vertical-align: middle;
white-space: pre-wrap;
}
.item-list {
display: flex;
flex-direction: column;
gap: 8px;
align-items: stretch;
flex-wrap: nowrap;
.inner-item {
border: 1px solid #dee0e3;
display: flex;
flex-direction: column;
gap: 8px;
border-radius: 12px;
padding: 16px;
background: #ffffff;
.inner-item-title {
color: #1f2329;
font-weight: 500;
line-height: 22px;
font-size: 14px;
vertical-align: middle;
}
.inner-item-description {
color: #646a73;
font-weight: 400;
line-height: 22px;
font-size: 14px;
vertical-align: middle;
}
}
}
</style>
@@ -0,0 +1,85 @@
<script setup lang="ts">
import BaseContent from './BaseContent.vue'
import { type ChatLogHistoryItem } from '@/api/chat.ts'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
const props = withDefaults(
defineProps<{
item?: ChatLogHistoryItem
error?: string
}>(),
{
item: undefined,
error: '',
}
)
const { t } = useI18n()
const list = computed(() => {
return (props.item?.message as Array<any>) ?? []
})
const title = computed(() => {
return t('chat.find_custom_prompt_title', [list.value.length])
})
</script>
<template>
<BaseContent class="base-container">
<template v-if="item.error">
{{ error }}
</template>
<template v-else>
<div class="inner-title">{{ title }}</div>
<div v-if="list.length > 0" style="margin-top: 8px" class="item-list">
<div v-for="(ele, index) in list" :key="index" class="inner-item">
<div v-dompurify-html="ele" class="inner-item-description" />
</div>
</div>
</template>
</BaseContent>
</template>
<style scoped lang="less">
.inner-title {
color: #646a73;
font-size: 12px;
line-height: 20px;
font-weight: 500;
vertical-align: middle;
}
.item-list {
display: flex;
flex-direction: column;
gap: 8px;
align-items: stretch;
flex-wrap: nowrap;
.inner-item {
border: 1px solid #dee0e3;
display: flex;
flex-direction: column;
gap: 8px;
border-radius: 12px;
padding: 16px;
background: #ffffff;
.inner-item-title {
color: #1f2329;
font-weight: 500;
line-height: 22px;
font-size: 14px;
vertical-align: middle;
}
.inner-item-description {
//color: #646a73;
color: #1f2329;
font-weight: 400;
line-height: 22px;
font-size: 14px;
vertical-align: middle;
white-space: pre-wrap;
}
}
}
</style>
@@ -0,0 +1,78 @@
<script setup lang="ts">
import BaseContent from './BaseContent.vue'
import { type ChatLogHistoryItem } from '@/api/chat.ts'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
const props = withDefaults(
defineProps<{
item?: ChatLogHistoryItem
error?: string
}>(),
{
item: undefined,
error: '',
}
)
const { t } = useI18n()
const message = computed(() => {
return props.item?.message ?? { count: 0 }
})
const title = computed(() => {
return t('chat.query_count_title', [message.value.count])
})
</script>
<template>
<BaseContent class="base-container">
<template v-if="item.error">
{{ error }}
</template>
<template v-else>
<div class="inner-title">{{ title }}</div>
</template>
</BaseContent>
</template>
<style scoped lang="less">
.inner-title {
color: #646a73;
font-size: 12px;
line-height: 20px;
font-weight: 500;
vertical-align: middle;
}
.item-list {
display: flex;
flex-direction: column;
gap: 8px;
align-items: stretch;
flex-wrap: nowrap;
.inner-item {
border: 1px solid #dee0e3;
display: flex;
flex-direction: column;
gap: 8px;
border-radius: 12px;
padding: 16px;
background: #ffffff;
.inner-item-title {
color: #1f2329;
font-weight: 500;
line-height: 22px;
font-size: 14px;
vertical-align: middle;
}
.inner-item-description {
color: #646a73;
font-weight: 400;
line-height: 22px;
font-size: 14px;
vertical-align: middle;
}
}
}
</style>
@@ -0,0 +1,79 @@
<script setup lang="ts">
import BaseContent from './BaseContent.vue'
import { type ChatLogHistoryItem } from '@/api/chat.ts'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
const props = withDefaults(
defineProps<{
item?: ChatLogHistoryItem
error?: string
}>(),
{
item: undefined,
error: '',
}
)
const { t } = useI18n()
const message = computed(() => {
return props.item?.message ?? ''
})
const title = computed(() => {
return t('chat.generate_picture_success')
})
</script>
<template>
<BaseContent class="base-container">
<template v-if="item.error">
{{ error }}
</template>
<template v-else>
<div class="inner-title">{{ title }}</div>
<div v-if="message.length > 0" class="inner-title">{{ message }}</div>
</template>
</BaseContent>
</template>
<style scoped lang="less">
.inner-title {
color: #646a73;
font-size: 12px;
line-height: 20px;
font-weight: 500;
vertical-align: middle;
}
.item-list {
display: flex;
flex-direction: column;
gap: 8px;
align-items: stretch;
flex-wrap: nowrap;
.inner-item {
border: 1px solid #dee0e3;
display: flex;
flex-direction: column;
gap: 8px;
border-radius: 12px;
padding: 16px;
background: #ffffff;
.inner-item-title {
color: #1f2329;
font-weight: 500;
line-height: 22px;
font-size: 14px;
vertical-align: middle;
}
.inner-item-description {
color: #646a73;
font-weight: 400;
line-height: 22px;
font-size: 14px;
vertical-align: middle;
}
}
}
</style>
@@ -0,0 +1,93 @@
<script setup lang="ts">
import BaseContent from './BaseContent.vue'
import { type ChatLogHistoryItem } from '@/api/chat.ts'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import SQLComponent from '@/views/chat/component/SQLComponent.vue'
const props = withDefaults(
defineProps<{
item?: ChatLogHistoryItem
error?: string
}>(),
{
item: undefined,
error: '',
}
)
const { t } = useI18n()
const list = computed(() => {
return (props.item?.message as Array<any>) ?? []
})
const title = computed(() => {
return t('chat.find_sql_sample_title', [list.value.length])
})
</script>
<template>
<BaseContent class="base-container">
<template v-if="item.error">
{{ error }}
</template>
<template v-else>
<div class="inner-title">{{ title }}</div>
<div v-if="list.length > 0" style="margin-top: 8px" class="item-list">
<div v-for="(ele, index) in list" :key="index" class="inner-item">
<div class="inner-item-title">
{{ ele.question }}
</div>
<div class="inner-item-description">
<SQLComponent :sql="ele['suggestion-answer']" />
</div>
</div>
</div>
</template>
</BaseContent>
</template>
<style scoped lang="less">
.inner-title {
color: #646a73;
font-size: 12px;
line-height: 20px;
font-weight: 500;
vertical-align: middle;
}
.item-list {
display: flex;
flex-direction: column;
gap: 8px;
align-items: stretch;
flex-wrap: nowrap;
.inner-item {
border: 1px solid #dee0e3;
display: flex;
flex-direction: column;
gap: 8px;
border-radius: 12px;
padding: 16px;
background: #ffffff;
.inner-item-title {
color: #1f2329;
font-weight: 500;
line-height: 22px;
font-size: 14px;
vertical-align: middle;
}
.inner-item-description {
color: #646a73;
font-weight: 400;
line-height: 22px;
font-size: 14px;
vertical-align: middle;
.hljs {
padding: 0 1rem;
}
}
}
}
</style>
@@ -0,0 +1,89 @@
<script setup lang="ts">
import BaseContent from './BaseContent.vue'
import { type ChatLogHistoryItem } from '@/api/chat.ts'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
const props = withDefaults(
defineProps<{
item?: ChatLogHistoryItem
error?: string
}>(),
{
item: undefined,
error: '',
}
)
const { t } = useI18n()
const list = computed(() => {
return (props.item?.message as Array<any>) ?? []
})
const title = computed(() => {
return t('chat.find_term_title', [list.value.length])
})
</script>
<template>
<BaseContent class="base-container">
<template v-if="item.error">
{{ error }}
</template>
<template v-else>
<div class="inner-title">{{ title }}</div>
<div v-if="list.length > 0" style="margin-top: 8px" class="item-list">
<div v-for="(ele, index) in list" :key="index" class="inner-item">
<div>
<div v-for="(word, index2) in ele.words" :key="index2" class="inner-item-title">
{{ word }}
</div>
</div>
<div v-dompurify-html="ele.description" class="inner-item-description" />
</div>
</div>
</template>
</BaseContent>
</template>
<style scoped lang="less">
.inner-title {
color: #646a73;
font-size: 12px;
line-height: 20px;
font-weight: 500;
vertical-align: middle;
}
.item-list {
display: flex;
flex-direction: column;
gap: 8px;
align-items: stretch;
flex-wrap: nowrap;
.inner-item {
border: 1px solid #dee0e3;
display: flex;
flex-direction: column;
gap: 8px;
border-radius: 12px;
padding: 16px;
background: #ffffff;
.inner-item-title {
color: #1f2329;
font-weight: 500;
line-height: 22px;
font-size: 14px;
vertical-align: middle;
}
.inner-item-description {
color: #646a73;
font-weight: 400;
line-height: 22px;
font-size: 14px;
vertical-align: middle;
white-space: pre-wrap;
}
}
}
</style>
@@ -0,0 +1,140 @@
<script setup lang="ts">
import BaseContent from './BaseContent.vue'
import { type ChatLogHistoryItem } from '@/api/chat.ts'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import SQLComponent from '@/views/chat/component/SQLComponent.vue'
import { find, findLastIndex } from 'lodash-es'
const props = withDefaults(
defineProps<{
item?: ChatLogHistoryItem
error?: string
}>(),
{
item: undefined,
error: '',
}
)
const { t } = useI18n()
const data = computed(() => {
return (props.item?.message as Array<any>) ?? []
})
// 1. 获取 role 为 system 的一条记录
const systemRecord = computed(() => find(data.value, { type: 'system' }))
// 2. 获取最后一个 role 为 human 的记录
const lastHumanIndex = computed(() => findLastIndex(data.value, { type: 'human' }))
const lastHumanRecord = computed(() => data.value[lastHumanIndex.value])
// 3. 获取最后一个 role 为 ai 且在当前对话提问后出现的记录
// 使用原生 filter 和 pop
const aiRecordsAfterHuman = computed(() =>
data.value.slice(lastHumanIndex.value + 1).filter((item) => item.type === 'ai')
)
const lastAiAfterHuman = computed(() =>
aiRecordsAfterHuman.value.length > 0
? aiRecordsAfterHuman.value[aiRecordsAfterHuman.value.length - 1]
: undefined
)
// 4. 获取除 system 以外,当前对话提问前的所有记录
const recordsBeforeCurrentQuestion = computed(() =>
data.value.slice(0, lastHumanIndex.value).filter((item) => item.type !== 'system')
)
</script>
<template>
<BaseContent class="base-container">
<template v-if="item.error">
{{ error }}
</template>
<template v-else>
<div class="item-list">
<div class="inner-title">{{ t('chat.log_system') }}</div>
<div class="inner-item">
<div class="inner-item-title">
{{ systemRecord.type }}
</div>
<div class="inner-item-description">
<SQLComponent :sql="systemRecord.content" />
</div>
</div>
<template v-if="recordsBeforeCurrentQuestion.length > 0">
<div class="inner-title">{{ t('chat.log_history') }}</div>
<div class="inner-item">
<div v-for="(ele, index) in recordsBeforeCurrentQuestion" :key="index">
<div class="inner-item-title">
{{ ele.type }}
</div>
<div class="inner-item-description">
<SQLComponent :sql="ele.content" />
</div>
</div>
</div>
</template>
<div class="inner-title">{{ t('chat.log_question') }}</div>
<div class="inner-item">
<div class="inner-item-description">
<SQLComponent :sql="lastHumanRecord.content" />
</div>
</div>
<template v-if="lastAiAfterHuman">
<div class="inner-title">{{ t('chat.log_answer') }}</div>
<div class="inner-item">
<div class="inner-item-description">
<SQLComponent :sql="lastAiAfterHuman.content" />
</div>
</div>
</template>
</div>
</template>
</BaseContent>
</template>
<style scoped lang="less">
.inner-title {
color: #646a73;
font-size: 12px;
line-height: 20px;
font-weight: 500;
vertical-align: middle;
}
.item-list {
display: flex;
flex-direction: column;
gap: 8px;
align-items: stretch;
flex-wrap: nowrap;
.inner-item {
border: 1px solid #dee0e3;
display: flex;
flex-direction: column;
gap: 8px;
border-radius: 12px;
padding: 16px;
background: #ffffff;
.inner-item-title {
color: #1f2329;
font-weight: 500;
line-height: 22px;
font-size: 14px;
vertical-align: middle;
}
.inner-item-description {
color: #646a73;
font-weight: 400;
line-height: 22px;
font-size: 14px;
vertical-align: middle;
.hljs {
padding: 0 1rem;
}
}
}
}
</style>