Compare commits

...

4 Commits

Author SHA1 Message Date
chantouRichard c7319a564d 立论和复盘
4 months ago
chantouRichard 8daef61652 提交数据库部分
6 months ago
pcv2s4hl5 c7e68ac35f Delete 'requirement Analysis'
6 months ago
pcv2s4hl5 e0a97ceb8b Add requirement Analysis
6 months ago

@ -2,6 +2,7 @@
<project version="4">
<component name="CompilerConfiguration">
<annotationProcessing>
<profile default="true" name="Default" enabled="true" />
<profile name="Maven default annotation processors profile" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />

@ -8,5 +8,5 @@
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="21" project-jdk-type="JavaSDK" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="liberica-17" project-jdk-type="JavaSDK" />
</project>

@ -3,6 +3,7 @@ package com.learning.newdemo.controller;
import com.learning.newdemo.common.Result;
import com.learning.newdemo.service.WxArgumentService;
import com.learning.newdemo.service.WxReviewService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
@ -20,10 +21,15 @@ public class WxAIController {
@Autowired
private WxArgumentService wxArgumentService;
@Autowired
private WxReviewService wxReviewService;
private String topic;
private String stance;
private String content;
@PostMapping("/argument")
public Result<Map<String, Object>> getArgument(@RequestBody Map<String, String> params){
topic = params.get("topic");
@ -34,6 +40,8 @@ public class WxAIController {
return Result.error("立论主题或者内容为空");
}
log.info("请求内容: {}", params);
try {
String argument = wxArgumentService.getArgument(topic, stance);
if (argument == null) {
@ -51,4 +59,28 @@ public class WxAIController {
return Result.error("立论获取失败:" + e.getMessage());
}
}
@PostMapping("/review")
public Result<Map<String, Object>> review(@RequestBody Map<String, String> params){
log.info("请求内容: {}", params);
content = params.get("content");
try {
String review = wxReviewService.getReview(content);
if (review == null) {
return Result.error("复盘获取失败");
}
Map<String, Object> data = new HashMap<>();
data.put("review", review);
// 查看data
log.info("复盘获取成功:{}", review);
return Result.success(data);
}catch (Exception e){
log.error("复盘获取失败", e);
return Result.error("复盘获取失败:" + e.getMessage());
}
}
}

@ -0,0 +1,5 @@
package com.learning.newdemo.service;
public interface WxReviewService {
String getReview(String content);
}

@ -0,0 +1,91 @@
package com.learning.newdemo.service.impl;
import com.learning.newdemo.service.WxReviewService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
@Service
@Slf4j
public class WxReviewServiceImpl implements WxReviewService {
private final RestTemplate restTemplate;
@Value("${ai.argument.header.Authorization}") private String authorizationHeader;
@Value("${ai.argument.body.message.role-sys}") private String roleSys;
@Value("${ai.review.body.message.content-sys}") private String contentSys;
@Value("${ai.argument.body.message.role-user}") private String roleUser;
@Value("${ai.argument.body.model}") private String model;
@Value("${ai.argument.body.frequency_penalty}") private int frequencyPenalty;
@Value("${ai.argument.body.max_tokens}") private int maxTokens;
@Value("${ai.argument.body.presence_penalty}") private int presencePenalty;
@Value("${ai.argument.body.response_format}") private String responseFormatType;
@Value("${ai.argument.body.stream}") private boolean stream;
@Value("${ai.argument.body.temperature}") private double temperature;
@Value("${ai.argument.body.top_p}") private double topP;
@Value("${ai.argument.body.tool_choice}") private String toolChoice;
@Value("${ai.argument.body.logprobs}") private boolean logprobs;
@Value("${ai.argument.url}") private String url;
public WxReviewServiceImpl(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
@Override
public String getReview(String content) {
try {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("Authorization", authorizationHeader);
// 构建请求体,使用字符串拼接
String requestBody = "{"
+ "\"messages\": ["
+ "{"
+ "\"role\": \"" + roleSys + "\","
+ "\"content\": \"" + escapeJson(contentSys) + "\""
+ "},"
+ "{"
+ "\"role\": \"" + roleUser + "\","
+ "\"content\": \"" + content + "\""
+ "}"
+ "],"
+ "\"model\": \"" + model + "\","
+ "\"frequency_penalty\": " + frequencyPenalty + ","
+ "\"max_tokens\": " + maxTokens + ","
+ "\"presence_penalty\": " + presencePenalty + ","
+ "\"response_format\": {\"type\": \"" + responseFormatType + "\"},"
+ "\"stop\": null,"
+ "\"stream\": " + stream + ","
+ "\"stream_options\": null,"
+ "\"temperature\": " + temperature + ","
+ "\"top_p\": " + topP + ","
+ "\"tools\": null,"
+ "\"tool_choice\": \"" + toolChoice + "\","
+ "\"logprobs\": " + logprobs + ","
+ "\"top_logprobs\": null"
+ "}";
log.info("请求体:{}", requestBody);
HttpEntity<String> requestEntity = new HttpEntity<>(requestBody, headers);
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class);
return response.getBody();
} catch (Exception e) {
log.error("向AI获取立论失败", e);
return null;
}
}
// 工具方法:转义 JSON 字符串中的特殊字符
private String escapeJson(String input) {
if (input == null) return "";
return input.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t");
}
}

@ -53,6 +53,9 @@ public class WxUserServiceImpl implements WxUserService {
// 获取openid
String openid = json.getString("openid");
System.out.println("url:"+url);
System.out.println("response:"+response.getBody());
System.out.println("json:"+json);
if (openid == null || openid.isEmpty()) {
log.error("获取openid失败: {}", json);
return null;

@ -5,7 +5,7 @@ spring:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/wx_miniapp?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: 1234
password: 123456
mybatis:
mapper-locations: classpath:mapper/*.xml
@ -107,8 +107,126 @@ ai:
top_p: 1
tool_choice: "none"
logprobs: false
review:
url: https://api.deepseek.com/chat/completions
header:
Authorization: "Bearer sk-53428f3b07ff426f8c11d988f649b5e7"
body:
message:
role-sys: system
content-sys: |
评委指南
你是一个华语辩论人类评委。这是你的竞技辩论赛评委评判指南,你需要基于这个指南,完成辩论比赛的评判:以下为综合多篇辩论赛评委指南与培训资料整理的**完整评委点评操作手册**,严格遵循客观性、细节化与责任量化原则:
---
一、开场与礼节
1. 礼仪问候
- 向主办方、主席、辩手及观众致谢,肯定比赛价值
- 示例:“感谢主办方提供交流平台,双方辩手为准备比赛付出的努力值得掌声。”
2. 判准说明
- 明确告知评判标准(如说服力优先/逻辑完整性优先)及心证介入程度
- 示例:“本场以‘有效回应率’为核心判准,仅介入基础常识性心证(如历史事实),价值观争议由辩手自行论证。”
---
二、立论拆解与记录(尽可能详细和完整,不要主观概括)
1. 正方立论复现
- 定义:逐字记录核心概念界定(如“宿命=结构性高概率风险”)
- 框架:分论点逻辑链(例:认知落差→自我透明错觉→时空差异)
- 实证:标注数据来源(如《科学》杂志研究)及案例适用性(如数学家构建零误解语言失败)
2. 反方立论复现
- 定义:差异化界定(如“宿命=必然不可改变属性”)
- 框架:论证路径(例:方法论规避→技术干预→价值主张)
- 实证记录反例类型如韩国网络实名制误解率下降43%)及数据完整性
记录工具建议:使用分栏表格(如下),红笔标注未论证部分
维度
正方内容
反方内容
定义
结构性高概率风险
必然不可改变属性
核心论点
三阶机理(认知落差→自我透明错觉→时空差异)
双重路径(开放式表达+算法过滤)
---
三、攻防细节实录(尽可能详细和完整,不要主观概括)
1. 关键交锋逐句记录
- 定义战:
- 反方质疑:“若宿命=高概率30%文盲率是否构成宿命?”→ 正方回应:“宿命需机理不可逆性,文盲可通过教育改变,时空差异无法消除”(引用联合国扫盲数据)→ **有效性**:正方回应有效(守住机理标准),反方未追击“不可逆性”自洽性
- 实证战:
- 反方引用韩国案例,正方质疑数据覆盖范围(未包含社交媒体)→ 反方补充首尔大学报告第17页样本说明→ **有效性**:反方数据完整,但未回应“算法过滤导致表达萎缩”衍生问题
2. 无效回应标注
- 正方四辩未量化“高概率阈值”如87%是否达标),导致机理说服力下降
- 反方混淆“多元解读”与“误解”界限,未区分艺术与日常表达场景
记录工具建议时间轴标记法12:30-13:00 反方三辩未回应正方质询)
---
四、论证责任完成度量化(不是简单的量化,需要评估核心战场和挑战,不是所有挑战都是一样的权重)
1. 正方责任评估
- 完成项三阶机理论证60%),定义防御(回应文盲类比)
- 未完成项未解释“30% vs 87%”阈值差异,未反驳反方群体必然性关联性质疑
2. 反方责任评估
- 完成项证伪“绝对必然性”50%),韩国案例实证完整
- 未完成项:未覆盖日常表达场景(如医疗告知需精确语义),未衔接实证与价值主张
量化公式参考:
单方完成度 = (有效回应数 ÷ 总挑战数)× 100%
正方回应反方5次挑战中的3次 → 完成度60%
---
五、胜负判定模板
1. 战场归属统计
战场
正方得分
反方得分
依据
定义战
守住机理不可逆性
实证战
韩国案例数据完整但场景受限
价值战
双方未衔接实证与价值
2. 终局判词“正方通过机理不可逆性论证更贴近宿命本质,反方实证覆盖不足导致价值断裂,故判正方胜。”
---
六、改进建议指南
1. 正方优化方向
- 补充“高概率阈值”学术界定(如引用社会心理学“群体认知偏差”研究)
- 深化价值自洽:回应“宿命论是否导致消极躺平”(例:加缪式存在主义勇气)
2. 反方优化方向
- 区分艺术与日常表达场景,补充欧盟《数字服务法》平衡性案例
- 实证延伸:量化“开放式表达促进社会创新”数据(如专利合作沟通案例)
---
七、评委禁忌清单
1. 表述禁区
- 绝对化断言:“反方立论完全错误”
- 主观情感:“我被正方四辩的煽情打动”
2. 程序禁区
- 提前暗示结果:“反方某论点已决定胜负”
- 介入未挑战定义:“默认反方扩大外延无效”
---
八、工具包与训练建议
1. 记录工具:康奈尔笔记法(主栏记录/副栏批注)、多色荧光笔标注攻防有效性
2. 判准校准训练观摩5场不同风格比赛哲理辩/政策辩),检验判准普适性
3. 反馈话术库:
- 建设性批评:“建议补充**世界卫生组织沟通指南**强化数据权威性”
- 价值升华:“辩论如苏格拉底产婆术,助真理诞生而非制造对立”
---
role-user: user
model: deepseek-chat
frequency_penalty: 0
max_tokens: 2048
presence_penalty: 0
response_format: text
stream: false
temperature: 1
top_p: 1
tool_choice: "none"
logprobs: false
# JWT配置
jwt:
secret: yoursecretkey123456789abcdefghijklmnopqrstuvwxyz

@ -5,7 +5,7 @@ spring:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/wx_miniapp?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: 1234
password: 123456
mybatis:
mapper-locations: classpath:mapper/*.xml
@ -107,8 +107,126 @@ ai:
top_p: 1
tool_choice: "none"
logprobs: false
review:
url: https://api.deepseek.com/chat/completions
header:
Authorization: "Bearer sk-53428f3b07ff426f8c11d988f649b5e7"
body:
message:
role-sys: system
content-sys: |
评委指南
你是一个华语辩论人类评委。这是你的竞技辩论赛评委评判指南,你需要基于这个指南,完成辩论比赛的评判:以下为综合多篇辩论赛评委指南与培训资料整理的**完整评委点评操作手册**,严格遵循客观性、细节化与责任量化原则:
---
一、开场与礼节
1. 礼仪问候
- 向主办方、主席、辩手及观众致谢,肯定比赛价值
- 示例:“感谢主办方提供交流平台,双方辩手为准备比赛付出的努力值得掌声。”
2. 判准说明
- 明确告知评判标准(如说服力优先/逻辑完整性优先)及心证介入程度
- 示例:“本场以‘有效回应率’为核心判准,仅介入基础常识性心证(如历史事实),价值观争议由辩手自行论证。”
---
二、立论拆解与记录(尽可能详细和完整,不要主观概括)
1. 正方立论复现
- 定义:逐字记录核心概念界定(如“宿命=结构性高概率风险”)
- 框架:分论点逻辑链(例:认知落差→自我透明错觉→时空差异)
- 实证:标注数据来源(如《科学》杂志研究)及案例适用性(如数学家构建零误解语言失败)
2. 反方立论复现
- 定义:差异化界定(如“宿命=必然不可改变属性”)
- 框架:论证路径(例:方法论规避→技术干预→价值主张)
- 实证记录反例类型如韩国网络实名制误解率下降43%)及数据完整性
记录工具建议:使用分栏表格(如下),红笔标注未论证部分
维度
正方内容
反方内容
定义
结构性高概率风险
必然不可改变属性
核心论点
三阶机理(认知落差→自我透明错觉→时空差异)
双重路径(开放式表达+算法过滤)
---
三、攻防细节实录(尽可能详细和完整,不要主观概括)
1. 关键交锋逐句记录
- 定义战:
- 反方质疑:“若宿命=高概率30%文盲率是否构成宿命?”→ 正方回应:“宿命需机理不可逆性,文盲可通过教育改变,时空差异无法消除”(引用联合国扫盲数据)→ **有效性**:正方回应有效(守住机理标准),反方未追击“不可逆性”自洽性
- 实证战:
- 反方引用韩国案例,正方质疑数据覆盖范围(未包含社交媒体)→ 反方补充首尔大学报告第17页样本说明→ **有效性**:反方数据完整,但未回应“算法过滤导致表达萎缩”衍生问题
2. 无效回应标注
- 正方四辩未量化“高概率阈值”如87%是否达标),导致机理说服力下降
- 反方混淆“多元解读”与“误解”界限,未区分艺术与日常表达场景
记录工具建议时间轴标记法12:30-13:00 反方三辩未回应正方质询)
---
四、论证责任完成度量化(不是简单的量化,需要评估核心战场和挑战,不是所有挑战都是一样的权重)
1. 正方责任评估
- 完成项三阶机理论证60%),定义防御(回应文盲类比)
- 未完成项未解释“30% vs 87%”阈值差异,未反驳反方群体必然性关联性质疑
2. 反方责任评估
- 完成项证伪“绝对必然性”50%),韩国案例实证完整
- 未完成项:未覆盖日常表达场景(如医疗告知需精确语义),未衔接实证与价值主张
量化公式参考:
单方完成度 = (有效回应数 ÷ 总挑战数)× 100%
正方回应反方5次挑战中的3次 → 完成度60%
---
五、胜负判定模板
1. 战场归属统计
战场
正方得分
反方得分
依据
定义战
守住机理不可逆性
实证战
韩国案例数据完整但场景受限
价值战
双方未衔接实证与价值
2. 终局判词“正方通过机理不可逆性论证更贴近宿命本质,反方实证覆盖不足导致价值断裂,故判正方胜。”
---
六、改进建议指南
1. 正方优化方向
- 补充“高概率阈值”学术界定(如引用社会心理学“群体认知偏差”研究)
- 深化价值自洽:回应“宿命论是否导致消极躺平”(例:加缪式存在主义勇气)
2. 反方优化方向
- 区分艺术与日常表达场景,补充欧盟《数字服务法》平衡性案例
- 实证延伸:量化“开放式表达促进社会创新”数据(如专利合作沟通案例)
---
七、评委禁忌清单
1. 表述禁区
- 绝对化断言:“反方立论完全错误”
- 主观情感:“我被正方四辩的煽情打动”
2. 程序禁区
- 提前暗示结果:“反方某论点已决定胜负”
- 介入未挑战定义:“默认反方扩大外延无效”
---
八、工具包与训练建议
1. 记录工具:康奈尔笔记法(主栏记录/副栏批注)、多色荧光笔标注攻防有效性
2. 判准校准训练观摩5场不同风格比赛哲理辩/政策辩),检验判准普适性
3. 反馈话术库:
- 建设性批评:“建议补充**世界卫生组织沟通指南**强化数据权威性”
- 价值升华:“辩论如苏格拉底产婆术,助真理诞生而非制造对立”
---
role-user: user
model: deepseek-chat
frequency_penalty: 0
max_tokens: 2048
presence_penalty: 0
response_format: text
stream: false
temperature: 1
top_p: 1
tool_choice: "none"
logprobs: false
# JWT配置
jwt:
secret: yoursecretkey123456789abcdefghijklmnopqrstuvwxyz
@ -116,5 +234,5 @@ jwt:
# 服务端口配置
server:
port: 8000
port: 8080
address: 0.0.0.0

@ -1,4 +1,22 @@
{
"miniprogramRoot": "",
"libVersion": "3.0.2"
"libVersion": "3.0.2",
"appid": "wxdc5c8df2ec2453e4",
"compileType": "miniprogram",
"packOptions": {
"ignore": [],
"include": []
},
"setting": {
"babelSetting": {
"ignore": [],
"disablePlugins": [],
"outputPath": ""
}
},
"condition": {},
"editorSetting": {
"tabIndent": "insertSpaces",
"tabSize": 2
}
}

@ -0,0 +1,7 @@
{
"description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html",
"projectname": "Debate_front",
"setting": {
"compileHotReLoad": true
}
}

@ -2,17 +2,143 @@
<view class="argument-component">
<view class="content-card">
<view class="card-title">立论功能</view>
<view class="card-content">
在这里可以进行辩题立论和论点整理
<view class="card-content">在这里可以进行辩题立论和论点整理</view>
</view>
<!-- 聊天展示区 -->
<scroll-view
class="chat-area"
scroll-y
:scroll-into-view="scrollToView"
scroll-with-animation
>
<view
v-for="(msg, index) in messages"
:key="index"
:id="'msg' + index"
class="chat-message"
:class="msg.role === 'user' ? 'from-user' : 'from-ai'"
>
<view class="bubble">
<block v-if="msg.loading">
<view class="dot-flashing"></view>
</block>
<block v-else>
{{ msg.content }}
</block>
</view>
</view>
</scroll-view>
<!-- 输入框与发送按钮 -->
<view class="chat-input">
<view class="input-group">
<input
class="input-box"
type="text"
v-model="position"
placeholder="请输入论方,例如:正方"
/>
<textarea
class="textarea-box"
v-model="question"
placeholder="请输入辩题,例如:应不应该取消作业"
auto-height
/>
</view>
<button class="send-button" @click="sendMessage()"></button>
</view>
</view>
</template>
<script>
export default {
data() {
return {
input: "",
messages: [],
scrollToView: "",
position: "",
question: "",
};
},
methods: {
async sendMessage() {
//
this.messages.push({
role: "user",
content: this.position + " " + this.question,
});
this.scrollToBottom();
// AI loading
const aiIndex = this.messages.length;
this.messages.push({ role: "ai", content: "", loading: true });
this.scrollToBottom();
// AI
const reply = await this.callAI(this.position, this.question);
// loading AI
this.messages.splice(aiIndex, 1, { role: "ai", content: reply });
this.scrollToBottom();
},
async callAI(topic, stance) {
this.position = "";
this.question = "";
return new Promise((resolve, reject) => {
console.log("callAI:", topic, stance);
uni.request({
url: "http://localhost:8080/api/ai/argument", // URL
method: "POST",
header: {
"Content-Type": "application/json",
},
data: {
topic: topic,
stance: stance,
},
success: (res) => {
console.log("res:", res);
let reviewJson = JSON.parse(res.data.data.argument);
if (res.statusCode === 200 && res.data.code === 200) {
resolve(
reviewJson.choices[0].message.content || "AI 没有返回有效内容"
);
} else {
reject("请求失败:" + (res.data.msg || "未知错误"));
}
},
fail: (err) => {
reject("请求失败:" + err.errMsg);
},
});
});
},
scrollToBottom() {
this.$nextTick(() => {
this.scrollToView = "msg" + (this.messages.length - 1);
});
},
},
};
</script>
<style scoped>
.argument-component {
width: 100%;
padding: 20rpx;
display: flex;
flex-direction: column;
height: 78vh;
background: linear-gradient(135deg, #2c3e50, #4ca1af);
overflow-x: hidden; /* 禁止横向滚动 */
box-sizing: border-box; /* 避免 padding 导致溢出 */
}
.content-card {
@ -20,7 +146,7 @@
border-radius: 20rpx;
padding: 30rpx;
width: 90%;
margin: 0 auto;
margin: 0 auto 30rpx;
backdrop-filter: blur(10px);
}
@ -35,4 +161,145 @@
font-size: 28rpx;
color: rgba(255, 255, 255, 0.8);
}
/* 聊天区域 */
.chat-area {
flex: 1;
width: 90%;
margin: 0 auto;
background-color: rgba(255, 255, 255, 0.05);
border-radius: 20rpx;
padding: 20rpx;
overflow-y: auto;
}
/* 消息通用样式 */
.chat-message {
display: flex;
margin-bottom: 20rpx;
}
/* 用户消息靠右 */
.from-user {
justify-content: flex-end;
}
/* AI消息靠左 */
.from-ai {
justify-content: flex-start;
}
/* 气泡样式 */
.bubble {
max-width: 70%;
padding: 20rpx;
border-radius: 20rpx;
font-size: 28rpx;
line-height: 1.6;
word-break: break-word;
}
/* 用户气泡颜色 */
.from-user .bubble {
background-color: #00c4ff;
color: white;
border-bottom-right-radius: 0;
}
/* AI气泡颜色 */
.from-ai .bubble {
background-color: #ffd54f;
color: #333;
border-bottom-left-radius: 0;
min-width: 80px;
}
/* 输入区域 */
.chat-input {
display: flex;
padding: 20rpx;
background-color: rgba(255, 255, 255, 0.05);
border-top: 1rpx solid #ccc;
align-items: flex-end;
}
.input-group {
flex: 1;
display: flex;
flex-direction: column;
gap: 10rpx;
}
.input-box {
background-color: #fff;
border-radius: 10rpx;
padding: 10rpx 20rpx;
margin-bottom: 20rpx;
font-size: 28rpx;
border: none;
}
.textarea-box {
background-color: #fff;
border-radius: 10rpx;
padding: 10rpx 20rpx;
font-size: 28rpx;
min-height: 80rpx;
border: none;
}
.send-button {
margin-left: 20rpx;
background-color: #00bcd4;
color: #fff;
border-radius: 10rpx;
padding: 20rpx;
font-size: 28rpx;
white-space: nowrap;
}
/* AI loading 动画 */
.dot-flashing {
width: 20rpx;
height: 20rpx;
border-radius: 50%;
background-color: #333;
animation: dotFlashing 1s infinite linear alternate;
position: relative;
margin-left: auto;
margin-right: auto;
}
.dot-flashing::before,
.dot-flashing::after {
content: "";
display: inline-block;
position: absolute;
top: 0;
width: 20rpx;
height: 20rpx;
border-radius: 50%;
background-color: #333;
}
.dot-flashing::before {
left: -30rpx;
animation: dotFlashing 1s infinite linear alternate;
animation-delay: 0.2s;
}
.dot-flashing::after {
left: 30rpx;
animation: dotFlashing 1s infinite linear alternate;
animation-delay: 0.4s;
}
@keyframes dotFlashing {
0% {
background-color: #333;
}
50%,
100% {
background-color: rgba(51, 51, 51, 0.3);
}
}
</style>

@ -0,0 +1,295 @@
<template>
<view class="argument-component">
<view class="content-card">
<view class="card-title">复盘功能</view>
<view class="card-content">在这里可以进行辩论过程的复盘</view>
</view>
<!-- 聊天展示区 -->
<scroll-view
class="chat-area"
scroll-y
:scroll-into-view="scrollToView"
scroll-with-animation
>
<view
v-for="(msg, index) in messages"
:key="index"
:id="'msg' + index"
class="chat-message"
:class="msg.role === 'user' ? 'from-user' : 'from-ai'"
>
<view class="bubble">
<block v-if="msg.loading">
<view class="dot-flashing"></view>
</block>
<block v-else>
{{ msg.content }}
</block>
</view>
</view>
</scroll-view>
<!-- 输入框与发送按钮 -->
<view class="chat-input">
<view class="input-group">
<textarea
class="textarea-box"
v-model="content"
placeholder="请输入辩论的过程"
auto-height
/>
</view>
<button class="send-button" @click="sendMessage()"></button>
</view>
</view>
</template>
<script>
export default {
data() {
return {
input: "",
messages: [],
scrollToView: "",
content: "",
};
},
methods: {
async sendMessage() {
//
this.messages.push({
role: "user",
content: this.content,
});
this.scrollToBottom();
// AI loading
const aiIndex = this.messages.length;
this.messages.push({ role: "ai", content: "", loading: true });
this.scrollToBottom();
// AI
const reply = await this.callAI(this.content);
// loading AI
this.messages.splice(aiIndex, 1, { role: "ai", content: reply });
this.scrollToBottom();
},
async callAI(content) {
this.content = "";
return new Promise((resolve, reject) => {
console.log("callAI:", content);
uni.request({
url: "http://localhost:8080/api/ai/review",
method: "POST",
header: {
"Content-Type": "application/json",
},
data: {
content: content
},
success: (res) => {
console.log("res:", res);
let reviewJson = JSON.parse(res.data.data.review);
if (res.statusCode === 200 && res.data.code === 200) {
resolve(
reviewJson.choices[0].message.content || "AI 没有返回有效内容"
);
} else {
reject("请求失败:" + (res.data.msg || "未知错误"));
}
},
fail: (err) => {
reject("请求失败:" + err.errMsg);
},
});
});
},
scrollToBottom() {
this.$nextTick(() => {
this.scrollToView = "msg" + (this.messages.length - 1);
});
},
},
};
</script>
<style scoped>
.argument-component {
width: 100%;
padding: 20rpx;
display: flex;
flex-direction: column;
height: 78vh;
background: linear-gradient(135deg, #2c3e50, #337fc6);
overflow-x: hidden; /* 禁止横向滚动 */
box-sizing: border-box; /* 避免 padding 导致溢出 */
}
.content-card {
background-color: rgba(255, 255, 255, 0.1);
border-radius: 20rpx;
padding: 30rpx;
width: 90%;
margin: 0 auto 30rpx;
backdrop-filter: blur(10px);
}
.card-title {
font-size: 36rpx;
color: #ffffff;
font-weight: bold;
margin-bottom: 20rpx;
}
.card-content {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.8);
}
/* 聊天区域 */
.chat-area {
flex: 1;
width: 90%;
margin: 0 auto;
background-color: rgba(255, 255, 255, 0.05);
border-radius: 20rpx;
padding: 20rpx;
overflow-y: auto;
}
/* 消息通用样式 */
.chat-message {
display: flex;
margin-bottom: 20rpx;
}
/* 用户消息靠右 */
.from-user {
justify-content: flex-end;
}
/* AI消息靠左 */
.from-ai {
justify-content: flex-start;
}
/* 气泡样式 */
.bubble {
max-width: 70%;
padding: 20rpx;
border-radius: 20rpx;
font-size: 28rpx;
line-height: 1.6;
word-break: break-word;
}
/* 用户气泡颜色 */
.from-user .bubble {
background-color: #00c4ff;
color: white;
border-bottom-right-radius: 0;
}
/* AI气泡颜色 */
.from-ai .bubble {
background-color: #ffd54f;
color: #333;
border-bottom-left-radius: 0;
min-width: 80px;
}
/* 输入区域 */
.chat-input {
display: flex;
padding: 20rpx;
background-color: rgba(255, 255, 255, 0.05);
border-top: 1rpx solid #ccc;
align-items: flex-end;
}
.input-group {
flex: 1;
display: flex;
flex-direction: column;
gap: 10rpx;
}
.input-box {
background-color: #fff;
border-radius: 10rpx;
padding: 10rpx 20rpx;
margin-bottom: 20rpx;
font-size: 28rpx;
border: none;
}
.textarea-box {
background-color: #fff;
border-radius: 10rpx;
padding: 10rpx 20rpx;
font-size: 28rpx;
min-height: 80rpx;
border: none;
}
.send-button {
margin-left: 20rpx;
background-color: #00bcd4;
color: #fff;
border-radius: 10rpx;
padding: 20rpx;
font-size: 28rpx;
white-space: nowrap;
}
/* AI loading 动画 */
.dot-flashing {
width: 20rpx;
height: 20rpx;
border-radius: 50%;
background-color: #333;
animation: dotFlashing 1s infinite linear alternate;
position: relative;
margin-left: auto;
margin-right: auto;
}
.dot-flashing::before,
.dot-flashing::after {
content: "";
display: inline-block;
position: absolute;
top: 0;
width: 20rpx;
height: 20rpx;
border-radius: 50%;
background-color: #333;
}
.dot-flashing::before {
left: -30rpx;
animation: dotFlashing 1s infinite linear alternate;
animation-delay: 0.2s;
}
.dot-flashing::after {
left: 30rpx;
animation: dotFlashing 1s infinite linear alternate;
animation-delay: 0.4s;
}
@keyframes dotFlashing {
0% {
background-color: #333;
}
50%,
100% {
background-color: rgba(51, 51, 51, 0.3);
}
}
</style>

@ -28,7 +28,7 @@ import { ref } from 'vue';
//
import PageOfHome from '../../components/HomeCom.vue';
import PageOfArgument from '../../components/ArgumentCom.vue';
// import PageOfReview from '../../components/ReviewCom.vue';
import PageOfReview from '../../components/ReviewCom.vue';
// import PageOfDebate from '../../components/DebateCom.vue';
//

@ -0,0 +1,19 @@
create database if not exists wx_miniapp default charset utf8mb4;
use wx_miniapp;
CREATE TABLE IF NOT EXISTS `wx_user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`openid` varchar(100) NOT NULL COMMENT '微信openid',
`nickname` varchar(50) DEFAULT NULL COMMENT '昵称',
`avatar_url` varchar(500) DEFAULT NULL COMMENT '头像URL',
`gender` tinyint(4) DEFAULT NULL COMMENT '性别 0-未知 1-男 2-女',
`country` varchar(50) DEFAULT NULL COMMENT '国家',
`province` varchar(50) DEFAULT NULL COMMENT '省份',
`city` varchar(50) DEFAULT NULL COMMENT '城市',
`language` varchar(50) DEFAULT NULL COMMENT '语言',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_openid` (`openid`) COMMENT 'openid唯一索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='微信用户表';
Loading…
Cancel
Save