|
|
@@ -1,514 +0,0 @@
|
|
|
-import { Component, OnInit, OnDestroy, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
|
|
-import { CommonModule } from '@angular/common';
|
|
|
-import { FormsModule } from '@angular/forms';
|
|
|
-import { DaofaService } from '../../../services/daofa.service';
|
|
|
-import { FmodeObject, NovaUploadService } from 'fmode-ng';
|
|
|
-import { FmodeLoadingController, FmodeLoadingInstance, TipsController } from 'fmode-ng/lib/core/agent';
|
|
|
-import Parse from 'parse';
|
|
|
-
|
|
|
-interface QuestionData {
|
|
|
- id?: string;
|
|
|
- title: string;
|
|
|
- content: string;
|
|
|
- questionType: string;
|
|
|
- material?:string;
|
|
|
- options?: { label: string; value: string; check?: boolean }[];
|
|
|
- answer?: string;
|
|
|
- keywords?: string[];
|
|
|
-}
|
|
|
-
|
|
|
-@Component({
|
|
|
- selector: 'app-search',
|
|
|
- standalone: true,
|
|
|
- imports: [CommonModule, FormsModule],
|
|
|
- templateUrl: './search.component.html',
|
|
|
- styleUrl: './search.component.scss',
|
|
|
- schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
|
|
-})
|
|
|
-export class SearchComponent implements OnInit, OnDestroy {
|
|
|
- // 上传相关
|
|
|
- uploadedImages: string[] = [];
|
|
|
- isUploading: boolean = false;
|
|
|
- uploadProgressList: number[] = [];
|
|
|
- uploadError: string = '';
|
|
|
- minImagesRequired: number = 1;
|
|
|
- maxImagesAllowed: number = 3;
|
|
|
-
|
|
|
- // 识别和解析相关
|
|
|
- isRecognizing: boolean = false;
|
|
|
- isGenerating: boolean = false;
|
|
|
- recognitionProgress: string = '';
|
|
|
-
|
|
|
- // 题目数据
|
|
|
- surveyItem: FmodeObject | null = null;
|
|
|
- questionData: QuestionData = {
|
|
|
- title: '',
|
|
|
- content: '',
|
|
|
- questionType: '',
|
|
|
- options: [],
|
|
|
- answer: '',
|
|
|
- keywords: []
|
|
|
- };
|
|
|
-
|
|
|
- // 解析展示相关
|
|
|
- showAnswer: boolean = false;
|
|
|
- showAnalysis: boolean = false;
|
|
|
- showKnowledgeExpansion: boolean = false;
|
|
|
-
|
|
|
- // 问答相关
|
|
|
- showQASection: boolean = false;
|
|
|
- userQuestion: string = '';
|
|
|
- qaHistory: { question: string; answer: string; isAnswering?: boolean }[] = [];
|
|
|
- isAnswering: boolean = false;
|
|
|
-
|
|
|
- // 常见问题快捷按钮
|
|
|
- quickQuestions: string[] = [
|
|
|
- '这个知识点在教材哪一课?',
|
|
|
- '为什么这个选项是错的?',
|
|
|
- '有没有相似的题目?',
|
|
|
- '如何记忆这个知识点?'
|
|
|
- ];
|
|
|
-
|
|
|
- // 骨架屏相关
|
|
|
- showSkeleton: boolean = false;
|
|
|
- skeletonSections = [
|
|
|
- { name: 'questionType', progress: 0 },
|
|
|
- { name: 'content', progress: 0 },
|
|
|
- { name: 'options', progress: 0 }
|
|
|
- ];
|
|
|
-
|
|
|
- // Loading和Tips
|
|
|
- loading: FmodeLoadingInstance | null = null;
|
|
|
- loadCtrl: FmodeLoadingController = new FmodeLoadingController();
|
|
|
- tipsController: TipsController | null = null;
|
|
|
-
|
|
|
- tipsList: string[] = [
|
|
|
- '正在查阅相关法律条文...',
|
|
|
- '正在关联教材知识点...',
|
|
|
- 'AI正在深度理解题意...',
|
|
|
- '正在生成专业解析...',
|
|
|
- '马上为你呈现答案...',
|
|
|
- '分析题目考查要点中...',
|
|
|
- '理解题目逻辑关系中...'
|
|
|
- ];
|
|
|
-
|
|
|
- // 时间统计
|
|
|
- startTime: number = 0;
|
|
|
- recognitionTime: number = 0;
|
|
|
- surveyLogId: string = '';
|
|
|
-
|
|
|
- constructor(
|
|
|
- private daofaService: DaofaService,
|
|
|
- private uploadService: NovaUploadService
|
|
|
- ) {}
|
|
|
-
|
|
|
- ngOnInit() {
|
|
|
- this.startTime = Date.now();
|
|
|
- }
|
|
|
-
|
|
|
- ngOnDestroy() {
|
|
|
- this.loading?.dismiss();
|
|
|
- // 更新浏览时长
|
|
|
- if (this.surveyLogId) {
|
|
|
- const duration = Math.floor((Date.now() - this.startTime) / 1000);
|
|
|
- this.daofaService.updateSurveyLog(this.surveyLogId, { duration });
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 触发文件选择
|
|
|
- */
|
|
|
- triggerFileInput() {
|
|
|
- const fileInput = document.getElementById('fileInput') as HTMLInputElement;
|
|
|
- fileInput?.click();
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 文件选择事件
|
|
|
- */
|
|
|
- onFileSelect(event: Event) {
|
|
|
- const input = event.target as HTMLInputElement;
|
|
|
- if (input.files && input.files.length > 0) {
|
|
|
- const files = Array.from(input.files);
|
|
|
-
|
|
|
- // 检查是否超过最大图片数量限制
|
|
|
- const remainingSlots = this.maxImagesAllowed - this.uploadedImages.length;
|
|
|
- if (remainingSlots <= 0) {
|
|
|
- this.uploadError = `最多只能上传${this.maxImagesAllowed}张图片`;
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // 如果选择的文件数量超过剩余槽位,只取前面的文件
|
|
|
- const filesToUpload = files.slice(0, remainingSlots);
|
|
|
- if (files.length > remainingSlots) {
|
|
|
- this.uploadError = `只能再上传${remainingSlots}张图片,已自动选择前${remainingSlots}张`;
|
|
|
- }
|
|
|
-
|
|
|
- this.uploadMultipleImages(filesToUpload);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 上传多张图片
|
|
|
- */
|
|
|
- async uploadMultipleImages(files: File[]) {
|
|
|
- try {
|
|
|
- this.isUploading = true;
|
|
|
- this.uploadProgressList = new Array(files.length).fill(0);
|
|
|
- this.uploadError = '';
|
|
|
-
|
|
|
- const uploadPromises = files.map(async (file, index) => {
|
|
|
- const fileResult = await this.uploadService.upload(file, (progress: any) => {
|
|
|
- const currentFileProgress = progress.percent || 0;
|
|
|
- this.uploadProgressList[index] = Math.round(currentFileProgress);
|
|
|
- });
|
|
|
- return fileResult.url;
|
|
|
- });
|
|
|
-
|
|
|
- const uploadedUrls = await Promise.all(uploadPromises);
|
|
|
- this.uploadedImages = [...this.uploadedImages, ...uploadedUrls];
|
|
|
-
|
|
|
- this.isUploading = false;
|
|
|
- this.uploadProgressList = [];
|
|
|
-
|
|
|
- // 自动开始识别
|
|
|
- if (this.uploadedImages.length >= this.minImagesRequired) {
|
|
|
- await this.startRecognition();
|
|
|
- }
|
|
|
-
|
|
|
- } catch (error) {
|
|
|
- console.error('Upload failed:', error);
|
|
|
- this.isUploading = false;
|
|
|
- this.uploadProgressList = [];
|
|
|
- this.uploadError = '上传失败,请重试';
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 移除图片
|
|
|
- */
|
|
|
- removeImage(index: number) {
|
|
|
- this.uploadedImages.splice(index, 1);
|
|
|
-
|
|
|
- // 如果移除后需要重新识别
|
|
|
- if (this.surveyItem && this.uploadedImages.length >= this.minImagesRequired) {
|
|
|
- // 可以选择重新识别或保持当前结果
|
|
|
- } else if (this.uploadedImages.length < this.minImagesRequired) {
|
|
|
- // 重置状态
|
|
|
- this.resetState();
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 开始识别题目
|
|
|
- */
|
|
|
- async startRecognition() {
|
|
|
- try {
|
|
|
- this.isRecognizing = true;
|
|
|
- this.showSkeleton = true;
|
|
|
- this.loadTips();
|
|
|
- await this.presentLoading({ message: '正在识别题目内容...' });
|
|
|
-
|
|
|
- const recognitionStartTime = Date.now();
|
|
|
-
|
|
|
- // 调用识别服务
|
|
|
- this.surveyItem = await this.daofaService.recognizeQuestion({
|
|
|
- images: this.uploadedImages,
|
|
|
- onProgressChange: (progress) => {
|
|
|
- this.recognitionProgress = progress;
|
|
|
- if (this.loading) {
|
|
|
- this.loading.message = progress;
|
|
|
- }
|
|
|
- },
|
|
|
- loading: this.loading
|
|
|
- });
|
|
|
-
|
|
|
- this.recognitionTime = (Date.now() - recognitionStartTime) / 1000;
|
|
|
-
|
|
|
- // 更新题目数据 - 此时就有题目内容了
|
|
|
- this.updateQuestionData();
|
|
|
-
|
|
|
- // 关闭loading,显示题目
|
|
|
- this.loading?.dismiss();
|
|
|
-
|
|
|
- // 模拟骨架屏逐步展开
|
|
|
- await this.animateSkeleton();
|
|
|
-
|
|
|
- this.isRecognizing = false;
|
|
|
- this.showSkeleton = false;
|
|
|
-
|
|
|
- // 保存搜题记录
|
|
|
- const log = await this.daofaService.saveSurveyLog({
|
|
|
- surveyItem: this.surveyItem!,
|
|
|
- searchMode: 'image-upload',
|
|
|
- uploadedImages: this.uploadedImages,
|
|
|
- recognitionTime: this.recognitionTime,
|
|
|
- viewedSections: ['recognition']
|
|
|
- });
|
|
|
- this.surveyLogId = log.id;
|
|
|
-
|
|
|
- // 立即开始生成解析(不等待,让题目先显示)
|
|
|
- this.generateAnalysis().catch(err => {
|
|
|
- console.error('Generate analysis error:', err);
|
|
|
- });
|
|
|
-
|
|
|
- } catch (error) {
|
|
|
- console.error('Recognition failed:', error);
|
|
|
- this.isRecognizing = false;
|
|
|
- this.showSkeleton = false;
|
|
|
- this.uploadError = error.message || '识别失败,请重试';
|
|
|
- } finally {
|
|
|
- this.loading?.dismiss();
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 生成题目解析
|
|
|
- */
|
|
|
- async generateAnalysis() {
|
|
|
- try {
|
|
|
- this.isGenerating = true;
|
|
|
- await this.presentLoading({ message: '正在生成专业解析...' });
|
|
|
-
|
|
|
- await this.daofaService.generateAnswer({
|
|
|
- surveyItem: this.surveyItem!,
|
|
|
- onContentChange: (content) => {
|
|
|
- // 实时更新答案内容
|
|
|
- this.questionData.answer = content;
|
|
|
-
|
|
|
- // 逐步展开解析内容
|
|
|
- if (!this.showAnswer && content.length > 20) {
|
|
|
- this.showAnswer = true;
|
|
|
- }
|
|
|
- if (!this.showAnalysis && content.length > 100) {
|
|
|
- setTimeout(() => { this.showAnalysis = true; }, 500);
|
|
|
- }
|
|
|
- if (!this.showKnowledgeExpansion && content.length > 300) {
|
|
|
- setTimeout(() => { this.showKnowledgeExpansion = true; }, 1000);
|
|
|
- }
|
|
|
- },
|
|
|
- loading: this.loading
|
|
|
- });
|
|
|
-
|
|
|
- // 生成完成后,从surveyItem同步最新数据
|
|
|
- this.updateQuestionData();
|
|
|
-
|
|
|
- // 显示问答区域
|
|
|
- setTimeout(() => {
|
|
|
- this.showQASection = true;
|
|
|
- }, 1500);
|
|
|
-
|
|
|
- this.isGenerating = false;
|
|
|
-
|
|
|
- // 更新搜题记录
|
|
|
- if (this.surveyLogId) {
|
|
|
- await this.daofaService.updateSurveyLog(this.surveyLogId, {
|
|
|
- viewCount: 1,
|
|
|
- questions: []
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- } catch (error) {
|
|
|
- console.error('Generate analysis failed:', error);
|
|
|
- this.isGenerating = false;
|
|
|
- this.uploadError = error.message || '生成解析失败,请重试';
|
|
|
- } finally {
|
|
|
- this.loading?.dismiss();
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 更新题目数据
|
|
|
- */
|
|
|
- updateQuestionData() {
|
|
|
- if (!this.surveyItem) return;
|
|
|
-
|
|
|
- this.questionData = {
|
|
|
- id: this.surveyItem.id,
|
|
|
- title: this.surveyItem.get('title') || '',
|
|
|
- material: this.surveyItem.get('createOptions')?.params?.material || '',
|
|
|
- content: this.surveyItem.get('content') || '',
|
|
|
- questionType: this.surveyItem.get('createOptions')?.params?.questionType || '',
|
|
|
- options: this.surveyItem.get('options') || [],
|
|
|
- answer: this.surveyItem.get('answer') || '',
|
|
|
- keywords: this.surveyItem.get('keywords') || []
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 骨架屏动画
|
|
|
- */
|
|
|
- async animateSkeleton() {
|
|
|
- // 模拟逐步加载效果
|
|
|
- for (let i = 0; i < this.skeletonSections.length; i++) {
|
|
|
- await new Promise(resolve => setTimeout(resolve, 300));
|
|
|
- this.skeletonSections[i].progress = 100;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 发送问题
|
|
|
- */
|
|
|
- async askQuestion(question?: string) {
|
|
|
- const questionText = question || this.userQuestion.trim();
|
|
|
-
|
|
|
- if (!questionText || !this.surveyItem) return;
|
|
|
-
|
|
|
- // 添加到问答历史
|
|
|
- this.qaHistory.push({
|
|
|
- question: questionText,
|
|
|
- answer: '',
|
|
|
- isAnswering: true
|
|
|
- });
|
|
|
-
|
|
|
- const qaIndex = this.qaHistory.length - 1;
|
|
|
- this.isAnswering = true;
|
|
|
- this.userQuestion = '';
|
|
|
-
|
|
|
- try {
|
|
|
- await this.daofaService.handleQuestion({
|
|
|
- parentQuestion: this.surveyItem,
|
|
|
- userQuestion: questionText,
|
|
|
- onAnswerChange: (answer) => {
|
|
|
- // 实时更新回答内容
|
|
|
- this.qaHistory[qaIndex].answer = answer;
|
|
|
- this.qaHistory[qaIndex].isAnswering = false;
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- this.qaHistory[qaIndex].isAnswering = false;
|
|
|
- this.isAnswering = false;
|
|
|
-
|
|
|
- // 更新搜题记录
|
|
|
- if (this.surveyLogId) {
|
|
|
- const qaCount = this.qaHistory.length;
|
|
|
- const questions = this.qaHistory.map((qa, idx) => ({
|
|
|
- questionId: `qa_${idx}`,
|
|
|
- question: qa.question,
|
|
|
- timestamp: new Date().toISOString()
|
|
|
- }));
|
|
|
-
|
|
|
- await this.daofaService.updateSurveyLog(this.surveyLogId, {
|
|
|
- qaCount: qaCount,
|
|
|
- questions: questions
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- } catch (error) {
|
|
|
- console.error('Ask question failed:', error);
|
|
|
- this.qaHistory[qaIndex].answer = '回答失败,请重试';
|
|
|
- this.qaHistory[qaIndex].isAnswering = false;
|
|
|
- this.isAnswering = false;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 快捷问题点击
|
|
|
- */
|
|
|
- selectQuickQuestion(question: string) {
|
|
|
- this.askQuestion(question);
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 重置状态
|
|
|
- */
|
|
|
- resetState() {
|
|
|
- this.surveyItem = null;
|
|
|
- this.questionData = {
|
|
|
- title: '',
|
|
|
- content: '',
|
|
|
- material: '',
|
|
|
- questionType: '',
|
|
|
- options: [],
|
|
|
- answer: '',
|
|
|
- keywords: []
|
|
|
- };
|
|
|
- this.showAnswer = false;
|
|
|
- this.showAnalysis = false;
|
|
|
- this.showKnowledgeExpansion = false;
|
|
|
- this.showQASection = false;
|
|
|
- this.qaHistory = [];
|
|
|
- this.recognitionProgress = '';
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 重新上传
|
|
|
- */
|
|
|
- resetUpload() {
|
|
|
- this.uploadedImages = [];
|
|
|
- this.uploadError = '';
|
|
|
- this.resetState();
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * Loading相关
|
|
|
- */
|
|
|
- async presentLoading(options?: { message: string }) {
|
|
|
- this.loading = await this.loadCtrl.create({
|
|
|
- message: options?.message || '处理中...',
|
|
|
- position: 'bottom'
|
|
|
- });
|
|
|
-
|
|
|
- await this.loading.present();
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * Tips相关
|
|
|
- */
|
|
|
- loadTips() {
|
|
|
- this.tipsController = new TipsController({
|
|
|
- tipsList: this.tipsList,
|
|
|
- position: 'bottom',
|
|
|
- random: true
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 获取题型中文名称
|
|
|
- */
|
|
|
- getQuestionTypeName(type: string): string {
|
|
|
- const typeMap: { [key: string]: string } = {
|
|
|
- 'single-choice': '单选题',
|
|
|
- 'multi-choice': '多选题',
|
|
|
- 'judge': '判断题',
|
|
|
- 'short-answer': '简答题',
|
|
|
- 'material-analysis': '材料分析题'
|
|
|
- };
|
|
|
- return typeMap[type] || type;
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 获取题型图标
|
|
|
- */
|
|
|
- getQuestionTypeIcon(type: string): string {
|
|
|
- const iconMap: { [key: string]: string } = {
|
|
|
- 'single-choice': '📋',
|
|
|
- 'multi-choice': '📑',
|
|
|
- 'judge': '✓✗',
|
|
|
- 'short-answer': '📝',
|
|
|
- 'material-analysis': '📚'
|
|
|
- };
|
|
|
- return iconMap[type] || '📋';
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 解析答案的各个部分
|
|
|
- */
|
|
|
- parseAnswerSection(sectionName: string): string {
|
|
|
- if (!this.questionData.answer) return '';
|
|
|
-
|
|
|
- const patterns: { [key: string]: RegExp } = {
|
|
|
- 'standard': /【标准答案】([\s\S]*?)(?=【|$)/,
|
|
|
- 'knowledge': /【知识点】([\s\S]*?)(?=【|$)/,
|
|
|
- 'thinking': /【解题思路】([\s\S]*?)(?=【|$)/,
|
|
|
- 'mistakes': /【易错点】([\s\S]*?)(?=【|$)/,
|
|
|
- 'expansion': /【知识拓展】([\s\S]*?)(?=【|$)/
|
|
|
- };
|
|
|
-
|
|
|
- const pattern = patterns[sectionName];
|
|
|
- if (!pattern) return '';
|
|
|
-
|
|
|
- const match = this.questionData.answer.match(pattern);
|
|
|
- return match ? match[1].trim() : '';
|
|
|
- }
|
|
|
-}
|