Files
miniprogram-1/miniprogram/pages/qa_exercise/qa_exercise.wxml
2026-01-27 19:57:56 +08:00

503 lines
27 KiB
Plaintext

<view class="qa-exercise-container">
<view wx:if="{{!contentReady || loadingMaskVisible}}" class="page-loading-mask">
<view class="loading-center">
<view class="scanner scanner-visible">
<view class="star star1"></view>
<view class="star star2"></view>
<view class="star star3"></view>
</view>
<view class="status-text">{{statusText}}</view>
</view>
</view>
<view class="type-container" wx:if="{{fixedMode}}">
<view class="type-item {{fixedMode === 'cloze' ? 'active' : ''}}" hover-class="type-item-hover" bindtap="switchMode" data-mode="cloze">填空</view>
<view class="type-item {{fixedMode === 'choice' ? 'active' : ''}}" hover-class="type-item-hover" bindtap="switchMode" data-mode="choice">问答</view>
<!-- <view class="type-item {{fixedMode === 'variation' ? 'active' : ''}}" hover-class="type-item-hover" bindtap="switchMode" data-mode="variation">识图</view> -->
<view class="type-item {{fixedMode === 'conversation' ? 'active' : ''}}" hover-class="type-item-hover" bindtap="switchMode" data-mode="conversation">对话</view>
</view>
<view class="container {{contentVisible ? 'fade-in' : 'fade-out'}}" wx:if="{{contentReady && !loadingMaskVisible}}">
<view class="process-container" wx:if="{{qaList && qaList.length > 0 && questionMode !== 'conversation'}}">
<block wx:for="{{qaList}}" wx:key="index">
<view class="process-dot {{processDotClasses[index]}} {{index === currentIndex ? 'current' : ''}}"></view>
</block>
</view>
<view class="question-scroll-wrapper {{questionMode === 'conversation' && conversationViewMode === 'chat' && isChatInputVisible && !isMicrophoneMode ? 'chat-input-mode-scroll' : 'chat-mode-scroll'}}">
<scroll-view class="inner-scroll" scroll-y >
<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">
<t-icon name="zoom-in" size="32rpx" />
</view>
</view>
<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="question-content {{modeAnim}}" wx:if="{{questionMode === 'choice'}}">
<view class="choice-title">Select the correct answer ({{selectedCount}}/{{choiceRequiredCount}})</view>
<view class="option-list">
<view wx:for="{{choiceOptions}}" wx:key="index" class="option-item {{evalClasses[index]}} {{(!choiceSubmitted && !selectedFlags[index] && selectedCount >= choiceRequiredCount) ? 'disabled' : ''}} {{resultDisplayed ? 'disabled' : ''}}" data-index="{{index}}" data-word="{{item.content}}" bindtap="selectOption" bindlongpress="onOptionLongPress">
<view class="option-radio">
<view class="radio-dot {{selectedFlags[index] ? 'on' : ''}} {{(evalClasses[index] === 'opt-incorrect' && selectedFlags[index]) ? 'red' : ''}}"></view>
</view>
<text class="option-text">{{item.content}}</text>
</view>
</view>
</view>
<view class="question-content {{modeAnim}}" wx:if="{{questionMode === 'cloze'}}">
<view class="cloze-sentence">
<text wx:for="{{clozeSentenceTokens}}" wx:key="index" class="{{item.isBlank ? 'cloze-fill' : 'cloze-text'}}" data-word="{{item.word}}" bindtap="handleWordClick">{{item.text}}</text>
</view>
<view class="option-list">
<view wx:for="{{clozeOptions}}" wx:key="index" class="option-item {{evalClasses[index]}} {{resultDisplayed ? 'disabled' : ''}}" data-index="{{index}}" data-word="{{item}}" bindtap="selectClozeOption" bindlongpress="onOptionLongPress">
<view class="option-radio">
<view class="radio-dot {{selectedClozeIndex === index ? 'on' : ''}} {{(evalClasses[index] === 'opt-incorrect' && selectedClozeIndex === index) ? 'red' : ''}}"></view>
</view>
<text class="option-text">{{item}}</text>
</view>
</view>
</view>
<view class="question-content {{modeAnim}}" wx:if="{{questionMode === 'variation'}}">
<view class="variation-container">
<view class="variation-grid">
<view class="variation-item" wx:for="{{variationQaList}}" wx:key="index">
<view class="variation-image-wrapper {{index === variationSelectedIndex ? 'selected' : ''}}" data-index="{{index}}" bindtap="selectVariationOption">
<cloud-image
file-id="{{item.file_id}}"
mode="widthFix"
height="auto"
radius="24rpx"
bind:load="onVariationImageLoad"
data-fileid="{{item.file_id}}"
/>
<view class="view-full" wx:if="{{variationImageLoaded[item.file_id]}}" data-fileid="{{item.file_id}}" catchtap="previewVariationImage">
<t-icon name="zoom-in" size="32rpx" />
</view>
<view wx:if="{{variationImageLoaded[item.file_id]}}" class="selection-badge {{variationResultStatus === 'incorrect' && index === variationSelectedIndex ? 'incorrect' : (index === variationSelectedIndex ? 'selected' : 'unselected')}}">
<t-icon name="{{variationResultStatus === 'incorrect' && index === variationSelectedIndex ? 'close-circle' : 'check-circle'}}" size="36rpx" color="#fff" />
</view>
<view wx:if="{{variationResultStatus && index === variationSelectedIndex}}" class="variation-border {{variationResultStatus}}"></view>
</view>
<!-- <view class="variation-text">{{item.question}}</view> -->
</view>
</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="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>
<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="fact-check" class="bottom-btn {{resultDisplayed ? '' : 'disabled'}}" size="48rpx" bind:tap="onScoreTap" />
<t-icon name="{{nextButtonIcon || 'chevron-right'}}" class="bottom-btn {{(qaList && (currentIndex >= qaList.length - 1)) ? 'disabled' : ''}}" size="48rpx" bind:tap="onNextTap" />
</view>
<t-drawer visible="{{historyVisible}}" placement="left" bind:visible-change="onHistoryDrawerClose" close-btn="{{true}}">
<scroll-view scroll-y class="history-drawer-scroll" bindscrolltolower="onHistoryScrollBottom">
<view class="history-list">
<t-cell
wx:for="{{historyList}}"
wx:key="session_id"
title="{{item.created_at}}"
hover
class="history-item {{conversationLatestSession && conversationLatestSession.session_id === item.session_id ? 'active' : ''}}"
bind:click="chatItemClick"
data-item="{{item}}"
>
<view slot="description" class="history-tags">
<block wx:for="{{item.scene}}" wx:for-item="scene" wx:key="index">
<view class="history-tag" wx:if="{{scene.en || scene.zh}}">
{{conversationSceneLang === 'zh' ? (scene.zh || scene.en) : (scene.en || scene.zh)}}
</view>
</block>
<block wx:for="{{item.event}}" wx:for-item="ev" wx:key="index">
<view class="history-tag" wx:if="{{ev.en || ev.zh}}">
{{conversationSceneLang === 'zh' ? (ev.zh || ev.en) : (ev.en || ev.zh)}}
</view>
</block>
</view>
</t-cell>
<view class="history-loading" wx:if="{{historyLoading}}">
<t-loading theme="spinner" size="40rpx" text="加载中..." />
</view>
<view class="history-no-more" wx:if="{{!historyHasMore && historyList.length > 0}}">没有更多内容</view>
<view class="history-no-more" wx:if="{{!historyHasMore && historyList.length === 0}}">暂无历史记录</view>
</view>
</scroll-view>
</t-drawer>
<view wx:if="{{isRecording}}" class="microphone-mask">
<view class="microphone-bars">
<view class="microphone-bar"></view>
<view class="microphone-bar"></view>
<view class="microphone-bar"></view>
<view class="microphone-bar"></view>
<view class="microphone-bar"></view>
<view class="microphone-bar"></view>
<view class="microphone-bar"></view>
<view class="microphone-bar"></view>
<view class="microphone-bar"></view>
<view class="microphone-bar"></view>
</view>
</view>
<view class="chat-sender-wrapper {{isChatInputVisible ? 'show' : ''}}" style="bottom: {{inputBottom}}px" wx:if="{{questionMode === 'conversation' && conversationViewMode === 'chat'}}">
<view class="suggestion-bar {{showChatSuggestions ? 'show' : ''}}"
wx:if="{{chatSuggestions.length > 0 && !isMicrophoneMode}}"
bind:touchstart="handleSuggestionTouchStart"
bind:touchend="handleSuggestionTouchEnd">
<scroll-view scroll-x class="suggestion-scroll" enable-flex>
<view class="suggestion-item" wx:for="{{chatSuggestions}}" wx:key="index" bindtap="handleSuggestionTap" data-text="{{item.text}}">
{{item.label}}
</view>
</scroll-view>
</view>
<t-chat-sender
wx:if="{{!isMicrophoneMode}}"
value="{{chatInputValue}}"
placeholder="{{chatPlaceholder}}"
loading="{{replyLoading}}"
focus="{{isChatInputVisible}}"
renderPresets="{{renderPresets}}"
bind:send="onSendMessage"
bind:input="onChatInput"
bind:blur="onChatBlur"
>
<view slot="footer-prefix" class="footer-prefix">
<view class="chat-icon-block" bind:tap="onChatCloseTap">
<t-icon name="close-circle" size="64rpx" color="#dcdcdc"/>
</view>
<!-- <view class="chat-icon-block" bind:tap="switchToVoiceMode">
<t-icon name="microphone-1" size="64rpx" color="#dcdcdc"/>
</view> -->
</view>
</t-chat-sender>
<view class="voice-mode-panel" wx:if="{{isMicrophoneMode}}">
<view class="voice-panel-controls">
<view class="chat-icon-block" bind:tap="switchToTextMode">
<t-icon name="keyboard" size="64rpx" color="#666"/>
</view>
<view class="microphone-btn {{isRecording ? 'recording' : ''}}"
bind:touchstart="handleRecordStart"
bind:touchend="handleRecordEnd">
{{isRecording ? '松开发送' : '按住说话'}}
</view>
<view class="chat-icon-block" bind:tap="onChatCloseTap">
<t-icon name="close-circle" size="64rpx" color="#666"/>
</view>
</view>
</view>
</view>
<view class="bottom-bar {{contentVisible && !isChatInputVisible ? 'show' : ''}}" wx:if="{{questionMode === 'conversation'}}">
<view class="bottom-btn bottom-button-img-wrap" bind:tap="toggleConversationSceneLang">
<t-icon name="translate" class="trans-button left-half {{conversationSceneLang === 'en' ? 'trans-active' : 'trans-deactive'}}" size="48rpx" />
<t-icon name="translate" class="trans-button right-half {{conversationSceneLang === 'zh' ? 'trans-active' : 'trans-deactive'}}" size="48rpx" />
</view>
<t-icon name="microphone-1" class="bottom-btn {{conversationLatestSession && conversationViewMode !== 'setup' ? '' : 'disabled'}}" size="48rpx" bind:tap="switchToVoiceMode" />
<t-icon name="keyboard" class="bottom-btn {{conversationLatestSession && conversationViewMode !== 'setup' ? '' : '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}}"
expanded="{{showDictExtended}}"
loading="{{dictLoading}}"
wordDict="{{wordDict}}"
showBackIcon="{{showBackIcon}}"
prototypeWord="{{prototypeWord}}"
forceHidePrototype="{{forceHidePrototype}}"
isWordEmptyResult="{{isWordEmptyResult}}"
dictDefaultTabValue="{{dictDefaultTabValue}}"
activeWordAudioType="{{activeWordAudioType}}"
wordAudioPlaying="{{wordAudioPlaying}}"
wordAudioIconName="{{wordAudioIconName}}"
bind:close="handleDictClose"
bind:more="handleDictMore"
bind:tabsChange="onTabsChange"
bind:tabsClick="onTabsClick"
bind:back="handleBackToPreviousWord"
bind:wordTap="handleWordClick"
/>
<view class="word-popup-mask" wx:if="{{showDictPopup}}" bindtap="handleDictClose"></view>
<view class="qa-detail-overlay" wx:if="{{qaDetailVisible}}" bindtap="onCloseDetailModal"></view>
<view class="qa-detail-modal {{qaDetailVisible ? 'show' : ''}}" wx:if="{{qaDetailVisible}}">
<view class="modal-header">
<text class="modal-title">题目解析</text>
<t-icon name="close" class="modal-close" size="40rpx" bind:tap="onCloseDetailModal" />
</view>
<scroll-view class="detail-body" scroll-y="{{true}}">
<view class="section">
<text class="question-text">{{qaDetailQuestionText}}</text>
</view>
<view class="section">
<view class="overview-card {{qaDetailResultStatus}}">
<view class="overview-icon">
<t-icon name="{{qaDetailIconName}}" size="40rpx" color="#fff" />
</view>
<view class="overview-content">
<text class="overview-label">评估详情 (EVALUATION DETAIL)</text>
<text class="overview-text">{{qaDetailOverviewText}}</text>
</view>
</view>
</view>
<block wx:for="{{qaDetailBlocks}}" wx:key="index">
<view class="section" wx:if="{{item.items && item.items.length}}">
<!-- <text class="section-title">{{item.title}}</text> -->
<view wx:if="{{item.variant === 'incorrect'}}" class="detail-row incorrect">
<view class="detail-icon">
<t-icon name="{{item.iconName || 'data-error' }}" size="40rpx" color="#fff" />
</view>
<view class="detail-content">
<text class="detail-label">错误选项</text>
<view class="detail-items incorrect-list">
<view class="incorrect-item" wx:for="{{item.items}}" wx:key="index">
<view class="incorrect-head">
<view class="chip incorrect">{{item.content}}</view>
<text class="error-type">{{item.error_type}}</text>
</view>
<text class="error-reason">{{item.error_reason}}</text>
</view>
</view>
</view>
</view>
<view wx:if="{{item.variant === 'info'}}" class="detail-row your-choice {{qaDetailResultStatus}}">
<view class="detail-icon">
<t-icon name="{{item.iconName || 'info-circle' }}" size="40rpx" color="#fff" />
</view>
<view class="detail-content">
<text class="detail-label">{{item.title}}</text>
<view class="detail-items">
<text class="detail-item" wx:for="{{item.items}}" wx:key="index">{{item}}</text>
</view>
</view>
</view>
<view wx:if="{{item.variant !== 'incorrect' && item.variant !== 'info'}}" class="detail-row {{item.variant}}">
<view class="detail-icon">
<t-icon name="{{item.iconName || 'info-circle' }}" size="40rpx" color="#fff" />
</view>
<view class="detail-content">
<text class="detail-label">{{item.title}}</text>
<view class="detail-items">
<text class="detail-item" wx:for="{{item.items}}" wx:key="index">{{item}}</text>
</view>
</view>
</view>
</view>
</block>
</scroll-view>
</view>
<view class="completion-popup-mask" wx:if="{{showCompletionPopup}}">
<view class="completion-card">
<view class="completion-close" bindtap="handleCompletionPopupClose">
<t-icon name="close" size="48rpx" color="#ccc" />
</view>
<view class="completion-trophy">
<view class="trophy-circle">
<t-icon name="thumb-up-2" size="80rpx" color="#001858" />
</view>
<view class="confetti c1"></view>
<view class="confetti c2"></view>
<view class="confetti c3"></view>
</view>
<view class="completion-title">Excellent Job!</view>
<!-- <view class="completion-subtitle">You've mastered the vocabulary for this photo scene.</view> -->
<button class="completion-share-btn" open-type="share" data-type="achievement">
<t-icon name="share" size="36rpx" color="#fff" style="margin-right: 12rpx;" />
Share Achievement
</button>
</view>
</view>
<vx-confetti id="confetti" class="confetti-canvas {{showConfetti ? 'show' : ''}}" width="{{canvasWidth}}" height="{{canvasHeight}}"></vx-confetti>
</view>