上传文件至 /

This commit is contained in:
索梦露 2026-01-08 22:01:13 +08:00
parent 521ee1132f
commit 4c1233fff2
5 changed files with 492 additions and 0 deletions

89
README.md Normal file
View File

@ -0,0 +1,89 @@
# AI 写作助手AI Writing Assistant
---
## 2.1 团队成员与贡献
| 姓名 | 学号 | 主要贡献(具体分工) |
|----|----|----------------|
| 索梦露 | 2411020218 | (组长)项目整体设计、后端核心逻辑开发、编写 |
| 李秀芬 | 2411020130 | Web 前端界面设计、页面样式美化、 |
---
## 2.2 项目简介 & 运行指南
### 简介
本项目是一个基于 Python 和大语言模型 API 的 **AI 写作助手系统**
旨在解决学生和内容创作者在日常写作中 **表达不够通顺、反复修改效率低** 的问题。
用户只需输入原始文本,即可通过 AI 自动生成更加流畅、自然的改写结果,从而提升写作效率和质量。
---
### 如何运行
```bash
# 1⃣ 安装依赖
uv sync # 安装依赖
# 2⃣ 配置 API Key
#在 .env 中填写你的 API Key例如
DASHSCOPE_API_KEY=sk-xxxxxxxxxxxxxxxx
#3⃣ 启动项目
uv run app.py
#启动成功后,在浏览器中访问
http://127.0.0.1:5000
#即可打开 AI 写作助手网页界面并进行演示
```
---
## 2.3 开发心得
### 选题思考:为什么做这个?解决了谁的痛苦?
#### 在日常学习和课程作业中,我们经常需要撰写实验报告、课程设计说明或总结性文字。
很多时候,并不是没有想法,而是**不知道如何把想法组织成通顺、专业的文字**
往往需要反复修改,耗费大量时间。
因此我们选择了“AI 写作助手”作为课程设计题目,希望借助当前大语言模型在自然语言处理方面的能力,
为学生和内容创作者提供一个**低门槛、易使用、效果直观**的写作辅助工具,
帮助用户提升写作效率,减少无意义的重复修改。
### AI 协作体验
这是我们第一次在完整项目中深度使用 AI 来协助编程和功能设计。
在开发过程中AI 在以下方面给予了非常大的帮助:
1.后端接口逻辑的设计思路
2.Prompt 的不断优化与改进
3.前后端交互流程的梳理
4.常见错误的快速定位与修复
其中,让人直呼“牛逼”的 Prompt 是:
“请帮我润色以下文本,使其更加通顺自然,语气正式但不过于生硬。”
这一 Prompt 能够在多种输入情况下稳定输出高质量文本,极大提升了系统实用性。
但也并非所有时候都一帆风顺。有时由于 Prompt 描述不够明确,
AI 会输出偏离预期的内容,或者在代码细节上出现不符合实际环境的问题,
这时就需要人工不断尝试、调整和验证,甚至“推翻重来”,这一过程也让我们更加理解了
**“如何正确地向 AI 提问”本身就是一项重要能力。**
### 自我反思AI 时代,程序员的核心竞争力是什么?
通过本次课程设计,我们逐渐意识到:
在 AI 时代,程序员的核心竞争力并不是死记硬背语法,而是:
问题拆解能力 —— 能否把一个模糊需求拆解成清晰的模块
工程思维 —— 是否具备代码结构、项目规范和安全意识
Prompt 设计能力 —— 是否能高效地与 AI 协作
判断与验证能力 —— 是否能判断 AI 给出的结果是否合理、可用
AI 并不会取代程序员,但会放大程序员之间的差距。
只有真正理解需求、善于思考并具备持续学习能力的人,
才能在 AI 时代持续保持竞争力。
本次项目不仅锻炼了我们的 Python 编程能力,也让我们对 AI 技术的实际应用有了更加清晰和理性的认识。

196
app.py Normal file
View File

