diff --git a/miniprogram/pages/qa_exercise/qa_exercise.json b/miniprogram/pages/qa_exercise/qa_exercise.json index d3b65a9..944c7f3 100644 --- a/miniprogram/pages/qa_exercise/qa_exercise.json +++ b/miniprogram/pages/qa_exercise/qa_exercise.json @@ -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" } } diff --git a/miniprogram/pages/qa_exercise/qa_exercise.ts b/miniprogram/pages/qa_exercise/qa_exercise.ts index 6466792..9586b03 100644 --- a/miniprogram/pages/qa_exercise/qa_exercise.ts +++ b/miniprogram/pages/qa_exercise/qa_exercise.ts @@ -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 + conversationSelectedEvents?: string[] + conversationSelectedEventsMap?: Record + 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: (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({ @@ -179,6 +237,8 @@ Page({ evalClasses: [], clozeSentenceTokens: [], modeAnim: '', + isChatInputVisible: false, + scrollIntoView: '', isModeSwitching: false, progressText: '', questionWords: [], @@ -224,7 +284,37 @@ Page({ 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({ 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({ 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({ } }, 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({ 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() + 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({ }) } }, + 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) diff --git a/miniprogram/pages/qa_exercise/qa_exercise.wxml b/miniprogram/pages/qa_exercise/qa_exercise.wxml index 4582c13..7471f70 100644 --- a/miniprogram/pages/qa_exercise/qa_exercise.wxml +++ b/miniprogram/pages/qa_exercise/qa_exercise.wxml @@ -24,18 +24,17 @@ - - - - - + + + + + + + + + + {{item}} - - - {{item}} - - - Select the correct answer ({{selectedCount}}/{{choiceRequiredCount}}) @@ -61,20 +60,6 @@ - - - - Tip: Be specific about the object type. - - - Your Answer - Need a hint? - - -