add conversation

This commit is contained in:
Felix
2026-01-21 13:29:15 +08:00
parent 751b2ae087
commit 9d8f6d73ef
8 changed files with 1261 additions and 45 deletions

View File

@@ -10,6 +10,13 @@
"t-skeleton": "tdesign-miniprogram/skeleton/skeleton",
"word-dictionary": "../../components/word-dictionary/word-dictionary",
"vx-confetti": "/components/vx-confetti/vx-confetti",
"cloud-image": "../../components/cloud-image/cloud-image"
"cloud-image": "../../components/cloud-image/cloud-image",
"t-tag": "tdesign-miniprogram/tag/tag",
"t-check-tag": "tdesign-miniprogram/check-tag/check-tag",
"t-drawer": "tdesign-miniprogram/drawer/drawer",
"t-chat": "tdesign-miniprogram/chat-list/chat-list",
"t-chat-sender": "tdesign-miniprogram/chat-sender/chat-sender",
"t-chat-loading": "tdesign-miniprogram/chat-loading/chat-loading",
"t-chat-message": "tdesign-miniprogram/chat-message/chat-message"
}
}

View File

@@ -1,6 +1,6 @@
import apiManager from '../../utils/api'
import logger from '../../utils/logger'
import { IQaExerciseItem, IQaExerciseSession, IAppOption } from '../../types/app'
import { IQaExerciseItem, IQaExerciseSession, IAppOption, IQaConversationSettingPayload } from '../../types/app'
export const QUESTION_MODES = {
CLOZE: 'cloze',
@@ -96,7 +96,36 @@ interface IData {
variationSubmitted?: boolean
variationResultStatus?: 'correct' | 'incorrect'
variationExerciseId?: string
conversationSetting?: any
conversationSetting?: IQaConversationSettingPayload
conversationDifficulty?: string
selectedRole?: { roleIndex: number, roleSide: 1 | 2 } | null
conversationSelectedScenes?: string[]
conversationExtraNote?: string
conversationSceneLang?: 'zh' | 'en'
conversationSelectedScenesMap?: Record<string, boolean>
conversationSelectedEvents?: string[]
conversationSelectedEventsMap?: Record<string, boolean>
conversationCustomSceneText?: string
conversationCustomSceneKey?: string
conversationCustomSceneEditing?: boolean
conversationCustomScenes?: Array<{ key: string; text: string }>
conversationCustomSceneOverLimit?: boolean
conversationCustomEventText?: string
conversationCustomEventEditing?: boolean
conversationCustomEvents?: Array<{ key: string; text: string }>
conversationCustomEventOverLimit?: boolean
conversationSuggestedRoles?: Array<{ key: string; role1_en: string; role1_zh: string; role2_en: string; role2_zh: string }>
difficultyOptions: Array<{ value: string; label_zh: string; label_en: string }>
conversationViewMode: 'setup' | 'chat'
conversationLatestSession: any
conversationDetail: any
conversationMessages: any[]
replyLoading: boolean
chatInputValue: string
isChatInputVisible: boolean
scrollIntoView: string
renderPresets: Array<{ name: string; type: string }>
sidebar?: any[]
}
interface IPageInstance {
@@ -110,6 +139,7 @@ interface IPageInstance {
startPolling: (taskId: string, imageId: string) => void
startVariationPolling: (taskId: string, imageId: string) => void
startConversationInitPolling: (taskId: string, imageId: string) => void
startConversationPolling: (taskId: string, sessionId: string, showLoadingMask?: boolean, append?: boolean) => void
initExerciseContent: (exercise: any, session?: IQaExerciseSession) => void
updateActionButtonsState: () => void
shuffleArray: <T>(arr: T[]) => T[]
@@ -154,6 +184,34 @@ interface IPageInstance {
loadVariationImages: (list: any[]) => void
onVariationImageLoad: (e: any) => void
previewVariationImage: (e: any) => void
selectConversationDifficulty: (e: any) => void
toggleConversationScene: (e: any) => void
onConversationNoteInput: (e: any) => void
onStartConversationTap: () => void
toggleConversationSceneLang: () => void
toggleConversationEvent: (e: any) => void
onConversationCustomSceneAdd: () => void
onConversationCustomSceneInput: (e: any) => void
onConversationCustomSceneConfirm: () => void
onConversationCustomSceneCancel: () => void
onConversationCustomSceneBlur: () => void
onConversationCustomSceneDelete: (e: any) => void
onConversationCustomEventAdd: () => void
onConversationCustomEventInput: (e: any) => void
onConversationCustomEventConfirm: () => void
onConversationCustomEventBlur: () => void
onConversationCustomEventDelete: (e: any) => void
onRoleSelect: (e: any) => void
normalizeConversationTagLabel: (raw: string) => string
toggleConversationView: () => void
onSendMessage: (e: any) => void
onChatInput: (e: any) => void
onChatBlur: (e: any) => void
onChatCloseTap: (e: any) => void
updateConversationMessages: (detail: any, lang: string, append?: boolean) => void
showChatInput: () => void
onHistoryTap: () => void
chatItemClick: (e: any) => void
}
Page<IData, IPageInstance>({
@@ -179,6 +237,8 @@ Page<IData, IPageInstance>({
evalClasses: [],
clozeSentenceTokens: [],
modeAnim: '',
isChatInputVisible: false,
scrollIntoView: '',
isModeSwitching: false,
progressText: '',
questionWords: [],
@@ -224,7 +284,37 @@ Page<IData, IPageInstance>({
variationImagesLoading: {},
variationImageLoaded: {},
variationSelectedIndex: -1,
variationSubmitted: false
variationSubmitted: false,
conversationDifficulty: 'easy',
sidebar: [],
conversationSceneLang: 'zh',
conversationSelectedScenes: [],
conversationSelectedScenesMap: {},
conversationSelectedEvents: [],
conversationSelectedEventsMap: {},
conversationCustomSceneText: '',
conversationCustomSceneKey: '',
conversationCustomSceneEditing: false,
conversationCustomScenes: [],
conversationCustomSceneOverLimit: false,
conversationCustomEventText: '',
conversationCustomEventEditing: false,
conversationCustomEvents: [],
conversationCustomEventOverLimit: false,
conversationSuggestedRoles: [],
conversationExtraNote: '',
difficultyOptions: [
{ value: 'easy', label_zh: '初级', label_en: 'Easy' },
{ value: 'medium', label_zh: '中级', label_en: 'Medium' },
{ value: 'hard', label_zh: '高级', label_en: 'Hard' }
],
conversationViewMode: 'setup', // 'setup' | 'chat'
conversationLatestSession: null as any, // 存储 latest_session 信息
conversationDetail: null as any, // 存储会话详情
conversationMessages: [],
replyLoading: false,
chatInputValue: '',
renderPresets: [ { name: 'send', type: 'icon'} ]
},
updateProcessDots() {
const list = this.data.qaList || []
@@ -712,19 +802,41 @@ Page<IData, IPageInstance>({
async fetchConversationSetting(imageId: string) {
try {
this.setData({ loadingMaskVisible: true, statusText: '加载对话设置...' })
const setting = await apiManager.getQaConversationSetting(imageId)
logger.log('Conversation setting:', setting)
if (!setting) {
const res = await apiManager.getQaConversationSetting(imageId)
logger.log('Conversation setting:', res)
let viewMode = 'setup'
let detail = null
if (res && res.latest_session && res.latest_session.status === 'ongoing') {
const sessionId = res.latest_session.id || (res.latest_session as any).session_id
if (sessionId) {
try {
detail = await apiManager.getQaConversationDetail(sessionId)
logger.info('Recovered conversation detail:', detail)
viewMode = 'chat'
} catch (err) {
logger.error('Failed to recover conversation detail:', err)
}
}
}
if (!res) {
const { task_id } = await apiManager.createQaExerciseTask(imageId, 'init_conversion')
logger.log('Conversation init task created:', task_id)
this.startConversationInitPolling(task_id, imageId)
return
}
this.setData({
conversationSetting: setting,
conversationSetting: res.setting,
conversationLatestSession: res.latest_session,
conversationDetail: detail,
conversationViewMode: viewMode as any,
loadingMaskVisible: false,
statusText: '加载完成'
})
this.updateConversationMessages(detail, this.data.conversationSceneLang || 'zh', false)
} catch (e) {
logger.error('获取自由对话设置失败', e)
const msg = (e as any)?.message || ''
@@ -732,6 +844,16 @@ Page<IData, IPageInstance>({
this.setData({ loadingMaskVisible: false, statusText: '加载失败' })
}
},
toggleConversationView() {
const { conversationViewMode, conversationLatestSession } = this.data
// 如果没有 latest_session无法切换到 chat 模式,也不应该启用按钮
if (!conversationLatestSession) {
return
}
const nextMode = conversationViewMode === 'setup' ? 'chat' : 'setup'
this.setData({ conversationViewMode: nextMode })
},
startVariationPolling(taskId: string, imageId: string) {
if (this.variationPollTimer) {
clearInterval(this.variationPollTimer)
@@ -780,6 +902,51 @@ Page<IData, IPageInstance>({
}
}, 3000) as any
},
startConversationPolling(taskId: string, sessionId: string, showLoadingMask: boolean = true, append?: boolean) {
if (this.conversationPollTimer) {
clearInterval(this.conversationPollTimer)
this.conversationPollTimer = undefined
}
if (showLoadingMask) {
this.setData({ loadingMaskVisible: true, statusText: '对话生成中...' })
}
this.conversationPollTimer = setInterval(async () => {
try {
const res = await apiManager.getQaExerciseTaskStatus(taskId)
if (res.status === 'completed') {
clearInterval(this.conversationPollTimer!)
this.conversationPollTimer = undefined
try {
const detail = await apiManager.getQaConversationLatest(sessionId)
logger.info('Started conversation detail:', detail)
this.setData({
conversationDetail: detail,
conversationViewMode: 'chat',
loadingMaskVisible: false,
statusText: '加载完成',
conversationLatestSession: { id: sessionId, status: 'ongoing' },
replyLoading: false
})
this.updateConversationMessages(detail, this.data.conversationSceneLang || 'zh', append)
} catch (err) {
logger.error('Failed to get latest conversation detail:', err)
wx.showToast({ title: '获取对话详情失败', icon: 'none' })
this.setData({ loadingMaskVisible: false, statusText: '获取详情失败', replyLoading: false })
}
} else if (res.status === 'failed') {
clearInterval(this.conversationPollTimer!)
this.conversationPollTimer = undefined
wx.showToast({ title: '任务失败', icon: 'none' })
this.setData({ loadingMaskVisible: false, statusText: '任务失败', replyLoading: false })
}
} catch (err) {
logger.error('轮询对话任务状态失败', err)
}
}, 3000) as any
},
startPolling(taskId: string, imageId: string) {
if (this.pollTimer) {
clearInterval(this.pollTimer)
@@ -1569,6 +1736,521 @@ Page<IData, IPageInstance>({
this.setData({ variationSelectedIndex: i, variationResultStatus: null as any })
this.updateActionButtonsState()
},
selectConversationDifficulty(e: any) {
const level = String(e?.currentTarget?.dataset?.level || '')
if (!level) return
this.setData({ conversationDifficulty: level })
},
toggleConversationScene(e: any) {
const scene = String(e?.currentTarget?.dataset?.scene || '')
if (!scene) return
const list = (this.data.conversationSelectedScenes || []).slice()
const map = { ...(this.data.conversationSelectedScenesMap || {}) }
const idx = list.indexOf(scene)
if (idx >= 0) {
list.splice(idx, 1)
delete map[scene]
} else {
list.push(scene)
map[scene] = true
}
this.setData({ conversationSelectedScenes: list, conversationSelectedScenesMap: map })
},
toggleConversationEvent(e: any) {
const ev = String(e?.currentTarget?.dataset?.event || '')
if (!ev) return
const list = (this.data.conversationSelectedEvents || []).slice()
const map = { ...(this.data.conversationSelectedEventsMap || {}) }
const idx = list.indexOf(ev)
if (idx >= 0) {
list.splice(idx, 1)
delete map[ev]
} else {
list.push(ev)
map[ev] = true
}
const setting = this.data.conversationSetting as any
const allEvents = setting && Array.isArray((setting as any).all_possible_events) ? (setting as any).all_possible_events : []
const selectedSet = new Set(list)
const roles: Array<{ key: string; role1_en: string; role1_zh: string; role2_en: string; role2_zh: string }> = []
const seen = new Set<string>()
if (allEvents && allEvents.length && selectedSet.size) {
allEvents.forEach((item: any) => {
const evKey = String(item?.event_en || '')
if (!evKey || !selectedSet.has(evKey)) return
const sr = Array.isArray(item?.suggested_roles) ? item.suggested_roles : []
sr.forEach((r: any) => {
const r1en = String(r?.role1_en || '')
const r2en = String(r?.role2_en || '')
const r1zh = String(r?.role1_zh || '')
const r2zh = String(r?.role2_zh || '')
const dedupeKey = `${r1en}||${r2en}||${r1zh}||${r2zh}`
if (!dedupeKey.trim() || seen.has(dedupeKey)) return
seen.add(dedupeKey)
roles.push({
key: dedupeKey,
role1_en: r1en,
role1_zh: r1zh,
role2_en: r2en,
role2_zh: r2zh
})
})
})
}
this.setData({ conversationSelectedEvents: list, conversationSelectedEventsMap: map, conversationSuggestedRoles: roles })
},
onRoleSelect(e: WechatMiniprogram.TouchEvent) {
const { index, side } = e.currentTarget.dataset
const { selectedRole } = this.data
if (selectedRole && selectedRole.roleIndex === index && selectedRole.roleSide === side) {
this.setData({ selectedRole: null })
} else {
this.setData({ selectedRole: { roleIndex: index, roleSide: side } })
}
},
onConversationNoteInput(e: any) {
const v = String(e?.detail?.value || '')
this.setData({ conversationExtraNote: v.slice(0, 200) })
},
toggleConversationSceneLang() {
const cur = this.data.conversationSceneLang || 'zh'
const next = cur === 'zh' ? 'en' : 'zh'
this.setData({ conversationSceneLang: next })
this.updateConversationMessages(this.data.conversationDetail, next, false)
},
onConversationCustomSceneAdd() {
this.setData({ conversationCustomSceneEditing: true, conversationCustomSceneText: '' })
},
onConversationCustomSceneInput(e: any) {
const v = String(e?.detail?.value || '')
const text = v.trim()
let over = false
if (text) {
const parts = text.split(/\s+/)
if (parts.length > 1) {
if (parts.length > 5) {
over = true
}
} else {
const chars = Array.from(text)
if (chars.length > 12) {
over = true
}
}
}
this.setData({ conversationCustomSceneText: v, conversationCustomSceneOverLimit: over })
},
onConversationCustomSceneConfirm() {
const raw = this.data.conversationCustomSceneText || ''
const name = this.normalizeConversationTagLabel(raw)
if (!name) {
this.setData({ conversationCustomSceneEditing: false, conversationCustomSceneText: '' })
return
}
const key = `custom:${Date.now()}:${Math.random().toString(36).slice(2, 6)}`
const scenes = (this.data.conversationCustomScenes || []).slice()
scenes.push({ key, text: name })
const list = (this.data.conversationSelectedScenes || []).slice()
const map = { ...(this.data.conversationSelectedScenesMap || {}) }
if (list.indexOf(key) < 0) {
list.push(key)
}
map[key] = true
this.setData({
conversationCustomScenes: scenes,
conversationCustomSceneEditing: false,
conversationCustomSceneText: '',
conversationSelectedScenes: list,
conversationSelectedScenesMap: map
})
},
onConversationCustomSceneCancel() {
this.setData({ conversationCustomSceneEditing: false, conversationCustomSceneText: '' })
},
onConversationCustomSceneBlur() {
this.onConversationCustomSceneConfirm()
},
onConversationCustomSceneDelete(e: any) {
const key = String(e?.currentTarget?.dataset?.key || '')
if (!key) return
const scenes = (this.data.conversationCustomScenes || []).filter((s: any) => s.key !== key)
const list = (this.data.conversationSelectedScenes || []).slice()
const idx = list.indexOf(key)
if (idx >= 0) {
list.splice(idx, 1)
}
const map = { ...(this.data.conversationSelectedScenesMap || {}) }
delete map[key]
this.setData({
conversationCustomScenes: scenes,
conversationSelectedScenes: list,
conversationSelectedScenesMap: map
})
},
onConversationCustomEventAdd() {
this.setData({
conversationCustomEventEditing: true,
conversationCustomEventText: ''
})
},
async onSendMessage(e: any) {
const content = e.detail?.value || this.data.chatInputValue
if (!content || !content.trim()) return
const sessionId = this.data.conversationLatestSession?.session_id || this.data.conversationLatestSession?.id
if (!sessionId) {
wx.showToast({ title: '会话无效', icon: 'none' })
return
}
// Optimistic update
const newMsg = {
role: 'user',
content: [{ type: 'text', data: content }]
}
const messages = [...(this.data.conversationMessages || []), newMsg]
this.setData({
conversationMessages: messages,
chatInputValue: '',
replyLoading: true
})
try {
const res = await apiManager.replyQaConversation(sessionId, content)
if (res && res.task_id) {
this.startConversationPolling(res.task_id, sessionId, false, true)
} else {
wx.showToast({ title: '回复失败', icon: 'none' })
this.setData({ replyLoading: false })
}
} catch (err) {
logger.error('Reply failed:', err)
wx.showToast({ title: '发送失败', icon: 'none' })
this.setData({ replyLoading: false })
}
},
onChatInput(e: any) {
this.setData({ chatInputValue: e.detail?.value || '' })
},
onChatCloseTap(e: any) {
this.setData({ isChatInputVisible: false })
},
onChatBlur(e: any) {
this.setData({ isChatInputVisible: false, scrollIntoView: '' })
setTimeout(() => {
this.setData({ scrollIntoView: 'bottom-anchor' })
}, 300)
},
showChatInput() {
if (!this.data.conversationLatestSession) {
return
}
this.setData({ isChatInputVisible: true, scrollIntoView: '' })
setTimeout(() => {
this.setData({ scrollIntoView: 'bottom-anchor' })
logger.info('Scroll to bottom anchor')
}, 2000)
},
updateConversationMessages(detail: any, lang: string, append: boolean = false) {
if (!detail || !detail.messages) {
if (!append) {
this.setData({ conversationMessages: [] })
}
return
}
const messages = detail.messages.map((item: any) => {
const contentObj = item.content || {}
let text = ''
if (item.role === 'user') {
text = contentObj.text || ''
} else {
text = contentObj.response_en || ''
// if (lang === 'zh') {
// text = contentObj.response_zh || ''
// } else {
// text = contentObj.response_en || ''
// }
}
return {
role: item.role,
content: [{ type: 'text', data: text }]
}
})
if (append) {
const current = this.data.conversationMessages || []
this.setData({ conversationMessages: [...current, ...messages] })
} else {
this.setData({ conversationMessages: messages })
}
},
onConversationCustomEventInput(e: any) {
const v = String(e?.detail?.value || '')
const text = v.trim()
let over = false
if (text) {
const parts = text.split(/\s+/)
if (parts.length > 1) {
if (parts.length > 5) {
over = true
}
} else {
const chars = Array.from(text)
if (chars.length > 12) {
over = true
}
}
}
this.setData({ conversationCustomEventText: v, conversationCustomEventOverLimit: over })
},
onConversationCustomEventConfirm() {
const raw = this.data.conversationCustomEventText || ''
const name = this.normalizeConversationTagLabel(raw)
if (!name) {
this.setData({ conversationCustomEventEditing: false, conversationCustomEventText: '' })
return
}
const key = `custom:${Date.now()}:${Math.random().toString(36).slice(2, 6)}`
const events = (this.data.conversationCustomEvents || []).slice()
events.push({ key, text: name })
const list = (this.data.conversationSelectedEvents || []).slice()
const map = { ...(this.data.conversationSelectedEventsMap || {}) }
if (list.indexOf(key) < 0) {
list.push(key)
}
map[key] = true
this.setData({
conversationCustomEvents: events,
conversationCustomEventEditing: false,
conversationCustomEventText: '',
conversationSelectedEvents: list,
conversationSelectedEventsMap: map
})
},
normalizeConversationTagLabel(raw: string) {
const text = String(raw || '').trim()
if (!text) return ''
const parts = text.split(/\s+/)
if (parts.length > 1) {
const limited = parts.slice(0, 5)
return limited.join(' ')
}
const chars = Array.from(text)
if (chars.length > 12) {
return chars.slice(0, 12).join('')
}
return text
},
onConversationCustomEventBlur() {
this.onConversationCustomEventConfirm()
},
onConversationCustomEventDelete(e: any) {
const key = String(e?.currentTarget?.dataset?.key || '')
if (!key) return
const events = (this.data.conversationCustomEvents || []).filter((s: any) => s.key !== key)
const list = (this.data.conversationSelectedEvents || []).slice()
const idx = list.indexOf(key)
if (idx >= 0) {
list.splice(idx, 1)
}
const map = { ...(this.data.conversationSelectedEventsMap || {}) }
delete map[key]
this.setData({
conversationCustomEvents: events,
conversationSelectedEvents: list,
conversationSelectedEventsMap: map
})
},
onStartConversationTap() {
const run = async () => {
try {
const imageId = this.data.imageId
if (!imageId) {
wx.showToast({ title: '缺少图片信息', icon: 'none' })
return
}
const setting = this.data.conversationSetting as any
const scenesSelected = (this.data.conversationSelectedScenes || []).slice()
const eventsSelected = (this.data.conversationSelectedEvents || []).slice()
const customScenes = this.data.conversationCustomScenes || []
const customEvents = this.data.conversationCustomEvents || []
const selectedRole = this.data.selectedRole
const rolesList = this.data.conversationSuggestedRoles || []
const noteRaw = this.data.conversationExtraNote || ''
const info = noteRaw.trim()
let sceneList: any[] = []
let eventList: any[] = []
let userRole = {}
let assistantRole = {}
let style = {}
const hasScene = scenesSelected.length > 0
const hasEvent = eventsSelected.length > 0
const hasRole = !!selectedRole && !!rolesList[selectedRole.roleIndex]
const hasInfo = !!info
if (!hasScene && !hasEvent && !hasRole && !hasInfo) {
if (!setting) {
wx.showToast({ title: '请先加载对话设置', icon: 'none' })
return
}
const allScenes = Array.isArray(setting.all_possible_scenes) ? setting.all_possible_scenes : []
const allEvents = Array.isArray(setting.all_possible_events) ? setting.all_possible_events : []
if (!allEvents.length) {
wx.showToast({ title: '暂无可用事件', icon: 'none' })
return
}
const eventsWithRoles = allEvents.filter((ev: any) => Array.isArray(ev.suggested_roles) && ev.suggested_roles.length)
const baseEventList = eventsWithRoles.length ? eventsWithRoles : allEvents
const randEv = baseEventList[Math.floor(Math.random() * baseEventList.length)]
if (randEv && randEv.event_en) {
eventList = [String(randEv.event_en)]
}
if (randEv && randEv.style_en) {
style = String(randEv.style_en)
}
if (randEv && typeof randEv.scene_en === 'string' && randEv.scene_en) {
sceneList = [String(randEv.scene_en)]
} else if (allScenes.length) {
const randScene = allScenes[Math.floor(Math.random() * allScenes.length)]
if (randScene && randScene.scene_en) {
sceneList = [String(randScene.scene_en)]
}
}
const sr = Array.isArray((randEv as any).suggested_roles) ? (randEv as any).suggested_roles : []
if (sr.length) {
const pair = sr[Math.floor(Math.random() * sr.length)] || {}
const r1 = String(pair.role1_en || '')
const r2 = String(pair.role2_en || '')
if (r1 && r2) {
userRole = r1
assistantRole = r2
}
}
} else {
sceneList = scenesSelected.map((s) => {
if (s.startsWith('custom:')) {
const found = customScenes.find((cs: any) => cs.key === s)
if (found && found.text) {
return { en: String(found.text), zh: String(found.text) }
}
} else {
const allScenes = Array.isArray(setting.all_possible_scenes) ? setting.all_possible_scenes : []
const found = allScenes.find((item: any) => item.scene_en === s)
if (found) {
return { en: String(found.scene_en), zh: String(found.scene_zh || found.scene_en) }
}
}
return { en: String(s), zh: String(s) }
}).filter((e) => !!e.en)
const allEvents = Array.isArray(setting.all_possible_events) ? setting.all_possible_events : []
eventList = eventsSelected.map((evKey) => {
if (evKey.startsWith('custom:')) {
const found = customEvents.find((ce: any) => ce.key === evKey)
if (found && found.text) {
return { en: String(found.text), zh: String(found.text) }
}
} else {
const matchedEv = allEvents.find((e: any) => e.event_en === evKey)
if (matchedEv) {
// 如果找到了事件对象,也提取 style
if (matchedEv.style_en) {
style = { en: String(matchedEv.style_en), zh: String(matchedEv.style_zh || matchedEv.style_en) }
}
return { en: String(matchedEv.event_en), zh: String(matchedEv.event_zh || matchedEv.event_en) }
}
}
return { en: String(evKey), zh: String(evKey) }
}).filter((e) => !!e.en)
// 如果没有选中场景,但选中了事件,且事件有默认场景,则使用默认场景
if (sceneList.length === 0 && eventList.length > 0) {
// 尝试从第一个事件中获取关联场景
const firstEventKey = eventsSelected[0]
if (!firstEventKey.startsWith('custom:')) {
const foundEvent = allEvents.find((item: any) => item.event_en === firstEventKey)
if (foundEvent && foundEvent.scene_en) {
sceneList = [{ en: String(foundEvent.scene_en), zh: String(foundEvent.scene_zh || foundEvent.scene_en) }]
}
}
}
if (eventList.length > 0) {
const firstEventKey = eventsSelected[0]
if (!firstEventKey.startsWith('custom:')) {
const foundEvent = allEvents.find((item: any) => item.event_en === firstEventKey)
if (foundEvent && foundEvent.style_en) {
style = { en: String(foundEvent.style_en), zh: String(foundEvent.style_zh || foundEvent.style_en) }
}
}
}
if (hasRole && selectedRole) {
const idx = selectedRole.roleIndex
const side = selectedRole.roleSide
const pair = rolesList[idx]
if (pair) {
if (side === 1) {
userRole = { en: String(pair.role1_en), zh: String(pair.role1_zh || pair.role1_en) }
assistantRole = { en: String(pair.role2_en), zh: String(pair.role2_zh || pair.role2_en) }
} else {
userRole = { en: String(pair.role2_en), zh: String(pair.role2_zh || pair.role2_en) }
assistantRole = { en: String(pair.role1_en), zh: String(pair.role1_zh || pair.role1_en) }
}
}
}
}
const levelKey = this.data.conversationDifficulty || 'easy'
let level = levelKey
const payload: any = {
image_id: imageId,
level,
scene: sceneList,
event: eventList,
info: hasInfo ? info : undefined
}
if (userRole) payload.user_role = userRole
if (assistantRole) payload.assistant_role = assistantRole
if (style) payload.style = style
// wx.showLoading({ title: '启动对话...', mask: true })
if (info) {
payload.info = info
}
logger.info('开始对话 payload:', payload)
this.setData({ loadingMaskVisible: true, statusText: '正在创建对话...' })
const res = await apiManager.startQaConversation(payload)
if (res && res.task_id && res.session_id) {
this.startConversationPolling(res.task_id, res.session_id, false)
} else {
this.setData({ loadingMaskVisible: false })
// wx.showToast({ title: '已发起对话', icon: 'success' })
}
} catch (err) {
this.setData({ loadingMaskVisible: false })
logger.error('开始对话失败', err)
const msg = (err as any)?.message || '开始对话失败'
wx.showToast({ title: msg, icon: 'none' })
}
}
run()
},
async getQuestionAudio(questionId: string) {
try {
const map = this.data.audioUrlMap || {}
@@ -1674,6 +2356,67 @@ Page<IData, IPageInstance>({
})
}
},
onHistoryTap() {
const run = async () => {
try {
const { imageId } = this.data
if (!imageId) {
wx.showToast({ title: '缺少图片信息', icon: 'none' })
return
}
this.setData({ visible: true })
const res = await apiManager.listQaConversations(imageId)
logger.info('获取到的历史聊天对话记录:', res)
// Assuming res is { list: [...], total: ... } or just array
const list = Array.isArray(res) ? res : (res.list || [])
const sidebar = list.map((item: any) => ({
title: item.title || item.summary || (item.created_time ? `对话 ${item.created_time}` : `对话 ${item.id}`),
...item
}))
this.setData({ sidebar })
} catch (err) {
logger.error('获取历史聊天对话记录失败:', err)
wx.showToast({ title: '获取历史记录失败', icon: 'none' })
}
}
run()
},
chatItemClick(e: any) {
const { item } = e.detail
if (!item || !item.id) return
const sessionId = item.id
this.setData({ visible: false })
const run = async () => {
try {
this.setData({ loadingMaskVisible: true, statusText: '加载对话...' })
const detail = await apiManager.getQaConversationLatest(sessionId)
logger.info('Loaded conversation detail:', detail)
this.setData({
conversationDetail: detail,
conversationViewMode: 'chat',
loadingMaskVisible: false,
statusText: '加载完成',
conversationLatestSession: { id: sessionId, status: 'ongoing' },
replyLoading: false
})
this.updateConversationMessages(detail, this.data.conversationSceneLang || 'zh', false)
} catch (err) {
logger.error('Failed to load conversation:', err)
wx.showToast({ title: '加载对话失败', icon: 'none' })
this.setData({ loadingMaskVisible: false })
}
}
run()
},
onUnload() {
if (this.pollTimer) {
clearInterval(this.pollTimer)

View File

@@ -24,6 +24,7 @@
<view class="process-dot {{processDotClasses[index]}} {{index === currentIndex ? 'current' : ''}}"></view>
</block>
</view>
<scroll-view class="question-scroll {{conversationViewMode === 'chat' && isChatInputVisible ? 'chat-input-mode-scroll' : 'chat-mode-scroll'}}" scroll-y="true" scroll-into-view="{{scrollIntoView}}" scroll-with-animation>
<view class="image-card" wx:if="{{questionMode !== 'variation'}}">
<image wx:if="{{imageLocalUrl}}" class="image" src="{{imageLocalUrl}}" mode="aspectFill" bindtap="previewImage" bindload="onImageLoad" binderror="onImageError"></image>
<view class="view-full" wx:if="{{imageLocalUrl}}" bindtap="previewImage">
@@ -34,8 +35,6 @@
<view class="question-title" wx:if="{{questionMode !== 'conversation'}}">
<text wx:for="{{questionWords}}" wx:key="index" class="word-item" data-word="{{item}}" bindtap="handleWordClick">{{item}}</text>
</view>
<!-- <view class="progress-text">{{progressText}}</view> -->
<scroll-view class="question-scroll" scroll-y="true">
<view class="question-content {{modeAnim}}" wx:if="{{questionMode === 'choice'}}">
<view class="choice-title">Select the correct answer ({{selectedCount}}/{{choiceRequiredCount}})</view>
<view class="option-list">
@@ -61,20 +60,6 @@
</view>
</view>
</view>
<view class="question-content {{modeAnim}}" wx:if="{{questionMode === 'free_text'}}">
<view class="tip-row">
<t-icon name="info-circle" size="32rpx" />
<text class="tip-text">Tip: Be specific about the object type.</text>
</view>
<view class="answer-row">
<text class="answer-label">Your Answer</text>
<text class="answer-hint" bindtap="onHintTap">Need a hint?</text>
</view>
<view class="input-card">
<textarea class="answer-input" placeholder="Type your answer here..." maxlength="200" bindinput="inputChange" value="{{freeTextInput}}" />
<text class="input-type">English input</text>
</view>
</view>
<view class="question-content {{modeAnim}}" wx:if="{{questionMode === 'variation'}}">
<view class="variation-container">
<view class="variation-grid">
@@ -101,15 +86,194 @@
</view>
</view>
</view>
<view class="conversation-content {{modeAnim}}" wx:if="{{questionMode === 'conversation' && conversationViewMode === 'setup'}}">
<view class="conversation-section">
<view class="conversation-label">难度选择</view>
<view class="conversation-tags">
<t-check-tag
wx:for="{{difficultyOptions}}"
wx:key="value"
variant="outline"
size="medium"
data-level="{{item.value}}"
checked="{{conversationDifficulty === item.value}}"
bindtap="selectConversationDifficulty"
content="{{ [conversationSceneLang === 'zh' ? item.label_zh : item.label_en] }}"
/>
</view>
</view>
<view class="conversation-section">
<view class="conversation-label">场景标签</view>
<view class="conversation-tags" wx:if="{{conversationSetting && conversationSetting.all_possible_scenes && conversationSetting.all_possible_scenes.length}}">
<t-check-tag
wx:for="{{conversationSetting.all_possible_scenes}}"
wx:key="scene_en"
variant="outline"
size="medium"
data-scene="{{item.scene_en}}"
checked="{{conversationSelectedScenesMap && conversationSelectedScenesMap[item.scene_en]}}"
bindtap="toggleConversationScene"
>
{{conversationSceneLang === 'zh' ? item.scene_zh : item.scene_en}}
</t-check-tag>
<t-check-tag
wx:for="{{conversationCustomScenes}}"
wx:key="key"
variant="outline"
size="medium"
data-scene="{{item.key}}"
checked="{{conversationSelectedScenesMap && conversationSelectedScenesMap[item.key]}}"
bindtap="toggleConversationScene"
>
<view class="custom-scene-tag">
<text class="custom-scene-text">{{item.text}}</text>
<t-icon name="close" size="24rpx" data-key="{{item.key}}" catchtap="onConversationCustomSceneDelete" />
</view>
</t-check-tag>
<view wx:if="{{conversationCustomSceneEditing}}" class="conversation-custom-scene-input-wrapper">
<input
class="conversation-custom-scene-input {{conversationCustomSceneOverLimit ? 'conversation-custom-scene-input-error' : ''}}"
value="{{conversationCustomSceneText}}"
placeholder="自定义场景"
maxlength="60"
focus="{{true}}"
bindinput="onConversationCustomSceneInput"
bindblur="onConversationCustomSceneBlur"
/>
</view>
<t-check-tag
wx:if="{{!conversationCustomSceneEditing}}"
variant="outline"
size="medium"
bindtap="onConversationCustomSceneAdd"
>
+
</t-check-tag>
</view>
</view>
<view class="conversation-section">
<view class="conversation-label">事件标签</view>
<view class="conversation-tags" wx:if="{{conversationSetting && conversationSetting.all_possible_events && conversationSetting.all_possible_events.length}}">
<t-check-tag
wx:for="{{conversationSetting.all_possible_events}}"
wx:key="event_en"
variant="outline"
size="medium"
data-event="{{item.event_en}}"
checked="{{conversationSelectedEventsMap && conversationSelectedEventsMap[item.event_en]}}"
bindtap="toggleConversationEvent"
>
{{conversationSceneLang === 'zh' ? item.event_zh : item.event_en}}
</t-check-tag>
<t-check-tag
wx:for="{{conversationCustomEvents}}"
wx:key="key"
variant="outline"
size="medium"
data-event="{{item.key}}"
checked="{{conversationSelectedEventsMap && conversationSelectedEventsMap[item.key]}}"
bindtap="toggleConversationEvent"
>
<view class="custom-scene-tag">
<text class="custom-scene-text">{{item.text}}</text>
<t-icon name="close" size="24rpx" data-key="{{item.key}}" catchtap="onConversationCustomEventDelete" />
</view>
</t-check-tag>
<view wx:if="{{conversationCustomEventEditing}}" class="conversation-custom-scene-input-wrapper">
<input
class="conversation-custom-scene-input {{conversationCustomEventOverLimit ? 'conversation-custom-scene-input-error' : ''}}"
value="{{conversationCustomEventText}}"
placeholder="自定义事件"
maxlength="60"
focus="{{true}}"
bindinput="onConversationCustomEventInput"
bindblur="onConversationCustomEventBlur"
/>
</view>
<t-check-tag
wx:if="{{!conversationCustomEventEditing}}"
variant="outline"
size="medium"
bindtap="onConversationCustomEventAdd"
>
+
</t-check-tag>
</view>
</view>
<view class="conversation-section" wx:if="{{conversationSuggestedRoles && conversationSuggestedRoles.length}}">
<view class="conversation-label">角色扮演</view>
<view class="conversation-tags">
<block wx:for="{{conversationSuggestedRoles}}" wx:key="key" wx:for-index="idx">
<view style="display: flex; align-items: center;">
<t-check-tag
checked="{{selectedRole && selectedRole.roleIndex === idx && selectedRole.roleSide === 1}}"
bind:change="onRoleSelect"
data-index="{{idx}}"
data-side="{{1}}"
variant="outline"
size="medium"
>{{conversationSceneLang === 'zh' ? item.role1_zh : item.role1_en}}</t-check-tag>
<t-check-tag
checked="{{selectedRole && selectedRole.roleIndex === idx && selectedRole.roleSide === 2}}"
bind:change="onRoleSelect"
data-index="{{idx}}"
data-side="{{2}}"
variant="outline"
size="medium"
>{{conversationSceneLang === 'zh' ? item.role2_zh : item.role2_en}}</t-check-tag>
</view>
</block>
</view>
</view>
<view class="submit-row">
<view class="conversation-section">
<view class="conversation-label-row">
<text class="conversation-label">额外说明</text>
<text class="conversation-count">{{(conversationExtraNote && conversationExtraNote.length) || 0}}/200</text>
</view>
<view class="conversation-note-card">
<textarea
class="conversation-note-input"
placeholder="例如:描述一下图片中的人物关系或具体发生的事件,这将帮助 AI 更好地生成对话内容..."
maxlength="200"
value="{{conversationExtraNote}}"
bindinput="onConversationNoteInput"
/>
</view>
</view>
<view class="conversation-start-row">
<button class="conversation-start-btn" bindtap="onStartConversationTap">
开始对话
<t-icon name="chat" size="32rpx" style="margin-left: 12rpx;" />
</button>
</view>
</view>
<view class="conversation-content {{modeAnim}}" wx:if="{{questionMode === 'conversation' && conversationViewMode === 'chat'}}">
<block wx:if="{{conversationMessages && conversationMessages.length}}">
<view class="conversation-block {{item.role === 'user' ? 'user' : 'default'}}" wx:for="{{conversationMessages}}" wx:key="index"
>
<t-chat-message
role="{{item.role}}"
placement="{{item.role === 'user' ? 'right' : 'left'}}"
content="{{item.content}}"
/>
</view>
</block>
<view class="conversation-block" wx:if="{{replyLoading}}">
<t-chat-loading animation="dots" />
</view>
<view id="bottom-anchor" style="height: 1rpx;"></view>
</view>
<view class="submit-row" wx:if="{{questionMode !== 'conversation'}}">
<button class="submit-btn" bindtap="onSubmitTap" disabled="{{submitDisabled}}" wx:if="{{retryDisabled}}">提交</button>
<button class="submit-btn" bindtap="onRetryTap" disabled="{{retryDisabled}}" wx:else>重试</button>
</view>
</scroll-view>
</view>
<view class="bottom-bar {{contentVisible ? 'show' : ''}}">
<view class="bottom-bar {{contentVisible ? 'show' : ''}}" wx:if="{{questionMode !== 'conversation'}}">
<t-icon name="chevron-left" class="bottom-btn {{currentIndex <= 0 ? 'disabled' : ''}}" size="48rpx" bind:tap="onPrevTap" />
<t-icon name="{{isPlaying ? 'pause' : 'play'}}" class="bottom-btn" size="48rpx" bind:tap="playStandardVoice" />
<!-- <t-icon name="swap" class="bottom-btn" size="48rpx" bind:tap="toggleMode" /> -->
@@ -117,6 +281,43 @@
<t-icon name="{{nextButtonIcon || 'chevron-right'}}" class="bottom-btn {{(qaList && (currentIndex >= qaList.length - 1)) ? 'disabled' : ''}}" size="48rpx" bind:tap="onNextTap" />
</view>
<t-drawer
visible="{{visible}}"
placement="right"
items="{{conversationList}}"
bind:overlay-click="overlayClick"
bind:item-click="chatItemClick"
></t-drawer>
<view class="chat-sender-wrapper {{isChatInputVisible ? 'show' : ''}}" wx:if="{{questionMode === 'conversation' && conversationViewMode === 'chat'}}">
<t-chat-sender
placeholder="请输入..."
loading="{{replyLoading}}"
focus="{{isChatInputVisible}}"
renderPresets="{{renderPresets}}"
bind:send="onSendMessage"
bind:input="onChatInput"
bind:blur="onChatBlur"
>
<view slot="footer-prefix" class="footer-prefix">
<!-- <view class="deep-think-block {{deepThinkActive ? 'active' : ''}}" bind:tap="onDeepThinkTap">
<t-icon name="system-sum" size="40rpx" />
<text class="deep-think-text">深度思考</text>
</view> -->
<view class="chat-icon-block" bind:tap="onChatCloseTap">
<t-icon name="close-circle" size="64rpx" color="#dcdcdc"/>
</view>
</view>
</t-chat-sender>
</view>
<view class="bottom-bar {{contentVisible && !isChatInputVisible ? 'show' : ''}}" wx:if="{{questionMode === 'conversation'}}">
<t-icon name="translate" class="bottom-btn" size="48rpx" bind:tap="toggleConversationSceneLang" />
<t-icon name="keyboard" class="bottom-btn {{conversationLatestSession ? '' : 'disabled'}}" size="48rpx" bind:tap="showChatInput" />
<t-icon name="{{conversationLatestSession && conversationViewMode === 'chat' ? 'chat-bubble-add' : 'chat-bubble-1'}}" class="bottom-btn {{conversationLatestSession ? '' : 'disabled'}}" size="48rpx" bind:tap="toggleConversationView" />
<t-icon name="fact-check" class="bottom-btn {{resultDisplayed ? '' : 'disabled'}}" size="48rpx" bind:tap="" />
<t-icon name="chat-bubble-history" class="bottom-btn" size="48rpx" bind:tap="onHistoryTap" />
</view>
<word-dictionary
id="wordDict"
visible="{{showDictPopup}}"

View File

@@ -8,9 +8,61 @@
@keyframes fadeInUp { from { opacity: 0; transform: translate3d(0, 12rpx, 0) } to { opacity: 1; transform: translate3d(0, 0, 0) } }
@keyframes fadeOutDown { from { opacity: 1; transform: translate3d(0, 0, 0) } to { opacity: 0; transform: translate3d(0, 12rpx, 0) } }
.question-scroll { padding-bottom: calc(110rpx + env(safe-area-inset-bottom));}
.question-scroll {
flex: 1;
height: 0;
/* padding-bottom: calc(110rpx + env(safe-area-inset-bottom)); */
/* transition: padding-bottom 0.3s ease; */
transition: margin-bottom 0.3s ease;
}
.chat-mode-scroll {
/* padding-bottom: calc(110rpx + env(safe-area-inset-bottom)); */
margin-bottom: calc(110rpx + env(safe-area-inset-bottom));
}
.chat-input-mode-scroll {
/* padding-bottom: calc(240rpx + env(safe-area-inset-bottom)); */
margin-bottom: calc(240rpx + env(safe-area-inset-bottom));
}
.loading-container { padding: 32rpx; }
.chat-sender-wrapper {
position: fixed;
left: 0;
right: 0;
bottom: 0;
/* bottom: calc(120rpx + env(safe-area-inset-bottom)); */
z-index: 100;
background: #fff;
/* padding: 16rpx 32rpx; */
border-top: 1rpx solid #f0f4f8;
box-shadow: 0 -4rpx 16rpx rgba(0,0,0,0.04);
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
pointer-events: none;
}
.chat-sender-wrapper.show {
opacity: 1;
visibility: visible;
pointer-events: auto;
}
.footer-prefix {
position: absolute;
right: 80rpx;
display: flex;
align-items: center;
}
.chat-icon-block {
color: var(--td-text-color-primary);
border-radius: 200rpx;
border: 2rpx solid var(--td-component-border);
display: flex;
justify-content: center;
align-items: center;
}
.process-container { width: 100%; display: flex; gap: 16rpx; align-items: center; padding: 8rpx 0; }
.process-dot { flex: 1; height: 10rpx; border-radius: 8rpx; background: #e6eef9; transition: all 0.3s ease; border: 3rpx solid transparent; box-sizing: border-box; }
.process-dot.dot-0 { background: #d9e6f2; }
@@ -46,7 +98,7 @@
z-index: 99;
}
.image-card { position: relative; border-radius: 24rpx; overflow: hidden; height: 360rpx; }
.image-card { position: relative; border-radius: 24rpx; overflow: hidden; height: 360rpx; box-shadow: none; }
.image { width: 100%; height: 360rpx; border-radius: 24rpx; background: #f5f5f5; }
.view-full { position: absolute; right: 16rpx; bottom: 16rpx; display: flex; align-items: center; gap: 8rpx; padding: 8rpx; border-radius: 24rpx; background: rgba(0,0,0,0.4); color: #fff; }
.question-title { font-size: 40rpx; font-weight: 700; color: #001858; line-height: 56rpx; width: 100%; display: flex; flex-wrap: wrap; justify-content: flex-start;}
@@ -258,6 +310,133 @@
border-radius: 16rpx;
background: #f8f9fb;
}
.conversation-content {
display: flex;
flex-direction: column;
gap: 32rpx;
padding-top: 16rpx;
}
.conversation-block.user {
--td-bg-color-secondarycontainer: #f5f5f5;
--td-spacer-2: 0rpx;
}
.conversation-block.default {
--td-spacer-2: 12rpx;
width: 85%;
background-color: #e0e0e0;
padding: 12rpx 20rpx 0 20rpx;
border-radius: 0 24rpx 24rpx 24rpx;
}
.conversation-section {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.conversation-label {
font-size: 30rpx;
font-weight: 700;
color: #001858;
}
.conversation-segment {
display: flex;
background: #f5f7fb;
border-radius: 999rpx;
padding: 6rpx;
}
.segment-item {
flex: 1;
text-align: center;
padding: 20rpx 0;
font-size: 28rpx;
color: #666;
border-radius: 999rpx;
}
.segment-item.active {
background: #ffffff;
color: #00b578;
font-weight: 700;
}
.conversation-tags {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.conversation-custom-scene-input-wrapper {
min-width: 160rpx;
}
.conversation-custom-scene-input {
padding: 0rpx 15rpx;
border-radius: 8rpx;
border: 2rpx dashed #00b578;
background: #f5f7fb;
font-size: 24rpx;
color: #001858;
}
.conversation-custom-scene-input-error {
border-color: #ff4d4f;
}
.custom-scene-tag {
display: inline-flex;
align-items: center;
gap: 8rpx;
}
.custom-scene-text {
font-size: 26rpx;
}
.tag-item {
padding: 12rpx 28rpx;
border-radius: 999rpx;
font-size: 26rpx;
color: #333;
background: #f5f7fb;
}
.tag-item.active {
background: #e6fff4;
color: #00b578;
}
.conversation-label-row {
display: flex;
align-items: baseline;
justify-content: space-between;
}
.conversation-count {
font-size: 24rpx;
color: #999;
}
.conversation-note-card {
border-radius: 24rpx;
background: #f5f7fb;
padding: 20rpx;
}
.conversation-note-input {
width: 100%;
min-height: 200rpx;
font-size: 28rpx;
color: #001858;
}
.conversation-start-row {
margin-top: 24rpx;
}
.conversation-start-btn {
width: 100%;
height: 96rpx;
line-height: 96rpx;
border-radius: 999rpx;
background: #00b578;
color: #fff;
font-size: 32rpx;
font-weight: 700;
border: none;
padding: 0;
margin: 0;
display: flex;
align-items: center;
justify-content: center;
}
.conversation-start-btn::after {
border: none;
}
.detail-row.correct { background: #eafaf2; }
.detail-row.incorrect { background: #fff7f6; }
.detail-row.missing { background: #eaf2ff; }

View File

@@ -143,6 +143,54 @@ export interface IQaResult {
updated_time?: string
}
export interface IQaConversationCoreObject {
object_en: string
object_zh: string
[key: string]: any
}
export interface IQaConversationEvent {
event_en: string
event_zh: string
[key: string]: any
}
export interface IQaConversationScene {
scene_en: string
scene_zh: string
[key: string]: any
}
export interface IQaConversationSettingPayload {
all_possible_events: IQaConversationEvent[]
all_possible_scenes: IQaConversationScene[]
core_objects: IQaConversationCoreObject[]
}
export interface IQaConversationSession {
id: string | number
status: string
created_at?: string
updated_at?: string
ext?: any
}
export interface IQaConversationSettingResponse {
image_id: string
latest_session: IQaConversationSession | null
setting: IQaConversationSettingPayload
}
export interface IQaConversationDetail {
id: string | number
status: string
created_at?: string
updated_at?: string
ext?: any
messages?: any[]
[key: string]: any
}
// 单词详情接口
export interface ExtendedWordDetail {
word: string

View File

@@ -13,7 +13,9 @@ import {
IQaExerciseQueryResponse,
IQaQuestionAttemptAccepted,
IQaQuestionTaskStatus,
IQaResult
IQaResult,
IQaConversationSettingResponse,
IQaConversationDetail
} from '../types/app';
import { BASE_URL, USE_CLOUD } from './config';
import { cloudConfig } from './cloud.config';
@@ -2163,13 +2165,49 @@ class ApiManager {
return resp.data
}
async getQaConversationSetting(imageId: string): Promise<any> {
const resp = await this.request<any>('/api/v1/qa/conversations/setting', 'POST', {
async getQaConversationSetting(imageId: string): Promise<IQaConversationSettingResponse> {
const resp = await this.request<IQaConversationSettingResponse>('/api/v1/qa/conversations/setting', 'POST', {
image_id: imageId
})
return resp.data
}
async startQaConversation(payload: {
image_id: string | number
scene?: Array<{ en: string; zh: string }>
event?: Array<{ en: string; zh: string }>
user_role?: { en: string; zh: string }
assistant_role?: { en: string; zh: string }
level?: string
info?: string
style?: { en: string; zh: string }
}): Promise<any> {
const resp = await this.request<any>('/api/v1/qa/conversations/start', 'POST', payload)
return resp.data
}
async getQaConversationDetail(sessionId: string | number): Promise<IQaConversationDetail> {
const resp = await this.request<IQaConversationDetail>(`/api/v1/qa/conversations/${sessionId}`, 'GET')
return resp.data
}
async getQaConversationLatest(sessionId: string | number): Promise<IQaConversationDetail> {
const resp = await this.request<IQaConversationDetail>(`/api/v1/qa/conversations/${sessionId}/latest`, 'GET')
return resp.data
}
async listQaConversations(imageId: string | number, page: number = 1, pageSize: number = 10): Promise<any> {
const resp = await this.request<any>(`/api/v1/qa/conversations/${imageId}/list?page=${page}&page_size=${pageSize}`, 'GET')
return resp.data
}
async replyQaConversation(sessionId: string | number, content: string): Promise<any> {
const resp = await this.request<any>(`/api/v1/qa/conversations/${sessionId}/reply`, 'POST', {
content
})
return resp.data
}
async createQaQuestionAttempt(
questionId: string,
payload: {

8
package-lock.json generated
View File

@@ -9,7 +9,7 @@
"version": "25.9.10",
"dependencies": {
"cos-wx-sdk-v5": "^1.8.0",
"tdesign-miniprogram": "^1.11.2"
"tdesign-miniprogram": "^1.12.1"
},
"devDependencies": {
"miniprogram-api-typings": "^2.8.3-1"
@@ -80,9 +80,9 @@
"license": "MIT"
},
"node_modules/tdesign-miniprogram": {
"version": "1.11.2",
"resolved": "https://mirrors.huaweicloud.com/repository/npm/tdesign-miniprogram/-/tdesign-miniprogram-1.11.2.tgz",
"integrity": "sha512-lXcry3vRa9jHzjpOdIxuIAh7F85kImym82VwLbCqr/TkMhycOsOepx+r1S9fum7u2nsWiYRTV+HuvDHN3KlIuA==",
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/tdesign-miniprogram/-/tdesign-miniprogram-1.12.1.tgz",
"integrity": "sha512-L2w/6lqVXSCtptvlPGlY91dutMb3dJlx9rFpmpLRtG7c7v1l89rIhvgLRgsaBcwTA5btdB8VXGXpTVj621bv5w==",
"license": "MIT"
}
}

View File

@@ -11,6 +11,6 @@
},
"dependencies": {
"cos-wx-sdk-v5": "^1.8.0",
"tdesign-miniprogram": "^1.11.2"
"tdesign-miniprogram": "^1.12.1"
}
}