@ -0,0 +1,196 @@
import os
from flask import Flask, render_template, request, jsonify
from dotenv import load_dotenv
import dashscope
from dashscope import Generation
from datetime import datetime
import json
load_dotenv()
dashscope.api_key = os.getenv("DASHSCOPE_API_KEY")
app = Flask(__name__)
# --------------------------
# 配置
# --------------------------
SCENE_PROMPTS = {
"general": "这是通用写作场景,语言自然清晰。",
"media": "这是自媒体写作,语言有吸引力,适合传播。",
"script": "这是剧本写作,语言有画面感和情绪张力。",
"essay": "这是学生作文写作,结构清晰、表达规范。",
"academic_scene": "这是学术写作,语言严谨、逻辑清楚。"
}
POLISH_STYLES = {
"formal": "正式、书面",
"literary": "文学性强、有文采",
"spoken": "口语化、自然",
"business": "商务、专业",
"internet": "网络风格、轻松"
}
history_records = []
MAX_HISTORY = 10
# --------------------------
# AI 调用
# --------------------------
def call_ai(prompt):
resp = Generation.call(model="qwen-turbo", prompt=prompt)
if resp.status_code == 200:
return resp.output.text
return "❌ AI 调用失败"
# --------------------------
# 多版本润色
# --------------------------
def multi_version_rewrite(text, style):
prompt = f"""
请对以下文本进行润色生成 3 个不同版本
要求
1. 保持原意
2. 风格{style}
3. 输出 3 个版本分别标记 版本1/版本2/版本3
原文
{text}
"""
return call_ai(prompt)
# --------------------------
# 扩写
# --------------------------
def expand_text(text, target_length):
"""
扩写文本到指定长度字数/字符
"""
prompt = f"""
请将以下文本扩写保持原意但丰富细节和表达使字数达到 {target_length} 字左右
可以生成 3 个不同版本分别标记 版本1/版本2/版本3
原文
{text}
"""
return call_ai(prompt)
# --------------------------
# 修改原因生成
# --------------------------
def generate_revision_reason(original, revised):
prompt = f"""
请对下面两段文本进行对比说明修改的原因
仅输出文字说明不要高亮或格式化
原文
{original}
改写后
{revised}
请详细说明改动原因
"""
return call_ai(prompt)
# --------------------------
# 文学参考
# --------------------------
def generate_literary_reference(text):
prompt = f"""
请理解下面这句话的含义然后给出 3
文学作品风格中的相似表达示例
要求
1. 不是原文引用
2. 给出风格参考作者作品名
3. 仅作为文学风格参考
输出格式
1. 句子内容
风格参考作者作品名
原句
{text}
"""
return call_ai(prompt)
# --------------------------
# 历史记录
# --------------------------
def save_history(data, result):
history_records.insert(0, {
"time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"mode": data.get("mode"),
"scene": data.get("scene"),
"input": data.get("text", "")[:100],
"output": result[:200]
})
if len(history_records) > MAX_HISTORY:
history_records.pop()
# --------------------------
# 情感分析
# --------------------------
def sentiment_analysis(text):
prompt = f"""
请分析下面文本的情感倾向仅返回严格 JSON 格式
{{"positive": %, "neutral": %, "negative": %}}
文本
{text}
"""
result = call_ai(prompt)
try:
parsed = json.loads(result.replace("", ":").replace("%",""))
return parsed
except:
return {"positive":0, "neutral":0, "negative":0}
# --------------------------
# 路由
# --------------------------
@app.route("/")
def index():
return render_template("index.html")
@app.route("/rewrite", methods=["POST"])
def rewrite():
data = request.json
text = data.get("text","")
mode = data.get("mode")
scene = data.get("scene","general")
# 根据模式生成文本
if mode=="polish":
style = data.get("style","formal")
result = multi_version_rewrite(text, style)
elif mode=="expand":
target_length = data.get("target_length", 300)
result = expand_text(text, target_length)
else:
result = f"普通生成模式:{text}" # 保留原功能
save_history(data, result)
return jsonify({"result": result})
@app.route("/literary", methods=["POST"])
def literary():
text = request.json.get("text","")
return jsonify({"reference": generate_literary_reference(text)})
@app.route("/history")
def history():
return jsonify(history_records)
@app.route("/revision_reason", methods=["POST"])
def revision_reason():
data = request.json
original = data.get("original","")
revised = data.get("revised","")
reason = generate_revision_reason(original, revised)
return jsonify({"reason": reason})
@app.route("/sentiment", methods=["POST"])
def sentiment():
text = request.json.get("text","")
return jsonify({"result": sentiment_analysis(text)})
if __name__=="__main__":
app.run(debug=True)

174
index.html Normal file
View File

