初始化方言版AI对话助手

功能特点:
- 支持8种方言(普通话、四川话、粤语、上海话、客家话、闽南话、东北话、河南话)
- 用户注册登录系统(用户名、手机、邮箱可选、密码确认)
- 对话功能(文字输入、语音识别、文件图片上传)
- 多对话管理(新建、切换、删除)
- 移动端适配(响应式设计、触摸友好)

技术栈:
- Flask后端API
- 原生HTML/CSS/JS前端
- Web Speech API语音识别
- JWT用户认证
- 大模型: qwen3.5-4b

端口: 19002
This commit is contained in:
2026-04-08 12:09:46 +08:00
commit 376cdacf5e
7 changed files with 1354 additions and 0 deletions
+17
View File
@@ -0,0 +1,17 @@
# Python
__pycache__/
*.py[cod]
# 数据文件(保留空文件)
data/users.json
data/chats.json
data/uploads/*
!data/uploads/.gitkeep
# 环境
venv/
.env
# IDE
.idea/
.vscode/
+108
View File
@@ -0,0 +1,108 @@
# 方言版AI对话助手
> 用家乡话聊天,更亲切!支持多种方言的AI对话助手
## 功能特点
### 🗣️ 多方言支持
- 普通话
- 四川话
- 粤语
- 上海话
- 客家话
- 闽南话
- 东北话
- 河南话
### 💬 对话功能
- 文字输入
- **语音识别输入**(支持浏览器语音识别)
- 文件上传
- 图片上传
- 多对话管理
- 对话历史记录
### 👤 用户系统
- 用户注册(用户名、手机、邮箱可选、密码确认)
- 用户登录
- Token认证
### 📱 移动端适配
- 响应式设计
- 触摸友好
- 适配手机浏览器
## 快速开始
### 安装依赖
```bash
pip install -r requirements.txt
```
### 启动服务
```bash
cd backend
python app.py
```
### 访问地址
```
http://localhost:19002
```
## 项目结构
```
dialect-chat/
├── backend/
│ └── app.py # Flask后端API
├── frontend/
│ ├── index.html # 登录/注册页
│ └── chat.html # 对话界面
├── data/
│ ├── users.json # 用户数据
│ ├── chats.json # 对话数据
│ └── uploads/ # 上传文件
├── requirements.txt
└── README.md
```
## API接口
| 接口 | 方法 | 说明 |
|------|------|------|
| /api/dialects | GET | 获取方言列表 |
| /api/register | POST | 用户注册 |
| /api/login | POST | 用户登录 |
| /api/user | GET | 获取用户信息 |
| /api/chats | GET | 获取对话列表 |
| /api/chats | POST | 创建新对话 |
| /api/chats/:id | GET | 获取对话详情 |
| /api/chats/:id | DELETE | 删除对话 |
| /api/chats/:id/send | POST | 发送消息 |
| /api/upload | POST | 上传文件 |
## 大模型配置
```
API地址: http://192.168.2.5:1234/v1
模型: qwen3.5-4b
```
## 版本历史
### v0.1.0 (2026-04-08)
- 初始版本
- 支持8种方言
- 用户注册登录
- 对话功能
- 语音识别输入
- 文件图片上传
- 移动端适配
## License
MIT
+475
View File
@@ -0,0 +1,475 @@
"""
方言版AI对话助手 - 后端API
"""
from flask import Flask, request, jsonify, send_from_directory, send_file
from flask_cors import CORS
from werkzeug.security import generate_password_hash, check_password_hash
import jwt
import datetime
import json
import os
import uuid
import base64
import requests
from pathlib import Path
app = Flask(__name__, static_folder='../frontend', static_url_path='')
CORS(app)
# 配置
SECRET_KEY = 'dialect-chat-secret-key-2026'
LLM_BASE_URL = 'http://192.168.2.5:1234/v1'
LLM_API_KEY = 'sk-lm-fuP5tGU8:Hi7YU87jHyDP6Ay8Tl2j'
LLM_MODEL = 'qwen3.5-4b'
# 数据目录
DATA_DIR = Path(__file__).parent.parent / 'data'
USERS_FILE = DATA_DIR / 'users.json'
CHATS_FILE = DATA_DIR / 'chats.json'
UPLOAD_DIR = DATA_DIR / 'uploads'
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
# 方言配置
DIALECTS = {
'mandarin': {
'name': '普通话',
'prompt': '请用标准的普通话回复。',
'greeting': '你好!有什么我可以帮助你的吗?'
},
'sichuan': {
'name': '四川话',
'prompt': '请用四川话回复,使用四川方言的特色表达,比如"巴适""要得"""等。',
'greeting': '你好哇!有啥子事要得嘛?'
},
'cantonese': {
'name': '粤语',
'prompt': '请用粤语回复,使用粤语的特色表达,比如""""""""等。',
'greeting': '你好!有乜嘢可以帮你?'
},
'shanghai': {
'name': '上海话',
'prompt': '请用上海话回复,使用上海方言的特色表达。',
'greeting': '侬好!有啥事体伐?'
},
'hakka': {
'name': '客家话',
'prompt': '请用客家话回复,使用客家方言的特色表达。',
'greeting': '你好!有么个可以帮你?'
},
'minnan': {
'name': '闽南话',
'prompt': '请用闽南话回复,使用闽南方言的特色表达。',
'greeting': '你好!有啥米代志?'
},
'northeast': {
'name': '东北话',
'prompt': '请用东北话回复,使用东北方言的特色表达,比如"咋整""嘎哈""整挺好"等。',
'greeting': '哎呀妈呀,来了!有啥事儿啊?'
},
'henan': {
'name': '河南话',
'prompt': '请用河南话回复,使用河南方言的特色表达,比如"""弄啥嘞""可得劲"等。',
'greeting': '中!你有啥事儿说呗!'
}
}
# 初始化数据文件
def init_data():
if not USERS_FILE.exists():
USERS_FILE.write_text(json.dumps({}, ensure_ascii=False))
if not CHATS_FILE.exists():
CHATS_FILE.write_text(json.dumps({}, ensure_ascii=False))
init_data()
# 辅助函数
def load_users():
return json.loads(USERS_FILE.read_text(encoding='utf-8'))
def save_users(users):
USERS_FILE.write_text(json.dumps(users, ensure_ascii=False, indent=2), encoding='utf-8')
def load_chats():
return json.loads(CHATS_FILE.read_text(encoding='utf-8'))
def save_chats(chats):
CHATS_FILE.write_text(json.dumps(chats, ensure_ascii=False, indent=2), encoding='utf-8')
def generate_token(user_id):
return jwt.encode({
'user_id': user_id,
'exp': datetime.datetime.utcnow() + datetime.timedelta(days=7)
}, SECRET_KEY, algorithm='HS256')
def verify_token(token):
try:
return jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
except:
return None
def get_current_user():
token = request.headers.get('Authorization', '').replace('Bearer ', '')
if not token:
return None
data = verify_token(token)
if not data:
return None
users = load_users()
return users.get(data['user_id'])
def call_llm(messages, dialect='mandarin'):
"""调用大模型API"""
dialect_info = DIALECTS.get(dialect, DIALECTS['mandarin'])
# 添加系统提示
system_message = {
'role': 'system',
'content': f'你是一个友好的AI助手。{dialect_info["prompt"]}'
}
full_messages = [system_message] + messages[-20:] # 保留最近20条消息
try:
response = requests.post(
f'{LLM_BASE_URL}/chat/completions',
headers={
'Content-Type': 'application/json',
'Authorization': f'Bearer {LLM_API_KEY}'
},
json={
'model': LLM_MODEL,
'messages': full_messages,
'max_tokens': 1024,
'temperature': 0.8
},
timeout=60
)
if response.status_code == 200:
result = response.json()
return result['choices'][0]['message']['content']
else:
return f'抱歉,我遇到了一些问题,请稍后再试。'
except Exception as e:
return f'连接出现问题了,请稍后再试试。'
# ============ 页面路由 ============
@app.route('/')
def index():
return send_file('../frontend/index.html')
@app.route('/chat')
def chat_page():
return send_file('../frontend/chat.html')
# ============ API路由 ============
@app.route('/api/dialects')
def api_dialects():
"""获取支持的方言列表"""
return jsonify({k: {'name': v['name'], 'greeting': v['greeting']} for k, v in DIALECTS.items()})
@app.route('/api/register', methods=['POST'])
def api_register():
"""用户注册"""
data = request.json
username = data.get('username', '').strip()
password = data.get('password', '')
confirm_password = data.get('confirm_password', '')
phone = data.get('phone', '').strip()
email = data.get('email', '').strip()
# 验证
if not username or len(username) < 2:
return jsonify({'error': '用户名至少2个字符'}), 400
if not password or len(password) < 6:
return jsonify({'error': '密码至少6个字符'}), 400
if password != confirm_password:
return jsonify({'error': '两次密码不一致'}), 400
if not phone or len(phone) < 11:
return jsonify({'error': '请输入正确的手机号'}), 400
users = load_users()
# 检查用户名是否存在
for uid, user in users.items():
if user['username'] == username:
return jsonify({'error': '用户名已存在'}), 400
if user['phone'] == phone:
return jsonify({'error': '手机号已注册'}), 400
# 创建用户
user_id = str(uuid.uuid4())
users[user_id] = {
'id': user_id,
'username': username,
'password': generate_password_hash(password),
'phone': phone,
'email': email,
'created_at': datetime.datetime.now().isoformat(),
'chats': []
}
save_users(users)
token = generate_token(user_id)
return jsonify({
'success': True,
'token': token,
'user': {
'id': user_id,
'username': username,
'phone': phone,
'email': email
}
})
@app.route('/api/login', methods=['POST'])
def api_login():
"""用户登录"""
data = request.json
username = data.get('username', '').strip()
password = data.get('password', '')
if not username or not password:
return jsonify({'error': '请输入用户名和密码'}), 400
users = load_users()
for user_id, user in users.items():
if user['username'] == username:
if check_password_hash(user['password'], password):
token = generate_token(user_id)
return jsonify({
'success': True,
'token': token,
'user': {
'id': user_id,
'username': user['username'],
'phone': user['phone'],
'email': user.get('email', '')
}
})
else:
return jsonify({'error': '密码错误'}), 400
return jsonify({'error': '用户不存在'}), 400
@app.route('/api/user')
def api_user():
"""获取当前用户信息"""
user = get_current_user()
if not user:
return jsonify({'error': '未登录'}), 401
return jsonify({
'id': user['id'],
'username': user['username'],
'phone': user['phone'],
'email': user.get('email', '')
})
@app.route('/api/chats', methods=['GET'])
def api_get_chats():
"""获取用户的对话列表"""
user = get_current_user()
if not user:
return jsonify({'error': '未登录'}), 401
chats = load_chats()
user_chats = []
for chat_id in user.get('chats', []):
if chat_id in chats:
chat = chats[chat_id]
user_chats.append({
'id': chat_id,
'title': chat['title'],
'dialect': chat['dialect'],
'created_at': chat['created_at'],
'updated_at': chat.get('updated_at', chat['created_at'])
})
# 按更新时间倒序
user_chats.sort(key=lambda x: x['updated_at'], reverse=True)
return jsonify(user_chats)
@app.route('/api/chats', methods=['POST'])
def api_create_chat():
"""创建新对话"""
user = get_current_user()
if not user:
return jsonify({'error': '未登录'}), 401
data = request.json
dialect = data.get('dialect', 'mandarin')
chat_id = str(uuid.uuid4())
chats = load_chats()
chats[chat_id] = {
'id': chat_id,
'user_id': user['id'],
'title': '新对话',
'dialect': dialect,
'messages': [],
'created_at': datetime.datetime.now().isoformat(),
'updated_at': datetime.datetime.now().isoformat()
}
save_chats(chats)
# 更新用户的对话列表
users = load_users()
if chat_id not in users[user['id']]['chats']:
users[user['id']]['chats'].append(chat_id)
save_users(users)
return jsonify({
'id': chat_id,
'title': '新对话',
'dialect': dialect
})
@app.route('/api/chats/<chat_id>', methods=['GET'])
def api_get_chat(chat_id):
"""获取对话详情"""
user = get_current_user()
if not user:
return jsonify({'error': '未登录'}), 401
chats = load_chats()
chat = chats.get(chat_id)
if not chat or chat['user_id'] != user['id']:
return jsonify({'error': '对话不存在'}), 404
return jsonify(chat)
@app.route('/api/chats/<chat_id>', methods=['DELETE'])
def api_delete_chat(chat_id):
"""删除对话"""
user = get_current_user()
if not user:
return jsonify({'error': '未登录'}), 401
chats = load_chats()
chat = chats.get(chat_id)
if not chat or chat['user_id'] != user['id']:
return jsonify({'error': '对话不存在'}), 404
del chats[chat_id]
save_chats(chats)
# 更新用户的对话列表
users = load_users()
if chat_id in users[user['id']]['chats']:
users[user['id']]['chats'].remove(chat_id)
save_users(users)
return jsonify({'success': True})
@app.route('/api/chats/<chat_id>/send', methods=['POST'])
def api_send_message(chat_id):
"""发送消息"""
user = get_current_user()
if not user:
return jsonify({'error': '未登录'}), 401
data = request.json
content = data.get('content', '').strip()
if not content:
return jsonify({'error': '消息不能为空'}), 400
chats = load_chats()
chat = chats.get(chat_id)
if not chat or chat['user_id'] != user['id']:
return jsonify({'error': '对话不存在'}), 404
# 添加用户消息
user_message = {
'role': 'user',
'content': content,
'time': datetime.datetime.now().isoformat()
}
chat['messages'].append(user_message)
# 调用大模型
messages = [{'role': m['role'], 'content': m['content']} for m in chat['messages']]
ai_response = call_llm(messages, chat['dialect'])
# 添加AI回复
ai_message = {
'role': 'assistant',
'content': ai_response,
'time': datetime.datetime.now().isoformat()
}
chat['messages'].append(ai_message)
# 更新对话标题(第一条消息的前20字)
if len(chat['messages']) <= 2:
chat['title'] = content[:20] + ('...' if len(content) > 20 else '')
chat['updated_at'] = datetime.datetime.now().isoformat()
save_chats(chats)
return jsonify({
'user_message': user_message,
'ai_message': ai_message
})
@app.route('/api/upload', methods=['POST'])
def api_upload():
"""上传文件"""
user = get_current_user()
if not user:
return jsonify({'error': '未登录'}), 401
if 'file' not in request.files:
return jsonify({'error': '没有上传文件'}), 400
file = request.files['file']
if file.filename == '':
return jsonify({'error': '没有选择文件'}), 400
# 保存文件
file_id = str(uuid.uuid4())
filename = f"{file_id}_{file.filename}"
filepath = UPLOAD_DIR / filename
file.save(filepath)
# 如果是图片,返回base64预览
is_image = file.content_type.startswith('image/')
preview = None
if is_image:
preview = f"/api/uploads/{filename}"
return jsonify({
'success': True,
'file_id': file_id,
'filename': file.filename,
'is_image': is_image,
'preview': preview
})
@app.route('/api/uploads/<filename>')
def api_get_upload(filename):
"""获取上传的文件"""
return send_file(UPLOAD_DIR / filename)
if __name__ == '__main__':
print("=" * 50)
print("方言版AI对话助手")
print("=" * 50)
print(f"访问地址: http://localhost:19002")
print("=" * 50)
app.run(host='0.0.0.0', port=19002, debug=True)
+1
View File
@@ -0,0 +1 @@
# 占位文件
+531
View File
@@ -0,0 +1,531 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>方言AI助手</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" rel="stylesheet">
<style>
* { -webkit-tap-highlight-color: transparent; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
.gradient-bg { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
.chat-bubble { max-width: 85%; }
.chat-bubble-user { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
.chat-bubble-ai { background: #f3f4f6; }
.sidebar { transform: translateX(-100%); transition: transform 0.3s; }
.sidebar.open { transform: translateX(0); }
.dialect-select { max-height: 50vh; overflow-y: auto; }
.message-content img { max-width: 100%; border-radius: 8px; margin: 8px 0; }
.recording { animation: pulse 1s infinite; }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
.typing-indicator span { animation: blink 1.4s infinite both; }
.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
@keyframes blink { 0%, 80%, 100% { opacity: 0; } 40% { opacity: 1; } }
</style>
</head>
<body class="bg-gray-100 h-screen overflow-hidden">
<!-- 侧边栏遮罩 -->
<div id="sidebarOverlay" class="fixed inset-0 bg-black/50 z-40 hidden" onclick="closeSidebar()"></div>
<!-- 侧边栏 -->
<aside id="sidebar" class="sidebar fixed left-0 top-0 h-full w-72 bg-white z-50 shadow-xl flex flex-col">
<div class="gradient-bg text-white p-4">
<div class="flex items-center justify-between">
<h2 class="font-bold text-lg">🗣️ 方言AI助手</h2>
<button onclick="closeSidebar()" class="p-1"><i class="ri-close-line text-xl"></i></button>
</div>
<div class="mt-3 text-sm text-white/80" id="userInfo">加载中...</div>
</div>
<!-- 新建对话按钮 -->
<div class="p-3 border-b">
<button onclick="createNewChat()" class="w-full py-2.5 border-2 border-dashed border-purple-300 text-purple-600 rounded-lg hover:bg-purple-50 flex items-center justify-center gap-2">
<i class="ri-add-line"></i> 新建对话
</button>
</div>
<!-- 对话列表 -->
<div class="flex-1 overflow-y-auto" id="chatList">
<div class="p-4 text-center text-gray-400 text-sm">加载中...</div>
</div>
<!-- 底部操作 -->
<div class="p-3 border-t space-y-2">
<button onclick="showDialectModal()" class="w-full py-2 bg-gray-100 rounded-lg text-gray-700 flex items-center justify-center gap-2">
<i class="ri-translate-2"></i> 切换方言
</button>
<button onclick="logout()" class="w-full py-2 bg-red-50 text-red-500 rounded-lg flex items-center justify-center gap-2">
<i class="ri-logout-box-line"></i> 退出登录
</button>
</div>
</aside>
<!-- 主内容区 -->
<div class="h-full flex flex-col">
<!-- 顶部栏 -->
<header class="gradient-bg text-white p-4 flex items-center gap-3 shadow-lg">
<button onclick="openSidebar()" class="p-1"><i class="ri-menu-line text-xl"></i></button>
<div class="flex-1">
<h1 class="font-medium" id="chatTitle">新对话</h1>
<p class="text-xs text-white/70" id="currentDialect">普通话</p>
</div>
<button onclick="deleteCurrentChat()" class="p-1 text-white/70 hover:text-white">
<i class="ri-delete-bin-line text-xl"></i>
</button>
</header>
<!-- 消息区域 -->
<div class="flex-1 overflow-y-auto p-4 space-y-4" id="messagesContainer">
<!-- 欢迎消息 -->
<div class="text-center py-8">
<div class="text-6xl mb-4">🗣️</div>
<h2 class="text-xl font-bold text-gray-800">方言AI助手</h2>
<p class="text-gray-500 mt-2">用家乡话聊天,更亲切</p>
<div class="mt-4 text-sm text-gray-400">
当前方言: <span id="welcomeDialect">普通话</span>
</div>
</div>
</div>
<!-- 输入区域 -->
<div class="bg-white border-t p-3">
<!-- 图片预览 -->
<div id="imagePreview" class="hidden mb-2 relative inline-block">
<img src="" class="h-20 rounded-lg">
<button onclick="clearImagePreview()" class="absolute -top-2 -right-2 w-6 h-6 bg-red-500 text-white rounded-full text-xs">
<i class="ri-close-line"></i>
</button>
</div>
<div class="flex items-end gap-2">
<!-- 工具按钮 -->
<div class="flex gap-1">
<label class="p-2 text-gray-400 hover:text-purple-500 cursor-pointer">
<i class="ri-image-line text-xl"></i>
<input type="file" accept="image/*" class="hidden" onchange="handleImageUpload(event)">
</label>
<label class="p-2 text-gray-400 hover:text-purple-500 cursor-pointer">
<i class="ri-attachment-line text-xl"></i>
<input type="file" class="hidden" onchange="handleFileUpload(event)">
</label>
</div>
<!-- 输入框 -->
<div class="flex-1 relative">
<textarea id="messageInput" placeholder="输入消息..."
class="w-full px-4 py-2.5 border border-gray-200 rounded-2xl resize-none focus:ring-2 focus:ring-purple-500 focus:border-transparent"
rows="1" style="max-height: 120px;"
onkeydown="handleKeyDown(event)"></textarea>
</div>
<!-- 发送/语音按钮 -->
<button id="voiceBtn" onclick="toggleVoice()" class="p-2.5 text-gray-400 hover:text-purple-500">
<i class="ri-mic-line text-xl"></i>
</button>
<button onclick="sendMessage()" class="p-2.5 gradient-bg text-white rounded-full">
<i class="ri-send-plane-fill text-xl"></i>
</button>
</div>
</div>
</div>
<!-- 方言选择弹窗 -->
<div id="dialectModal" class="fixed inset-0 bg-black/50 z-50 hidden items-center justify-center p-4">
<div class="bg-white rounded-2xl w-full max-w-sm">
<div class="p-4 border-b flex justify-between items-center">
<h3 class="font-bold text-lg">选择方言</h3>
<button onclick="closeDialectModal()" class="text-gray-400"><i class="ri-close-line text-xl"></i></button>
</div>
<div class="dialect-select p-2" id="dialectList">
<!-- 方言列表 -->
</div>
</div>
</div>
<script>
const API_URL = '';
let token = localStorage.getItem('token');
let user = JSON.parse(localStorage.getItem('user') || '{}');
let currentChatId = null;
let dialects = {};
let currentDialect = 'mandarin';
let isRecording = false;
let recognition = null;
let uploadedImage = null;
// 初始化
document.addEventListener('DOMContentLoaded', () => {
if (!token) {
window.location.href = '/';
return;
}
loadDialects();
loadChatList();
initVoiceRecognition();
});
// 加载方言列表
async function loadDialects() {
try {
const res = await fetch('/api/dialects');
dialects = await res.json();
renderDialectList();
} catch (e) {
console.error('加载方言失败', e);
}
}
function renderDialectList() {
const container = document.getElementById('dialectList');
container.innerHTML = Object.entries(dialects).map(([key, val]) => `
<button onclick="selectDialect('${key}')"
class="w-full p-3 text-left rounded-lg hover:bg-purple-50 flex items-center justify-between ${currentDialect === key ? 'bg-purple-50 text-purple-600' : ''}">
<span>${val.name}</span>
${currentDialect === key ? '<i class="ri-check-line"></i>' : ''}
</button>
`).join('');
}
// 加载对话列表
async function loadChatList() {
try {
const res = await fetch('/api/chats', {
headers: { 'Authorization': `Bearer ${token}` }
});
const chats = await res.json();
renderChatList(chats);
document.getElementById('userInfo').textContent = `欢迎, ${user.username || '用户'}`;
// 如果有对话,加载第一个
if (chats.length > 0) {
loadChat(chats[0].id);
}
} catch (e) {
console.error('加载对话列表失败', e);
}
}
function renderChatList(chats) {
const container = document.getElementById('chatList');
if (chats.length === 0) {
container.innerHTML = '<div class="p-4 text-center text-gray-400 text-sm">暂无对话</div>';
return;
}
container.innerHTML = chats.map(chat => `
<div onclick="loadChat('${chat.id}')"
class="p-3 border-b hover:bg-gray-50 cursor-pointer ${currentChatId === chat.id ? 'bg-purple-50' : ''}">
<div class="font-medium text-gray-800 truncate">${chat.title}</div>
<div class="text-xs text-gray-400 mt-1">${dialects[chat.dialect]?.name || '普通话'}</div>
</div>
`).join('');
}
// 创建新对话
async function createNewChat() {
try {
const res = await fetch('/api/chats', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ dialect: currentDialect })
});
const chat = await res.json();
currentChatId = chat.id;
document.getElementById('chatTitle').textContent = chat.title;
document.getElementById('messagesContainer').innerHTML = `
<div class="text-center py-8">
<div class="text-6xl mb-4">🗣️</div>
<h2 class="text-xl font-bold text-gray-800">方言AI助手</h2>
<p class="text-gray-500 mt-2">${dialects[currentDialect]?.greeting || '你好!有什么我可以帮助你的吗?'}</p>
</div>
`;
loadChatList();
closeSidebar();
} catch (e) {
alert('创建对话失败');
}
}
// 加载对话
async function loadChat(chatId) {
try {
const res = await fetch(`/api/chats/${chatId}`, {
headers: { 'Authorization': `Bearer ${token}` }
});
const chat = await res.json();
currentChatId = chatId;
currentDialect = chat.dialect;
document.getElementById('chatTitle').textContent = chat.title;
document.getElementById('currentDialect').textContent = dialects[currentDialect]?.name || '普通话';
document.getElementById('welcomeDialect').textContent = dialects[currentDialect]?.name || '普通话';
renderMessages(chat.messages);
closeSidebar();
loadChatList();
} catch (e) {
console.error('加载对话失败', e);
}
}
function renderMessages(messages) {
const container = document.getElementById('messagesContainer');
if (messages.length === 0) {
container.innerHTML = `
<div class="text-center py-8">
<div class="text-6xl mb-4">🗣️</div>
<h2 class="text-xl font-bold text-gray-800">方言AI助手</h2>
<p class="text-gray-500 mt-2">${dialects[currentDialect]?.greeting || '你好!'}</p>
</div>
`;
return;
}
container.innerHTML = messages.map(msg => `
<div class="flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}">
<div class="chat-bubble ${msg.role === 'user' ? 'chat-bubble-user text-white' : 'chat-bubble-ai text-gray-800'} p-3 rounded-2xl">
<div class="message-content text-sm whitespace-pre-wrap">${msg.content}</div>
</div>
</div>
`).join('');
container.scrollTop = container.scrollHeight;
}
// 发送消息
async function sendMessage() {
const input = document.getElementById('messageInput');
let content = input.value.trim();
if (!currentChatId) {
await createNewChat();
}
if (uploadedImage) {
content = uploadedImage + '\n' + content;
uploadedImage = null;
document.getElementById('imagePreview').classList.add('hidden');
}
if (!content) return;
input.value = '';
input.style.height = 'auto';
// 显示用户消息
addMessage('user', content);
// 显示加载动画
const loadingId = showLoading();
try {
const res = await fetch(`/api/chats/${currentChatId}/send`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ content })
});
const data = await res.json();
removeLoading(loadingId);
if (data.ai_message) {
addMessage('assistant', data.ai_message.content);
document.getElementById('chatTitle').textContent = data.ai_message.content.substring(0, 20) + '...';
}
} catch (e) {
removeLoading(loadingId);
addMessage('assistant', '抱歉,网络出现问题了,请稍后再试。');
}
}
function addMessage(role, content) {
const container = document.getElementById('messagesContainer');
const div = document.createElement('div');
div.className = `flex ${role === 'user' ? 'justify-end' : 'justify-start'}`;
div.innerHTML = `
<div class="chat-bubble ${role === 'user' ? 'chat-bubble-user text-white' : 'chat-bubble-ai text-gray-800'} p-3 rounded-2xl">
<div class="message-content text-sm whitespace-pre-wrap">${content}</div>
</div>
`;
container.appendChild(div);
container.scrollTop = container.scrollHeight;
}
function showLoading() {
const container = document.getElementById('messagesContainer');
const id = 'loading-' + Date.now();
const div = document.createElement('div');
div.id = id;
div.className = 'flex justify-start';
div.innerHTML = `
<div class="chat-bubble chat-bubble-ai p-3 rounded-2xl">
<div class="typing-indicator flex gap-1">
<span class="w-2 h-2 bg-gray-400 rounded-full"></span>
<span class="w-2 h-2 bg-gray-400 rounded-full"></span>
<span class="w-2 h-2 bg-gray-400 rounded-full"></span>
</div>
</div>
`;
container.appendChild(div);
container.scrollTop = container.scrollHeight;
return id;
}
function removeLoading(id) {
document.getElementById(id)?.remove();
}
// 语音识别
function initVoiceRecognition() {
if ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window) {
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
recognition = new SpeechRecognition();
recognition.continuous = false;
recognition.interimResults = true;
recognition.lang = 'zh-CN';
recognition.onresult = (event) => {
const transcript = event.results[0][0].transcript;
document.getElementById('messageInput').value = transcript;
};
recognition.onend = () => {
isRecording = false;
document.getElementById('voiceBtn').innerHTML = '<i class="ri-mic-line text-xl"></i>';
document.getElementById('voiceBtn').classList.remove('text-red-500', 'recording');
};
}
}
function toggleVoice() {
if (!recognition) {
alert('您的浏览器不支持语音识别');
return;
}
if (isRecording) {
recognition.stop();
} else {
recognition.start();
isRecording = true;
document.getElementById('voiceBtn').innerHTML = '<i class="ri-mic-fill text-xl"></i>';
document.getElementById('voiceBtn').classList.add('text-red-500', 'recording');
}
}
// 图片上传
function handleImageUpload(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
uploadedImage = `[图片: ${file.name}]`;
const preview = document.getElementById('imagePreview');
preview.querySelector('img').src = e.target.result;
preview.classList.remove('hidden');
};
reader.readAsDataURL(file);
}
function clearImagePreview() {
uploadedImage = null;
document.getElementById('imagePreview').classList.add('hidden');
}
function handleFileUpload(event) {
const file = event.target.files[0];
if (!file) return;
document.getElementById('messageInput').value += `\n[文件: ${file.name}]`;
}
// 方言选择
function showDialectModal() {
renderDialectList();
document.getElementById('dialectModal').classList.remove('hidden');
document.getElementById('dialectModal').classList.add('flex');
}
function closeDialectModal() {
document.getElementById('dialectModal').classList.add('hidden');
document.getElementById('dialectModal').classList.remove('flex');
}
function selectDialect(key) {
currentDialect = key;
document.getElementById('currentDialect').textContent = dialects[key]?.name || '普通话';
document.getElementById('welcomeDialect').textContent = dialects[key]?.name || '普通话';
closeDialectModal();
}
// 侧边栏
function openSidebar() {
document.getElementById('sidebar').classList.add('open');
document.getElementById('sidebarOverlay').classList.remove('hidden');
}
function closeSidebar() {
document.getElementById('sidebar').classList.remove('open');
document.getElementById('sidebarOverlay').classList.add('hidden');
}
// 删除对话
async function deleteCurrentChat() {
if (!currentChatId || !confirm('确定删除这个对话吗?')) return;
try {
await fetch(`/api/chats/${currentChatId}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${token}` }
});
currentChatId = null;
loadChatList();
document.getElementById('messagesContainer').innerHTML = `
<div class="text-center py-8">
<div class="text-6xl mb-4">🗣️</div>
<h2 class="text-xl font-bold text-gray-800">方言AI助手</h2>
<p class="text-gray-500 mt-2">点击"新建对话"开始聊天</p>
</div>
`;
} catch (e) {
alert('删除失败');
}
}
// 退出登录
function logout() {
localStorage.removeItem('token');
localStorage.removeItem('user');
window.location.href = '/';
}
// 输入框事件
function handleKeyDown(event) {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
sendMessage();
}
}
// 自动调整输入框高度
document.getElementById('messageInput').addEventListener('input', function() {
this.style.height = 'auto';
this.style.height = Math.min(this.scrollHeight, 120) + 'px';
});
</script>
</body>
</html>
+217
View File
@@ -0,0 +1,217 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>方言AI助手 - 登录</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" rel="stylesheet">
<style>
* { -webkit-tap-highlight-color: transparent; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
.gradient-bg { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
</style>
</head>
<body class="bg-gray-100 min-h-screen">
<div class="min-h-screen flex flex-col">
<!-- 头部 -->
<div class="gradient-bg text-white p-6 pb-12">
<h1 class="text-2xl font-bold text-center">🗣️ 方言AI助手</h1>
<p class="text-center text-white/80 mt-2 text-sm">用家乡话聊天,更亲切</p>
</div>
<!-- 表单区域 -->
<div class="flex-1 -mt-6">
<div class="bg-white rounded-t-3xl shadow-lg p-6 max-w-md mx-auto">
<!-- Tab切换 -->
<div class="flex mb-6 bg-gray-100 rounded-lg p-1">
<button id="loginTab" onclick="showTab('login')"
class="flex-1 py-2 rounded-lg bg-white shadow text-gray-800 font-medium transition-all">
登录
</button>
<button id="registerTab" onclick="showTab('register')"
class="flex-1 py-2 rounded-lg text-gray-500 transition-all">
注册
</button>
</div>
<!-- 登录表单 -->
<form id="loginForm" class="space-y-4">
<div>
<label class="block text-sm text-gray-600 mb-1">用户名</label>
<div class="relative">
<i class="ri-user-line absolute left-3 top-3 text-gray-400"></i>
<input type="text" id="loginUsername" placeholder="请输入用户名"
class="w-full pl-10 pr-4 py-3 border border-gray-200 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent">
</div>
</div>
<div>
<label class="block text-sm text-gray-600 mb-1">密码</label>
<div class="relative">
<i class="ri-lock-line absolute left-3 top-3 text-gray-400"></i>
<input type="password" id="loginPassword" placeholder="请输入密码"
class="w-full pl-10 pr-4 py-3 border border-gray-200 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent">
</div>
</div>
<button type="submit"
class="w-full py-3 gradient-bg text-white rounded-lg font-medium hover:opacity-90 transition-opacity">
登录
</button>
</form>
<!-- 注册表单 -->
<form id="registerForm" class="space-y-4 hidden">
<div>
<label class="block text-sm text-gray-600 mb-1">用户名 <span class="text-red-500">*</span></label>
<div class="relative">
<i class="ri-user-line absolute left-3 top-3 text-gray-400"></i>
<input type="text" id="regUsername" placeholder="至少2个字符"
class="w-full pl-10 pr-4 py-3 border border-gray-200 rounded-lg focus:ring-2 focus:ring-purple-500">
</div>
</div>
<div>
<label class="block text-sm text-gray-600 mb-1">手机号 <span class="text-red-500">*</span></label>
<div class="relative">
<i class="ri-phone-line absolute left-3 top-3 text-gray-400"></i>
<input type="tel" id="regPhone" placeholder="请输入手机号"
class="w-full pl-10 pr-4 py-3 border border-gray-200 rounded-lg focus:ring-2 focus:ring-purple-500">
</div>
</div>
<div>
<label class="block text-sm text-gray-600 mb-1">邮箱 <span class="text-gray-400">(可选)</span></label>
<div class="relative">
<i class="ri-mail-line absolute left-3 top-3 text-gray-400"></i>
<input type="email" id="regEmail" placeholder="选填"
class="w-full pl-10 pr-4 py-3 border border-gray-200 rounded-lg focus:ring-2 focus:ring-purple-500">
</div>
</div>
<div>
<label class="block text-sm text-gray-600 mb-1">密码 <span class="text-red-500">*</span></label>
<div class="relative">
<i class="ri-lock-line absolute left-3 top-3 text-gray-400"></i>
<input type="password" id="regPassword" placeholder="至少6个字符"
class="w-full pl-10 pr-4 py-3 border border-gray-200 rounded-lg focus:ring-2 focus:ring-purple-500">
</div>
</div>
<div>
<label class="block text-sm text-gray-600 mb-1">确认密码 <span class="text-red-500">*</span></label>
<div class="relative">
<i class="ri-lock-2-line absolute left-3 top-3 text-gray-400"></i>
<input type="password" id="regConfirmPassword" placeholder="再次输入密码"
class="w-full pl-10 pr-4 py-3 border border-gray-200 rounded-lg focus:ring-2 focus:ring-purple-500">
</div>
</div>
<button type="submit"
class="w-full py-3 gradient-bg text-white rounded-lg font-medium hover:opacity-90">
注册
</button>
</form>
<!-- 错误提示 -->
<div id="errorMsg" class="mt-4 p-3 bg-red-50 text-red-500 rounded-lg text-sm hidden"></div>
</div>
</div>
<!-- 底部 -->
<div class="text-center p-4 text-gray-400 text-sm">
方言AI助手 © 2026
</div>
</div>
<script>
const API_URL = '';
let currentTab = 'login';
function showTab(tab) {
currentTab = tab;
document.getElementById('loginForm').classList.toggle('hidden', tab !== 'login');
document.getElementById('registerForm').classList.toggle('hidden', tab !== 'register');
document.getElementById('loginTab').classList.toggle('bg-white', tab === 'login');
document.getElementById('loginTab').classList.toggle('shadow', tab === 'login');
document.getElementById('loginTab').classList.toggle('text-gray-800', tab === 'login');
document.getElementById('loginTab').classList.toggle('text-gray-500', tab !== 'login');
document.getElementById('registerTab').classList.toggle('bg-white', tab === 'register');
document.getElementById('registerTab').classList.toggle('shadow', tab === 'register');
document.getElementById('registerTab').classList.toggle('text-gray-800', tab === 'register');
document.getElementById('registerTab').classList.toggle('text-gray-500', tab !== 'register');
document.getElementById('errorMsg').classList.add('hidden');
}
function showError(msg) {
const el = document.getElementById('errorMsg');
el.textContent = msg;
el.classList.remove('hidden');
}
// 登录
document.getElementById('loginForm').addEventListener('submit', async (e) => {
e.preventDefault();
const username = document.getElementById('loginUsername').value.trim();
const password = document.getElementById('loginPassword').value;
if (!username || !password) {
showError('请输入用户名和密码');
return;
}
try {
const res = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const data = await res.json();
if (data.success) {
localStorage.setItem('token', data.token);
localStorage.setItem('user', JSON.stringify(data.user));
window.location.href = '/chat';
} else {
showError(data.error);
}
} catch (err) {
showError('网络错误,请重试');
}
});
// 注册
document.getElementById('registerForm').addEventListener('submit', async (e) => {
e.preventDefault();
const username = document.getElementById('regUsername').value.trim();
const phone = document.getElementById('regPhone').value.trim();
const email = document.getElementById('regEmail').value.trim();
const password = document.getElementById('regPassword').value;
const confirm_password = document.getElementById('regConfirmPassword').value;
if (!username || !phone || !password || !confirm_password) {
showError('请填写必填项');
return;
}
try {
const res = await fetch('/api/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, phone, email, password, confirm_password })
});
const data = await res.json();
if (data.success) {
localStorage.setItem('token', data.token);
localStorage.setItem('user', JSON.stringify(data.user));
window.location.href = '/chat';
} else {
showError(data.error);
}
} catch (err) {
showError('网络错误,请重试');
}
});
// 检查是否已登录
if (localStorage.getItem('token')) {
window.location.href = '/chat';
}
</script>
</body>
</html>
+5
View File
@@ -0,0 +1,5 @@
flask>=2.3.0
flask-cors>=4.0.0
requests>=2.28.0
pyjwt>=2.8.0
werkzeug>=2.3.0