Browse Source

Merge branch 'beta'

lsc 1 year ago
parent
commit
de7c1a7911
33 changed files with 1409 additions and 5 deletions
  1. 1 1
      dist/index.html
  2. 0 0
      dist/static/css/app.7e26ddf9c0ba16f0750abd618cc9f533.css
  3. 0 0
      dist/static/css/app.7e26ddf9c0ba16f0750abd618cc9f533.css.map
  4. BIN
      dist/static/img/aiTeacherAvatar.802a64b.png
  5. BIN
      dist/static/img/top_pbl.ea9d02c.png
  6. BIN
      dist/static/img/userAvatar.6808cf3.png
  7. 0 0
      dist/static/js/app.892e3761b6f84582897c.js
  8. 0 0
      dist/static/js/app.892e3761b6f84582897c.js.map
  9. 0 0
      dist/static/js/manifest.571c38d63f24b1ae9e16.js.map
  10. BIN
      src/assets/icon/pblCourse/aiTeacherAvatar.png
  11. BIN
      src/assets/icon/pblCourse/backIcon.png
  12. BIN
      src/assets/icon/pblCourse/bookIcon.png
  13. BIN
      src/assets/icon/pblCourse/changeIcon.png
  14. BIN
      src/assets/icon/pblCourse/copyIcon.png
  15. BIN
      src/assets/icon/pblCourse/doWorkIcon.png
  16. BIN
      src/assets/icon/pblCourse/fileIcon.png
  17. BIN
      src/assets/icon/pblCourse/phaseIcon.png
  18. BIN
      src/assets/icon/pblCourse/recordIcon.png
  19. BIN
      src/assets/icon/pblCourse/sendIcon.png
  20. BIN
      src/assets/icon/pblCourse/taskIcon.png
  21. BIN
      src/assets/icon/pblCourse/top_book.png
  22. BIN
      src/assets/icon/pblCourse/top_book_active.png
  23. BIN
      src/assets/icon/pblCourse/top_pbl.png
  24. BIN
      src/assets/icon/pblCourse/userAvatar.png
  25. 1 1
      src/components/pages/classroomObservation/components/analysisItem.vue
  26. 1 1
      src/components/pages/classroomObservation/components/chatArea.vue
  27. 2 2
      src/components/pages/classroomObservation/components/startPage.vue
  28. 538 0
      src/components/pages/pblCourse/component/chatArea.vue
  29. 182 0
      src/components/pages/pblCourse/component/doWorkArea.vue
  30. 158 0
      src/components/pages/pblCourse/component/procedureArea.vue
  31. 293 0
      src/components/pages/pblCourse/component/work.vue
  32. 224 0
      src/components/pages/pblCourse/index.vue
  33. 9 0
      src/router/index.js

+ 1 - 1
dist/index.html

@@ -32,7 +32,7 @@
       width: 100%;
       background: #e6eaf0;
       font-family: '黑体';