@ -0,0 +1,174 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>AI 写作助手 Pro</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<div class="app">
<header>
<h1>🌙 AI 写作助手 Pro</h1>
<div class="time" id="time">当前时间:<span id="currentTime"></span></div>
</header>
<section class="card">
<textarea id="inputText" placeholder="请输入文本..."></textarea>
<div class="controls">
<select id="scene">
<option value="general">通用写作</option>
<option value="media">自媒体</option>
<option value="script">剧本</option>
<option value="essay">作文</option>
<option value="academic_scene">学术</option>
</select>
<select id="mode" onchange="updateOptions()">
<option value="polish">润色</option>
<option value="expand">扩写</option>
<option value="summary">总结</option>
<option value="simple">简化</option>
<option value="academic">学术改写</option>
</select>
</div>
<div id="extraOptions" class="extra-options"></div>
<button onclick="rewriteText()">🚀 生成</button>
</section>
<section class="card">
<h2>✍️ AI 输出(点击选择版本)</h2>
<div id="versions"></div>
</section>
<section class="card">
<h2>🔍 修改原因</h2>
<div id="revisionReason"></div>
</section>
<section class="card">
<h2>📚 文学参考</h2>
<div id="literaryBox" class="literary-box"></div>
</section>
<section class="card">
<h2>😊 情感分析</h2>
<button onclick="analyzeSentiment()">分析情感</button>
<pre id="sentimentBox"></pre>
</section>
</div>
<script>
function updateOptions(){
const mode=document.getElementById("mode").value;
const box=document.getElementById("extraOptions");
box.innerHTML="";
if(mode==="polish"){
box.innerHTML=`<label>风格:</label>
<select id="style">
<option value="formal">正式</option>
<option value="literary">文学</option>
<option value="spoken">口语</option>
<option value="business">商务</option>
<option value="internet">网络</option>
</select>`;
}
if(mode==="expand"){
box.innerHTML=`<label>目标字数:</label><input type="number" id="target_length" value="300">`;
}
if(mode==="summary" || mode==="simple"){
box.innerHTML=`<label>不超过字数:</label><input type="number" id="max_length" value="100">`;
}
}
updateOptions();
// -------------------
// 时间显示
// -------------------
function updateTime(){
const now=new Date();
document.getElementById("currentTime").innerText=now.toLocaleTimeString();
}
setInterval(updateTime,1000);
// -------------------
// 生成文本 + 多版本
// -------------------
let lastInput="";
function rewriteText(){
lastInput=document.getElementById("inputText").value;
const data={
text:lastInput,
mode:document.getElementById("mode").value,
scene:document.getElementById("scene").value
};
if(data.mode==="polish") data.style=document.getElementById("style").value;
if(data.mode==="expand") data.target_length=document.getElementById("target_length").value;
fetch("/rewrite",{
method:"POST",
headers:{"Content-Type":"application/json"},
body:JSON.stringify(data)
}).then(r=>r.json()).then(d=>{
const vbox=document.getElementById("versions");
vbox.innerHTML="";
const lines=d.result.split("\n");
lines.forEach((line)=>{
if(line.trim().length>0){
const div=document.createElement("div");
div.className="version";
div.innerText=line;
div.onclick=()=>{selectVersion(line)};
vbox.appendChild(div);
}
});
});
// 文学参考
fetch("/literary",{
method:"POST",
headers:{"Content-Type":"application/json"},
body:JSON.stringify({text:lastInput})
}).then(r=>r.json()).then(d=>{
document.getElementById("literaryBox").innerText=d.reference;
});
}
// -------------------
// 选择版本继续优化 + 显示修改原因
// -------------------
function selectVersion(text){
fetch("/revision_reason",{
method:"POST",
headers:{"Content-Type":"application/json"},
body:JSON.stringify({original:lastInput,revised:text})
}).then(r=>r.json()).then(d=>{
document.getElementById("revisionReason").innerText=d.reason;
});
document.getElementById("inputText").value=text;
lastInput=text;
}
// -------------------
// 情感分析
// -------------------
function analyzeSentiment(){
const text=document.getElementById("inputText").value;
fetch("/sentiment",{
method:"POST",
headers:{"Content-Type":"application/json"},
body:JSON.stringify({text:text})
}).then(r=>r.json()).then(d=>{
const s=d.result;
document.getElementById("sentimentBox").innerText=
`积极: ${s.positive}%\n中立: ${s.neutral}%\n消极: ${s.negative}%`;
});
}
</script>
</body>
</html>

11
pyproject.toml Normal file
View File

@ -0,0 +1,11 @@
[project]
name = "xiezuoapp"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"dashscope>=1.25.6",
"flask>=3.1.2",
"python-dotenv>=1.2.1",
]

22
style.css Normal file
View File

@ -0,0 +1,22 @@
body {
margin:0;
padding:0;
font-family:"Segoe UI",Tahoma,Geneva,Verdana,sans-serif;
background-color:#1e1e2f;
color:#e0e0e0;
}
.app{max-width:900px;margin:20px auto;padding:20px;}
header{text-align:center;margin-bottom:20px;}
header h1{font-size:2em;color:#fff;margin-bottom:5px;}
header .time{font-size:0.9em;color:#a0a0a0;}
.card{background-color:#2a2a40;padding:20px;border-radius:12px;margin-bottom:20px;box-shadow:0 5px 15px rgba(0,0,0,0.4);}
textarea{width:100%;min-height:120px;border-radius:8px;border:1px solid #444;background-color:#1f1f30;color:#e0e0e0;padding:10px;font-size:1em;resize:vertical;}
textarea:focus{outline:none;border-color:#5a5aff;}
select{border-radius:8px;border:1px solid #555;background-color:#1f1f30;color:#e0e0e0;padding:6px 10px;font-size:1em;margin-right:10px;}
select:focus{outline:none;border-color:#5a5aff;}
input[type="number"]{border-radius:8px;border:1px solid #555;background-color:#1f1f30;color:#e0e0e0;padding:5px 8px;font-size:1em;width:80px;}
button{border:none;border-radius:8px;padding:8px 16px;font-size:1em;cursor:pointer;background:linear-gradient(90deg,#5a5aff,#00d4ff);color:#fff;transition:all 0.3s ease;margin-top:10px;}
button:hover{transform:translateY(-2px);box-shadow:0 5px 10px rgba(0,0,0,0.4);}
.version{padding:8px;border-radius:6px;background-color:#3a3a55;margin-bottom:5px;cursor:pointer;transition:all 0.2s ease;}
.version:hover{background-color:#5a5aff;color:#fff;}
#revisionReason,#literaryBox,#sentimentBox{background-color:#1f1f30;padding:10px;border-radius:8px;font-size:0.95em;line-height:1.5em;}