-    }</style><link href=./static/css/app.a06a18e95256a854064ce808cc8889d7.css rel=stylesheet></head><body><div id=app></div><script type=text/javascript src=./static/js/manifest.571c38d63f24b1ae9e16.js></script><script type=text/javascript src=./static/js/vendor.3cd0a0187ca1f70ded67.js></script><script type=text/javascript src=./static/js/app.c991c0447e92f0e6873f.js></script></body></html><script>function stopSafari() {
+    }</style><link href=./static/css/app.7e26ddf9c0ba16f0750abd618cc9f533.css rel=stylesheet></head><body><div id=app></div><script type=text/javascript src=./static/js/manifest.571c38d63f24b1ae9e16.js></script><script type=text/javascript src=./static/js/vendor.3cd0a0187ca1f70ded67.js></script><script type=text/javascript src=./static/js/app.892e3761b6f84582897c.js></script></body></html><script>function stopSafari() {
     //阻止safari浏览器双击放大功能
     let lastTouchEnd = 0  //更新手指弹起的时间
     document.documentElement.addEventListener("touchstart", function (event) {

File diff suppressed because it is too large
+ 0 - 0
dist/static/css/app.7e26ddf9c0ba16f0750abd618cc9f533.css


File diff suppressed because it is too large
+ 0 - 0
dist/static/css/app.7e26ddf9c0ba16f0750abd618cc9f533.css.map


BIN
dist/static/img/aiTeacherAvatar.802a64b.png


BIN
dist/static/img/top_pbl.ea9d02c.png


BIN
dist/static/img/userAvatar.6808cf3.png


File diff suppressed because it is too large
+ 0 - 0
dist/static/js/app.892e3761b6f84582897c.js


File diff suppressed because it is too large
+ 0 - 0
dist/static/js/app.892e3761b6f84582897c.js.map


File diff suppressed because it is too large
+ 0 - 0
dist/static/js/manifest.571c38d63f24b1ae9e16.js.map


BIN
src/assets/icon/pblCourse/aiTeacherAvatar.png


BIN
src/assets/icon/pblCourse/backIcon.png


BIN
src/assets/icon/pblCourse/bookIcon.png


BIN
src/assets/icon/pblCourse/changeIcon.png


BIN
src/assets/icon/pblCourse/copyIcon.png


BIN
src/assets/icon/pblCourse/doWorkIcon.png


BIN
src/assets/icon/pblCourse/fileIcon.png


BIN
src/assets/icon/pblCourse/phaseIcon.png


BIN
src/assets/icon/pblCourse/recordIcon.png


BIN
src/assets/icon/pblCourse/sendIcon.png


BIN
src/assets/icon/pblCourse/taskIcon.png


BIN
src/assets/icon/pblCourse/top_book.png


BIN
src/assets/icon/pblCourse/top_book_active.png


BIN
src/assets/icon/pblCourse/top_pbl.png


BIN
src/assets/icon/pblCourse/userAvatar.png


+ 1 - 1
src/components/pages/classroomObservation/components/analysisItem.vue

@@ -1,6 +1,6 @@
 <template>
 	<div class="analysisItem">
-		<div class="ai-header">
+		<div class="ai-header" v-show="data.jsonData.name != '词频词汇分析'">
 			<div class="ai-h-left" @click.stop="changeOpenItem(!openItem)">
 				<span
 					:class="['ai-h-l-icon', openItem ? 'ai-h-l-iconActive' : '']"

+ 1 - 1
src/components/pages/classroomObservation/components/chatArea.vue

@@ -123,7 +123,7 @@
 							@click.stop="uploadRecording()"
 							v-loading="uploadFileLoading"
 						>
-							<div class="ca-b-o-h-b-l-text">上传录音</div>
+							<div class="ca-b-o-h-b-l-text">上传文件</div>
 						</div>
 					</div>
 					<div class="ca-b-o-h-right">

+ 2 - 2
src/components/pages/classroomObservation/components/startPage.vue

@@ -38,9 +38,9 @@
 			</div>
 			<div class="sp-m-item" @click.stop="$emit('uploadTape')">
 				<!-- <span class="sp-m-i-icon2"></span> -->
-				<div class="sp-m-item1">上传录音</div>
+				<div class="sp-m-item1">上传文件</div>
 				<div class="sp-m-item2">
-					<p>录音复盘</p>
+					<p>上传录音或实录文稿</p>
 					<p>一键分析课堂情况</p>
 				</div>
 				<div  class="sp-m-item3">

+ 538 - 0
src/components/pages/pblCourse/component/chatArea.vue

@@ -0,0 +1,538 @@
+<template>
+	<div class="chat" v-loading="loading">
+		<div class="c_chat" ref="chatRef">
+			<div class="c_c_item" v-for="item in chatList" :key="item.uid">
+				<div class="c_c_i_user" v-if="item.content">
+					<div class="c_c_i_u_message">
+						<div class="c_c_i_u_m_top">
+							<span class="chatTime">{{ item.createtime }}</span>
+							<span>科科</span>
+						</div>
+						<div class="c_c_i_u_m_bottom">
+							<span class="coptText" @click="copyText(item.aiContent)"></span>
+							<div
+								class="c_c_i_u_m_content chatContent"
+								v-html="item.content"
+							></div>
+						</div>
+					</div>
+					<div class="c_c_i_u_avatar">
+						<el-avatar
+							style="width: 100%; height: 100%"
+							:src="require('../../../../assets/icon/pblCourse/userAvatar.png')"
+							fit="fit"
+						></el-avatar>
+					</div>
+				</div>
+				<div class="c_c_i_ai">
+					<div class="c_c_i_ai_avatar">
+						<el-avatar
+							style="width: 100%; height: 100%"
+							:src="
+								require('../../../../assets/icon/pblCourse/aiTeacherAvatar.png')
+							"
+							fit="fit"
+						></el-avatar>
+					</div>
+					<div class="c_c_i_ai_message">
+						<div class="c_c_i_ai_m_top">
+							<span>小可老师</span>
+							<span class="chatTime">{{item.createtime}}</span>
+						</div>
+						<div class="c_c_i_ai_m_bottom">
+							<div
+								v-loading="item.loading"
+								class="c_c_i_ai_m_content chatContent"
+								v-html="item.aiContent"
+							></div>
+							<span class="coptText" @click="copyText(item.aiContent)"></span>
+						</div>
+					</div>
+				</div>
+			</div>
+		</div>
+		<!-- <div class="c_controls">
+			<span class="c_controls_item" @click="clearChat">清空对话</span>
+		</div> -->
+		<div class="c_bottom" 	v-loading="chatLoading">
+			<div class="c_b_record">
+				<span></span>
+			</div>
+			<div class="c_b_inputArea">
+				<el-input
+					class="c_b_input"
+					@keyup.enter.native="send()"
+					v-model="textValue"
+				
+				>
+				</el-input>
+				<span></span>
+			</div>
+			<div class="c_b_send" @click="send">
+				<span></span>
+			</div>
+		</div>
+	</div>
+</template>
+
+<script>
+import { v4 as uuidv4 } from "uuid";
+import MarkdownIt from "markdown-it";
+export default {
+	data() {
+		return {
+			loading: false,
+			chatLoading:false,
+			userId:this.$route.query['userid'],
+			chatList: [],
+			textValue: "",
+		};
+	},
+	methods: {
+		// 发送消息
+		send() {
+			if(this.chatLoading || this.loading)return this.$message.info("请稍等...")
+			if(!this.textValue.trim())return this.$message.info("请输入内容");
+			const _uuid = uuidv4();
+			this.chatLoading = true;
+			this.chatList.push({
+				role: "user",
+				content: `${this.textValue}`,
+				uid: _uuid,
+				AI: "AI",
+				aiContent: "",
+				oldContent: "",
+				filename: "",
+				index: this.chatList.length,
+				createtime:new Date().toLocaleString().replaceAll("/",'-'),
+				loading: true,
+			})
+			
+			let params = {
+				model: "gpt-3.5-turbo",
+					temperature: 0,
+					max_tokens: 4096,
+					top_p: 1,
+					frequency_penalty: 0,
+					presence_penalty: 0,
+					messages: [{role:"user",content:this.textValue}],
+					uid: _uuid,
+					mind_map_question: "",
+			}
+			this.scrollBottom();
+			this.textValue = "";
+			this.ajax
+					.post("https://gpt4.cocorobo.cn/chat", params)
+					.then((res) => {
+						if (res.data.FunctionResponse.result == "发送成功") {
+						} else {
+							this.chatLoading = false;
+							this.$message.warning(res.data.FunctionResponse.result);
+						}
+					})
+					.catch((e) => {
+						this.chatLoading = false;
+						console.log(e);
+					});
+			// this.$message.info(`发送:${this.textValue}`);
+			// this.textValue = "";
+			this.getAiContent(_uuid)
+		},
+		getAiContent(_uid) {
+			let _source = new EventSource(`https://gpt4.cocorobo.cn/stream/${_uid}`); //http://gpt4.cocorobo.cn:8011/stream/     https://gpt4.cocorobo.cn/stream/
+			let _allText = "";
+			let _mdText = "";
+			const md = new MarkdownIt();
+			_source.onmessage = (_e) => {
+				if (_e.data.replace("'", "").replace("'", "") == "[DONE]") {
+					//对话已经完成
+					_mdText = _mdText.replace("_", "");
+					_source.close();
+					this.chatList.find((i) => i.uid == _uid).aiContent = _mdText;
+					this.chatList.find((i) => i.uid == _uid).isalltext = true;
+					this.chatList.find((i) => i.uid == _uid).isShowSynchronization = true;
+					this.chatList.find((i) => i.uid == _uid).loading = false;
+					this.chatLoading = false;
+					this.insertChat(_uid);
+					return;
+				} else {
+					//对话还在继续
+					let _text = "";
+					_text = _e.data.replaceAll("'", "");
+					if (_allText == "") {
+						_allText = _text.replace(/^\n+/, ""); //去掉回复消息中偶尔开头就存在的连续换行符
+					} else {
+						_allText += _text;
+					}
+					_mdText = _allText + "_";
+					_mdText = _mdText.replace(/\\n/g, "\n");
+					_mdText = _mdText.replace(/\\/g, "");
+					if (_allText.split("```").length % 2 == 0) _mdText += "\n```\n";
+					//转化返回的回复流数据
+					_mdText = md.render(_mdText);
+					this.chatList.find((i) => i.uid == _uid).aiContent = _mdText;
+					this.chatList.find((i) => i.uid == _uid).loading = false;
+					this.scrollBottom();
+					// 处理流数据
+				}
+			};
+		},
+		// 复制
+		copyText(_html) {
+			const div = document.createElement("div")
+			div.innerHTML = _html
+			const _text = div.innerText
+			navigator.clipboard.writeText(_text).then(_=>{
+			  this.$message.success("复制成功")
+			}, function(err) {
+				this.$message.error(`无法复制:${err}`)
+			});
+		},
+		// 保存对话
+		//保存消息
+		insertChat(_uid) {
+			let _data = this.chatList.find((i) => i.uid == _uid);
+			if (!_data) return;
+			let params = {
+				userId: this.userId,
+				userName: "qgt",
+				groupId: "602def61-005d-11ee-91d8-005056b8q12w",
+				answer: _data.aiContent,
+				problem: _data.content,
+				file_id: "",
+				alltext: _data.aiContent,
+				type: "chat",
+				filename: _data.filename,
+				session_name: `${this.userId}-pblCourse`,
+			};
+			this.ajax
+				.post("https://gpt4.cocorobo.cn/insert_chat", params)
+				.then((res) => {
+					console.log("保存对话")
+				});
+		},
+		// 获取对话记录
+		getChatList() {
+			return new Promise((resolve, reject) => {
+				this.chatList = [];
+				this.loading = true;
+				this.chatLoading = true;
+				let params = {
+					userid: this.userId,
+					groupid: "602def61-005d-11ee-91d8-005056b8q12w",
+					// session_name:``
+					session_name: `${this.userId}-pblCourse`,
+				};
+				this.ajax
+					.post("https://gpt4.cocorobo.cn/get_agent_park_chat", params)
+					.then((res) => {
+						let _data = JSON.parse(res.data.FunctionResponse);
+						if (_data.length > 0) {
+							console.log(_data)
+							let _chatList = [];
+							for (let i = 0; i < _data.length; i++) {
+								_chatList.push({
+									loading: false,
+									role: "user",
+									content: _data[i].problem,
+									uid: _data[i].id,
+									AI: "AI",
+									aiContent: _data[i].answer,
+									oldContent: _data[i].answer,
+									isShowSynchronization: false,
+									filename: _data[i].filename,
+									index: i,
+									is_mind_map: false,
+									fileid: _data[i].fileid,
+									createtime:_data[i].createtime
+								});
+							}
+							this.chatList = _chatList;
+							this.chatLoading = false;
+							this.loading = false;
+						} else {
+							//没有对话记录
+							this.loading = false;
+							this.chatLoading = false;
+						}
+						resolve();
+						this.scrollBottom();
+					})
+					.catch((err) => {
+						console.log(err);
+						this.$message.error("获取对话记录失败");
+						this.chatLoading = false;
+						this.scrollBottom();
+						resolve();
+					});
+			});
+		},
+		//对话触底
+		scrollBottom(){
+			this.$nextTick(()=>{
+				this.$refs.chatRef.scrollTop = this.$refs.chatRef.scrollHeight;
+			})
+		},
+		//清除对话
+		clearChat(){
+			this.chatList = [];
+			this.scrollBottom();
+		},
+	},
+
+	mounted() {
+		this.getChatList().then(_=>{
+			this.scrollBottom();
+		})
+	},
+};
+</script>
+
+<style scoped>
+.chat {
+	width: 100%;
+	height: 100%;
+	background-color: #ffffff;
+	border-radius: 12px;
+	box-sizing: border-box;
+	border: solid 1px #f3f7fd;
+	box-shadow: 0 4px 10px 0 #1d388321;
+}
+
+.c_chat {
+	width: 100%;
+	height: calc(100% - 56px);
+	box-sizing: border-box;
+	padding: 15px;
+	overflow: auto;
+}
+
+.c_c_item {
+	background-color: none;
+}
+
+.c_c_i_user {
+	width: 100%;
+	height: auto;
+	display: flex;
+	justify-content: flex-end;
+	align-items: flex-start;
+	margin-bottom: 10px;
+}
+
+.c_c_i_u_message {
+	height: auto;
+	width: auto;
+	max-width: calc(100% - 50px - 40px);
+	display: flex;
+	flex-direction: column;
+	align-items: flex-end;
+	margin-right: 5px;
+}
+.c_c_i_u_m_top {
+	margin-bottom: 10px;
+	display: flex;
+	align-items: flex-end;
+}
+
+.c_c_i_u_m_top > span {
+	margin-left: 5px;
+}
+
+.c_c_i_u_m_bottom {
+	display: flex;
+	align-items: flex-end;
+}
+
+.coptText {
+	min-width: 15px;
+	min-height: 15px;
+	display: block;
+	background: url("../../../../assets/icon/pblCourse/copyIcon.png") no-repeat;
+	background-size: 100% 100%;
+	margin: 0px 10px 6px 10px;
+	cursor: pointer;
+}
+
+.c_c_i_u_m_content {
+	width: auto;
+	height: auto;
+	padding: 10px;
+	background-color: #e2eeff;
+	border-radius: 8px 2px 8px 8px;
+	border: solid 1px #000000e5;
+}
+
+.c_c_i_u_avatar {
+	width: 45px;
+	height: 45px;
+	margin: 0px 0 0 10px;
+}
+
+.c_c_i_ai {
+	margin-top: 10px;
+	width: 100%;
+	height: auto;
+	display: flex;
+	align-items: flex-start;
+	margin-bottom: 10px;
+}
+
+.c_c_i_ai_avatar {
+	width: 45px;
+	height: 45px;
+	margin: 0px 0 0 10px;
+}
+
+.c_c_i_ai_message {
+	height: auto;
+	width: auto;
+	max-width: calc(100% - 50px - 40px);
+	display: flex;
+	flex-direction: column;
+	align-items: flex-start;
+	margin-left: 5px;
+}
+.c_c_i_ai_m_top {
+	display: flex;
+	align-items: flex-end;
+	margin-bottom: 10px;
+}
+
+.c_c_i_ai_m_top > span {
+	margin-left: 5px;
+}
+
+.c_c_i_ai_m_bottom {
+	display: flex;
+	align-items: flex-end;
+}
+
+.c_c_i_ai_m_content {
+	min-width: 40px;
+	min-height: 25px;
+	width: auto;
+	height: auto;
+	padding: 10px;
+	background-color: #ffffff;
+	border-radius: 2px 8px 8px 8px;
+	border: solid 1px #000000e5;
+}
+
+.chatContent >>> ol {
+	margin-left: 25px;
+}
+
+.chatContent >>> ul {
+	margin-left: 25px;
+}
+
+.chatTime{
+	font-size: 14px;
+	margin: 0 10px;
+	color: #2c2f3b;
+}
+
+.c_controls{
+	width: 100%;
+	height: 30px;
+}
+
+.c_controls>span{
+	max-height: 30px;
+	width: auto;
+	padding: 5px 10px;
+	font-size: 14px;
+	border-radius: 15px;
+	box-shadow: 0 0 2px 0px gray;
+	margin-left: 10px;
+	cursor: pointer;
+}
+
+.c_controls>span:hover{
+	background-color: #f3f7fd;
+}
+
+.c_bottom {
+	width: 100%;
+	height: 56px;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	box-sizing: border-box;
+	border-top: solid 0.5px #e7e7e7;
+}
+
+.c_b_record {
+	width: 30px;
+	height: 30px;
+	display: flex;
+	justify-content: center;
+	align-items: center;
+}
+
+.c_b_record > span {
+	width: 24px;
+	height: 24px;
+	background-image: url("../../../../assets/icon/pblCourse/recordIcon.png");
+	background-size: 100% 100%;
+	display: flex;
+	justify-content: center;
+	align-items: center;
+}
+
+.c_b_inputArea {
+	width: calc(100% - 140px);
+	background-color: #f3f3f3;
+	border-radius: 50px;
+	height: 80%;
+	display: flex;
+	justify-content: center;
+	align-items: center;
+	margin: 0 15px;
+}
+
+.c_b_input {
+	width: calc(100% - 40px);
+	margin-right: 10px;
+}
+
+.c_b_input >>> .el-input__inner {
+	border: none;
+	background-color: #f3f3f3;
+	outline: none;
+	border-radius: 50px 0 0 50px;
+	font-size: 1.2em;
+}
+
+.c_b_inputArea > span {
+	width: 24px;
+	height: 24px;
+	background-image: url("../../../../assets/icon/pblCourse/fileIcon.png");
+	background-size: 100% 100%;
+	display: flex;
+	justify-content: center;
+	align-items: center;
+	margin-right: 10px;
+	cursor: pointer;
+}
+
+.c_b_send {
+	width: 40px;
+	height: 40px;
+	border-radius: 50%;
+	background-color: #3681fc;
+	cursor: pointer;
+	display: flex;
+	justify-content: center;
+	align-items: center;
+	cursor: pointer;
+}
+
+.c_b_send > span {
+	width: 60%;
+	height: 60%;
+	background-image: url("../../../../assets/icon/pblCourse/sendIcon.png");
+	background-size: 100% 100%;
+}
+</style>

+ 182 - 0
src/components/pages/pblCourse/component/doWorkArea.vue

@@ -0,0 +1,182 @@
+<template>
+	<div class="doWork">
+		<div class="dw_header">
+			<div class="dw_h_left">
+				<img :src="require('../../../../assets/icon/pblCourse/phaseIcon.png')"> 
+				<span>阶段{{ phase.atPhase+1 }}:{{ task?task.name:'' }}</span>
+			</div>
+			<div class="dw_h_right">
+				<span class="dw_h_r_back" @click.stop="back()"></span>
+				<span class="dw_h_r_down" @click.stop="down()"></span>
+			</div>
+		</div>
+		<div class="dw_work">
+			<work :task="task" @submitTask="submitTask" @choiceAnswer="choiceAnswer" @getTaskList="getTaskList" :phase="phase"/>
+		</div>
+		<div class="dw_bottom">
+			<div class="dw_b_btn" @click.stop="submitTask()">
+				<img :src="require('../../../../assets/icon/pblCourse/bookIcon.png')">
+				<span>提交作业</span>
+			</div>
+		</div>	
+	</div>
+</template>
+
+<script>
+import work from './work'
+	export default {
+		emits:["changePhase","choiceAnswer","submitTask","getTaskList"],
+		props:{
+			phase:{
+				type:Object,
+				default:()=>{
+					return {
+						doPhase:0,
+						atPhase:0,
+					}
+				}
+			},
+			task:{
+				type:Object,
+				require:true,
+			},
+		},
+		computed:{
+			isOk(){
+				return !(this.phase.doPhase>this.phase.atPhase)
+			}
+		},
+		components:{
+			work,
+		},
+		methods:{
+			submitTask(){
+				this.$emit("submitTask")
+			},
+			down(){
+				if(this.isOk)return this.submitTask();
+				if(this.phase.atPhase>=4)return this.$message.info("已经是最后一个阶段啦")
+				this.$emit("changePhase",'atPhase',(this.phase.atPhase+1))
+			},
+			back(){
+				if(this.phase.atPhase<=0)return this.$message.info("已经是第一个阶段啦")
+				this.$emit("changePhase",'atPhase',(this.phase.atPhase-1))
+			},
+			choiceAnswer(arr){
+				this.$emit("choiceAnswer",arr)
+			},
+			getTaskList(_data){
+				this.$emit("getTaskList",_data)
+			}
+		}
+	}
+</script>
+
+<style scoped>
+.doWork{
+	width: 100%;
+	height: 100%;
+	border-radius: 12px;
+	overflow: hidden;
+	background-color: white;
+}
+
+.dw_header{
+	width: 100%;
+	height: 60px;
+	background-color: #FFF3EA;
+	margin-bottom: 15px;
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+	box-sizing: border-box;
+	padding: 0 20px;
+}
+
+.dw_h_left{
+	display: flex;
+	align-items: center;
+}
+
+.dw_h_left>img{
+	width: 40px;
+	height: 40px;
+	margin-right: 10px;
+}
+
+
+.dw_h_left>span{
+	font-size: 22px;
+	font-weight: bold;
+}
+
+
+.dw_h_right{
+	display: flex;
+	align-items: center;
+}
+
+.dw_h_right>span{
+	display: block;
+	width: 30px;
+	height: 30px;
+	background: url("../../../../assets/icon/pblCourse/backIcon.png") no-repeat;
+	background-size: 100% 100%;
+	margin-right: 10px;
+	margin-left: 5px;
+	cursor: pointer;
+}
+
+.dw_h_r_down{
+	transform: rotate(180deg);
+}
+.dw_work{
+	width: calc(100% - 30px);
+	height: calc(100% - 60px - 100px - 30px);
+	box-sizing: border-box;
+	margin: 0 15px;
+	border-radius: 8px;
+	background-color: #F3F7FD;
+}
+
+.dw_bottom{
+	margin-top: 15px;
+	width: 100%;
+	height: 100px;
+	display: flex;
+	justify-content: center;
+	align-items: center;
+	box-sizing: border-box;
+	padding: 0 15px 15px 15px;
+}
+
+.dw_b_btn{
+	width: 100%;
+	height: 100%;
+	background-color: #F3F7FD;
+	border-radius: 8px;
+	display: flex;
+	justify-content: center;
+	align-items: center;
+	transition: .3s;
+	cursor: pointer;
+}
+
+.dw_b_btn:hover{
+	background-color: #eaeef5;
+}
+
+.dw_b_btn>img{
+	width: 50px;
+	height: 50px;
+	margin-right: 10px;
+	cursor: pointer;
+}
+
+.dw_b_btn>span{
+	font-size: 22px;
+	background: linear-gradient(to right, #3673E8, #AD88FD);
+	-webkit-background-clip: text;
+	color: transparent;
+}
+</style>

+ 158 - 0
src/components/pages/pblCourse/component/procedureArea.vue

@@ -0,0 +1,158 @@
+<template>
+	<div class="procedure">
+		<div class="title">5EX挑战</div>
+		<div class="content" ref="content">
+			<div class="procedure_content" v-for="(i, index) in 5" :key="index" :class="{active: phase.atPhase >= index}">
+				<i class="img" :class="{ isDo : phase.doPhase > index}"></i>
+				<i class="dot"></i>
+				<span class="name">
+					<span>阶段{{ index+1 }}</span>
+				</span>
+				<div class="stepBorder" v-if="i != 5" :style="{width: `calc(${contentWidth} / (${5 - 1}))`}" :class="{active: phase.doPhase > index}"></div>
+			</div>
+			
+		</div>
+	</div>
+</template>
+
+<script>
+	export default {
+		props:{
+			phase:{
+				type:Object,
+				default:()=>{
+					return {
+						doPhase:0,
+						atPhase:0,
+					}
+				}
+			}
+		},
+		data() {
+			return {
+				contentWidth:"100px",
+			}
+		},
+		mounted () {
+			this.$nextTick(() => {
+				this.contentWidth = this.$refs.content.offsetWidth + "px";
+    			window.addEventListener("resize", () => {
+					this.contentWidth = this.$refs.content.offsetWidth + "px";
+				})
+			})
+		},
+		methods: {
+
+		},
+	}
+</script>
+
+<style scoped>
+.procedure{
+	width: 100%;
+	height: 100%;
+	background-image: url('../../../../assets/icon/pblCourse/top_pbl.png');
+	background-size: 100% 100%;
+    border-radius: 15px;
+	position: relative;
+	display: flex;
+    align-items: center;
+    justify-content: center;
+}
+
+
+.procedure .title{
+	position: absolute;
+    top: 0;
+    color: #fff;
+    background: linear-gradient(90deg, rgba(54, 115, 232, 0) 0%, #727EF3 50%, rgba(54, 115, 232, 0) 100%);
+    width: 190px;
+    height: 24px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    font-size: 14px;
+}
+.procedure .content{
+	display: flex;
+    width: calc(100% - 40px);
+    margin: 0 auto;
+    justify-content: space-between;
+	position: relative;
+}
+.content .procedure_content{
+	display: flex;
+    flex-direction: column;
+    align-items: center;
+	position: relative;
+}
+.content .procedure_content > .img.isDo{
+	background-image: url('../../../../assets/icon/pblCourse/top_book_active.png');
+}
+.content .procedure_content > .img{
+	background-image: url('../../../../assets/icon/pblCourse/top_book.png');
+    display: block;
+    width: 40px;
+    height: 40px;
+    background-size: 100% 100%;
+}
+.content .procedure_content > .dot{
+	border: 2px solid #c8aeff;
+    width: 16px;
+    height: 16px;
+    border-radius: 28px;
+    margin-bottom: 10px;
+	z-index: 1;
+	background: #FFFFFF59;
+	box-sizing: border-box;
+}
+
+.content .procedure_content.active > .dot{
+	border: none;
+    background: linear-gradient(90deg, #3673E8 0%, #AD88FD 100%);
+}
+
+.content .procedure_content > .name{
+	background: rgba(255, 255, 255, .55);
+	border: 2px solid #c8aeff;
+    display: block;
+    border-radius: 49px;
+    font-size: 16px;
+    padding: 3px 7px;
+    font-weight: 600;
+	position: relative;
+	overflow: hidden;
+}
+
+.content .procedure_content.active > .name{
+	border: none;
+    background: linear-gradient(90deg, #3673E8 0%, #AD88FD 100%);
+}
+
+.content .procedure_content > .name > span{
+	background-image: linear-gradient(90deg, #3673E8 0%, #AD88FD 100%);
+    background-clip: text;
+    color: transparent;
+}
+
+.content .procedure_content.active > .name > span{
+	background-image: unset;
+    background-clip: unset;
+    color: #fff;
+}
+
+.content .procedure_content > .stepBorder{
+	position: absolute;
+    width: 100%;
+    height: 10px;
+    border: 1px solid #FFFFFF59;
+    background: #FFFFFF8C;
+    left: 22px;
+    top: 43px;
+    box-sizing: border-box;
+}
+
+.content .procedure_content > .stepBorder.active{
+	background: linear-gradient(90deg, #3673E8 0%, #AD88FD 100%);
+}
+</style>

+ 293 - 0
src/components/pages/pblCourse/component/work.vue

@@ -0,0 +1,293 @@
+<template>
+	<div class="work" ref="workRef">
+		<div class="w_nowWork">
+			<div class="w_nw_header">
+				<div class="w_nw_h_title">
+					<img :src="require('../../../../assets/icon/pblCourse/taskIcon.png')">  
+					<span>当前任务</span>
+				</div>
+				<div class="w_nw_h_btn" @click.stop="changeTask" v-if="!(phase.doPhase>phase.atPhase)">
+					<img :src="require('../../../../assets/icon/pblCourse/changeIcon.png')">
+					<span>更换任务</span>
+				</div>
+			</div>
+			<div class="w_nw_introduce">
+				<div v-html="task.target" style="font-weight: bold;"></div>
+				<div v-html="task.detail"></div>
+				<div v-html="task.steps"></div>
+				<div v-html="task.tips"></div>
+			</div>
+		</div>
+		<div class="w_doWork">
+			<div class="w_dw_header">
+				<div class="w_dw_h_title">
+					<img :src="require('../../../../assets/icon/pblCourse/doWorkIcon.png')">  
+					<span>通关挑战({{taskIndex+1}}/{{task.answerArray?task.answerArray.length:5}})</span>
+				</div>
+				<div class="w_dw_h_controls">
+					<span @click.stop="back()">上一题</span>
+					<span @click.stop="down()">下一题</span>
+				</div>
+			</div>
+			<div class="w_dw_work">
+					<span class="w_dw_w_title">{{ taskIndex+1 }}.{{ task.answerArray[taskIndex].title }}</span>
+					<div class="w_dw_w_radio">
+						<el-radio-group class="w_dw_w_r_group" v-model="task.answerArray[taskIndex].userAnswer" size="medium" @input="choiceAnswer">
+  					  <el-radio class="w_dw_w_r_g_item" v-for="(item,index) in task.answerArray[taskIndex].option" :key="index+''+taskIndex" size="medium " :label="index" @input="choiceAnswer">{{ item }}</el-radio>
+  					</el-radio-group>
+					</div>
+				</div>
+		</div>
+	</div>
+</template>
+
+<script>
+	
+	export default {
+		emits:['choiceAnswer','submitTask',"getTaskList"],
+		props:{
+			task:{
+				type:Object,
+				default:()=>{
+					return{
+						
+					}
+				},
+				
+			},
+			phase:{
+				type:Object,
+				default:()=>{
+					return {
+						doPhase:0,
+						atPhase:0,
+					}
+				}
+			},
+
+		},
+		data(){
+			return{
+				taskIndex:0,
+			}
+		},
+		watch:{
+			task(){
+				this.taskIndex = 0;
+				this.$refs.workRef.scrollTop = 0;
+			}
+		},
+		methods:{
+			changeTask(){
+				this.$emit("getTaskList",this.phase.atPhase)
+			},
+			back(){
+				if(this.taskIndex==0)return this.$message.info("已经是第一题咯");
+				this.taskIndex-=1;
+			},
+			down(){
+				if(this.taskIndex>=(this.task?this.task.answerArray.length-1:5))return this.$message.info("已经是最后一题咯");
+				this.taskIndex+=1;
+			},
+			choiceAnswer(_index){
+				this.$emit("choiceAnswer",[this.taskIndex,_index])
+				this.$forceUpdate();
+			}
+		}
+	}
+</script>
+
+<style scoped>
+.work{
+	width: 100%;
+	max-height: 100%;
+	overflow: auto;
+	box-sizing: border-box;
+	padding: 25px;
+	/* background-color: aqua; */
+}
+
+.w_nowWork{
+	width: 100%;
+	height: auto;
+	margin-bottom: 20px;
+}
+
+.w_nw_header{
+	width: 100%;
+	display: flex;
+	justify-content: space-between;
+	height: 35px;
+	margin-bottom: 20px;
+}
+
+.w_nw_h_title{
+	display: flex;
+	align-items: center;
+}
+
+.w_nw_h_title>img{
+	width: 50px;
+	height: 50px;
+	margin-right: 10px;
+}
+
+.w_nw_h_title>span{
+	font-size: 24px;
+	font-weight: bold;
+	background: linear-gradient(to right, #3673E8, #AD88FD);
+	-webkit-background-clip: text;
+	color: transparent;
+}
+
+.w_nw_h_btn{
+	width: auto;
+	height: 100%;
+	border-radius: 100px;
+	box-sizing: border-box;
+	border: solid 1px #AD88FD;
+	background-color: white;
+	display: flex;
+	justify-content: center;
+	align-items: center;
+	padding: 0px 20px 0 10px;
+	cursor: pointer;
+	transition: .3s;
+}
+
+.w_nw_h_btn:hover{
+	background-color: rgb(248, 246, 246);
+}
+
+.w_nw_h_btn>img{
+	width: 20px;
+	height: 20px;
+	margin-right: 10px;
+}
+
+.w_nw_h_btn>span{
+	font-size: 16px;
+}
+
+.w_nw_introduce>div{
+	margin: 10px 0px;
+}
+
+.w_nw_introduce >>> ol{
+	margin-left: 25px;
+}
+
+.w_nw_introduce >>> ul {
+	margin-left: 25px;
+}
+
+.w_nw_introduce >>> h2{
+	margin-top: 10px;
+}
+.w_nw_introduce >>> h3{
+	margin-top: 10px;
+}
+.w_nw_introduce >>> h4{
+	margin-top: 10px;
+}
+.w_nw_introduce >>> h5{
+	margin-top: 10px;
+}
+.w_nw_introduce >>> h6{
+	margin-top: 10px;
+}
+
+
+
+.w_doWork{
+	width: 100%;
+	height: auto;
+}
+
+.w_dw_header{
+	width: 100%;
+	display: flex;
+	justify-content: space-between;
+	height: 35px;
+	margin-bottom: 20px;
+}
+
+.w_dw_h_title{
+	display: flex;
+	align-items: center;
+}
+
+.w_dw_h_title>img{
+	width: 50px;
+	height: 50px;
+	margin-right: 10px;
+}
+
+.w_dw_h_title>span{
+	font-size: 24px;
+	font-weight: bold;
+	background: linear-gradient(to right, #3673E8, #AD88FD);
+	-webkit-background-clip: text;
+	color: transparent;
+}
+
+.w_dw_h_controls{
+	display: flex;
+	align-items: center;
+}
+
+.w_dw_h_controls>span{
+	font-size: 16px;
+	margin-left: 20px;
+	cursor: pointer;
+	transition: .3s;
+	box-sizing: border-box;
+	padding: 0 15px;
+	background-color: white;
+	height: 100%;
+	display: flex;
+	justify-content: center;
+	align-items: center;
+	border-radius: 100px;
+	border: solid 1px #AD88FD;
+}
+
+.w_dw_h_controls>span:hover{
+	background-color: rgb(248, 246, 246);
+}
+
+.w_dw_work{
+	width: 100%;
+	height: auto;
+
+}
+
+.w_dw_w_title{
+	font-size: 24px;
+	font-weight: bold;
+}
+
+.w_dw_w_radio{
+	margin-top: 10px;
+}
+
+.w_dw_w_r_group{
+	display: flex;
+	flex-direction: column;
+	margin-top: 10px;
+}
+
+.w_dw_w_r_g_item{
+	margin-top: 15px;
+}
+
+.w_dw_w_r_g_item>>>.el-radio__label{
+	font-size: 22px;
+}
+
+.w_dw_w_r_g_item>>>.el-radio__inner{
+	width: 20px;
+	height: 20px;
+	margin-right: 5px;
+}
+</style>

+ 224 - 0
src/components/pages/pblCourse/index.vue

@@ -0,0 +1,224 @@
+<template>
+	<div class="pblCourse" v-loading="loading">
+		<div class="pc_left">
+			<div class="pc_l_top">
+				<procedureArea :phase="phase" />
+			</div>
+			<div class="pc_l_bottom">
+				<doWorkArea :phase="phase" @changePhase="changePhase" @choiceAnswer="choiceAnswer" @submitTask="submitTask" :task="taskList[phase.atPhase]" @getTaskList="getTaskList"/>
+			</div>
+		</div>
+		<div class="pc_right">
+			<chatArea />
+		</div>
+	</div>
+</template>
+
+<script>
+import chatArea from './component/chatArea'
+import doWorkArea from './component/doWorkArea'
+import procedureArea from './component/procedureArea'
+import { v4 as uuidv4 } from "uuid";
+import MarkdownIt from "markdown-it";
+export default {
+	components: {
+		chatArea,
+		doWorkArea,
+		procedureArea,
+	},
+	data() {
+		return {
+			loading: false,
+			phase:{
+				doPhase:0,
+				atPhase:0,
+			},
+			taskList:[]
+		};
+	},
+	methods: {
+		changePhase(type,newValue){
+			this.phase[type] = newValue;
+		},
+		getTaskList(phase = 0){
+			return new Promise((resolve,reject)=>{
+				if(this.loading)return this.$message.info("请稍等")
+			this.loading = true;
+		const _uuid = uuidv4()
+		const _msg = `
+			NOTICE
+			Role: 作为学生的学习指导Agent,你熟悉熟悉PBL(基于问题的学习)和5EX教学模型,能够根据学生的学情数据(当前的学习任务设计、学习表现数据、作业数据等)生成自适应的学习任务和对应的5道考核题目。
+			Language: Please use the same language as the user requirement, if the user speaks Chinese, the specific text of your answer should also be in Chinese.
+			ATTENTION: Use '##' to SPLIT SECTIONS, not '#'. Output format carefully referenced "Format example".
+			Instruction: Based on the context, follow "Format example", write content.
+
+			# Context
+			## 语气
+			你的语气应该是亲切地,有趣的,循循善诱的一个老师
+
+			## 工具能力
+			1. 5E教学模型应用:
+			你需要熟悉并应用5E教学模型(即引入、探索、解释、扩展和评估)于学习任务的设计中,确保学习过程的有效性和吸引力。5E教学模型是一种以学生为中心的教学方法,旨在通过五个阶段(Engage, Explore, Explain, Elaborate, Evaluate)来促进学生的学习和理解。
+			Engage(引入):在这个阶段,教师通过引人入胜的活动或问题来激发学生的兴趣和好奇心,帮助他们建立与新知识的联系。
+			Explore(探索):学生通过动手实验或调查活动来探索新概念,培养他们的探究能力和批判性思维。
+			Explain(解释):学生在这个阶段分享他们的发现,教师提供进一步的解释和指导,帮助学生理解新概念。
+			Elaborate(拓展):学生通过应用新知识来解决更复杂的问题,进一步深化他们的理解。
+			Evaluate(评估):教师和学生共同评估学习效果,反思学习过程,确定需要改进的地方。
+			2. 学生表现与选择的感知:
+			    通过与学生互动,实时感知学生的学习表现和选择,理解他们的学习需求和难点。
+			3. 自适应任务生成:
+			    基于学生的反馈和选择,自动生成个性化的学习任务,任务难度和类型随学生的表现和需求而变化。
+
+			## 工作流程
+			1. 判断学生当前处在5E模型中哪一个学习阶段。如果未提供学情数据,或无法判断学生当前处在5E教学模型中的哪一个解释,则默认处在第一个阶段(引入)阶段。请随机选择一个适合小学五年级学生的科学学习主题,并生成相应的符合引入阶段的学习任务。
+			2. 结合学生当前的学情数据,生成紧随其后的下一个阶段的学习任务,但是仅仅生成紧随其后的下一个阶段的学习任务。你需要沿着这个顺序判断:引入阶段→探索阶段→解释阶段→拓展阶段→评估阶段。比如,当你判断学情数据中,学生目前已经完成了引入阶段的学习,那么你需要提供探索阶段的学习任务。
+			3. 生成上一步中学习任务对应的5道考核选择题
+
+			## 限制
+			1. 请仅仅生成某一个阶段对应的学习任务。不要同时给出多个阶段、多个学习任务。
+			2. 请严格按照以下格式要求输出内容,请仅仅告知相应的5E阶段名称和对应的任务描述,不需要包含学情数据等与【任务】无关的内容。
+			3. 生成相应的考核题目,仅限单选题
+			4. 任务描述的格式以markdown方式输出
+			${
+				this.phase.doPhase==0?'':`
+				## 学情数据
+				这是你生成适应性学习任务时,需要参考的前置学情数据${JSON.stringify(this.taskList[this.phase.doPhase])}(当前的学习任务设计、学习表现数据、作业数据等)。`
+			}
+
+			# Format example
+			{
+			    "name": "任务名字",
+			    "detail": "任务描述(要求markdown的格式)",
+			    "target":"任务目标",
+			    "steps":"任务步骤",
+			    "tips":"任务提示",
+			    "answerArray":[
+			      {
+			        title: "标题",
+			        type: "单选题",
+			        option: ["选项1","选项2","选项3","选项4"],
+			        answer: "答案(最好是index)"
+			      },
+			      {
+			        title: "标题",
+			        type: "单选题",
+			        option: ["选项1","选项2","选项3","选项4"],
+			        answer: "答案(最好是index)"
+			      }
+			    ]
+			}`
+
+			// ${
+			// 	this.phase.doPhase==0?'':`
+			// 	## 学情数据
+			// 	这是你生成适应性学习任务时,需要参考的前置学情数据${JSON.stringify(this.taskList[this.phase.doPhase])}(当前的学习任务设计、学习表现数据、作业数据等)。`
+			// }
+
+			let params = {
+				model: "gpt-3.5-turbo",
+				temperature: 0,
+				max_tokens: 4096,
+				top_p: 1,
+				frequency_penalty: 0,
+				presence_penalty: 0,
+				messages: [{role:"user",content:_msg}],
+				uid: _uuid,
+				mind_map_question: "",
+				stream:false
+			}
+			this.ajax
+			.post("https://gpt4.cocorobo.cn/chat", params)
+			.then((res) => {
+				let _data = res.data.FunctionResponse.choices[0];
+				const content = _data.message.content;
+				const _result = JSON.parse(content);
+				const md = new MarkdownIt();
+				_result.detail = _result.detail?md.render(_result.detail):"",
+				_result.steps = _result.steps?md.render(_result.steps):"",
+				_result.target = _result.target?md.render(_result.target):"",
+				_result.tips =_result.tips?md.render(_result.tips):""
+				this.taskList[phase] = _result;
+				// this.phase.doPhase = phase;
+				this.phase.atPhase = phase;
+				console.log(this.taskList)
+				this.loading = false;
+				resolve();
+			})
+			.catch((e) => {
+				this.loading = false;
+				this.$message.error("获取任务失败")
+				resolve();
+				console.log(e);
+			});
+			})
+			
+		},
+		choiceAnswer(_data){
+			this.taskList[this.phase.atPhase].answerArray[_data[0]].userAnswer = _data[1];
+			this.$forceUpdate()
+		},
+		submitTask(){
+			
+			this.loading = true;
+			let sum = 0;
+			this.taskList[this.phase.atPhase].answerArray.forEach(i=>{
+				if('userAnswer' in i){
+					sum++;
+				}
+			})
+			if(sum<this.taskList[this.phase.atPhase].answerArray.length){
+				this.loading = false;
+				return this.$message.error("当前阶段还未完成")
+			}else if((this.phase.doPhase>this.phase.atPhase)){
+				this.loading = false;
+				return this.$message.error("该阶段已经提交过了")
+			}else{
+				this.loading = false;
+				this.phase.doPhase++;
+				this.getTaskList(this.phase.doPhase)
+			}
+		}
+	},
+	mounted() {
+		this.getTaskList()
+	},
+};
+</script>
+
+<style scoped>
+.pblCourse {
+	min-width: 1500px;
+	/* min-height: 800px; */
+	width: 100%;
+	height: 100vh;
+	display: flex;
+	background-color: #f0f2f5;
+	box-sizing: border-box;
+	padding: 20px;
+}
+
+.pc_left {
+	width: calc(100% - 500px - 20px);
+	margin-right: 20px;
+	box-sizing: border-box;
+}
+
+.pc_l_top {
+	width: 100%;
+	height: 150px;
+	box-sizing: border-box;
+}
+
+.pc_l_bottom {
+	width: 100%;
+	height: calc(100% - 150px - 15px);
+	box-sizing: border-box;
+	margin-top: 15px;
+}
+
+.pc_right {
+	width: 500px;
+	height: 100%;
+	box-sizing: border-box;
+}
+</style>

+ 9 - 0
src/router/index.js

@@ -125,6 +125,7 @@ import studentEva from '@/components/pages/studentEva'
 import kindStudentEva from '@/components/pages/kindStudentEva/index'
 import record from '@/components/pages/record/class'
 import classroomObservation from '@/components/pages/classroomObservation/index'//课堂观察
+import pblCourse from '@/components/pages/pblCourse/index'
 
 // 全局修改默认配置,点击空白处不能关闭弹窗
 ElementUI.Dialog.props.closeOnClickModal.default = false
@@ -1073,6 +1074,14 @@ export default new Router({
 					meta:{
 						requireAuth:''//不需要鉴权
 					}
+				},
+				{//pblCourse
+					path:"/pblCourse",
+					name:"pblCourse",
+					component:pblCourse,
+					meta:{
+						requireAuth:''//不需要鉴权
+					}
 				}
     ]
 })

Some files were not shown because too many files changed in this diff