This commit is contained in:
eust-w 2025-06-17 20:14:11 +08:00
parent 6d3e07ce35
commit 31581e24a9
8 changed files with 993 additions and 16 deletions

173
app.py
View File

@ -4,13 +4,15 @@
import os
import json
import base64
from flask import Flask, render_template, request, jsonify, redirect, url_for, session
import requests
from flask import Flask, render_template, request, jsonify, redirect, url_for, session, Response
from flask_cors import CORS
from werkzeug.utils import secure_filename
from app.api.baidu_image_search import BaiduImageSearch
from app.api.image_utils import ImageUtils
from app.api.azure_openai import AzureOpenAI
from app.api.type_manager_mongo import TypeManagerMongo
from app.api.robot_manager import RobotManager
app = Flask(__name__, template_folder='app/templates', static_folder='app/static')
CORS(app) # 启用CORS支持跨域请求
@ -40,6 +42,9 @@ except ValueError as e:
# 初始化类型管理器
type_manager = TypeManagerMongo()
# 初始化机器人角色管理器
robot_manager = RobotManager()
def allowed_file(filename):
"""检查文件扩展名是否允许"""
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@ -231,6 +236,9 @@ def upload_chat_image():
if not allowed_file(file.filename):
return jsonify({'error': '不支持的文件类型'}), 400
# 获取选择的机器人角色ID
robot_id = request.form.get('robot_id', '')
# 保存文件
filename = secure_filename(file.filename)
file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
@ -239,6 +247,9 @@ def upload_chat_image():
# 存储图片路径到会话
session['chat_image_path'] = file_path
# 存储机器人角色ID到会话
session['robot_id'] = robot_id
# 清空对话历史
session['conversation_history'] = []
@ -269,11 +280,22 @@ def upload_chat_image():
except:
continue
# 获取机器人角色信息
robot_info = None
if robot_id:
robot = robot_manager.get_robot(robot_id)
if robot:
robot_info = {
'name': robot.get('name', ''),
'background': robot.get('background', '')
}
return jsonify({
'success': True,
'image_path': file_path,
'image_type': image_type,
'description': description
'description': description,
'robot': robot_info
})
except Exception as e:
return jsonify({'error': str(e)}), 500
@ -290,6 +312,7 @@ def chat():
message = data['message']
image_path = session.get('chat_image_path')
robot_id = session.get('robot_id', '')
if not image_path or not os.path.exists(image_path):
return jsonify({'error': '没有上传图片或图片已失效'}), 400
@ -297,12 +320,23 @@ def chat():
# 获取对话历史
conversation_history = session.get('conversation_history', [])
# 获取机器人角色信息
robot_info = None
if robot_id:
robot = robot_manager.get_robot(robot_id)
if robot:
robot_info = {
'name': robot.get('name', ''),
'background': robot.get('background', '')
}
try:
# 调用Azure OpenAI API进行对话
response = azure_openai_api.chat_with_image(
image_path=image_path,
message=message,
conversation_history=conversation_history
conversation_history=conversation_history,
robot_info=robot_info
)
# 提取回复内容
@ -320,5 +354,138 @@ def chat():
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/imgsearcherApi/robots', methods=['GET'])
def robot_page():
"""机器人角色管理页面"""
return render_template('robots.html')
@app.route('/imgsearcherApi/api/robots', methods=['GET'])
def get_robots():
"""获取所有机器人角色"""
try:
robots = robot_manager.get_all_robots()
return jsonify({
'success': True,
'robots': robots
})
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/imgsearcherApi/api/robots', methods=['POST'])
def add_robot():
"""添加新的机器人角色"""
try:
name = request.form.get('name')
background = request.form.get('background')
if not name or not background:
return jsonify({'error': '机器人名称和背景故事不能为空'}), 400
avatar_file = None
if 'avatar' in request.files:
avatar_file = request.files['avatar']
if avatar_file.filename == '':
avatar_file = None
elif not allowed_file(avatar_file.filename):
return jsonify({'error': '不支持的文件类型'}), 400
result = robot_manager.add_robot(name, background, avatar_file)
if 'error' in result:
return jsonify(result), 400
return jsonify({
'success': True,
'robot': result
})
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/imgsearcherApi/api/robots/<robot_id>', methods=['GET'])
def get_robot(robot_id):
"""获取指定机器人角色"""
try:
robot = robot_manager.get_robot(robot_id)
if not robot:
return jsonify({'error': '找不到该机器人'}), 404
return jsonify({
'success': True,
'robot': robot
})
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/imgsearcherApi/api/robots/<robot_id>', methods=['PUT'])
def update_robot(robot_id):
"""更新机器人角色"""
try:
name = request.form.get('name')
background = request.form.get('background')
avatar_file = None
if 'avatar' in request.files:
avatar_file = request.files['avatar']
if avatar_file.filename == '':
avatar_file = None
elif not allowed_file(avatar_file.filename):
return jsonify({'error': '不支持的文件类型'}), 400
result = robot_manager.update_robot(robot_id, name, background, avatar_file)
if 'error' in result:
return jsonify(result), 400
return jsonify({
'success': True,
'robot': result
})
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/imgsearcherApi/api/robots/<robot_id>', methods=['DELETE'])
def delete_robot(robot_id):
"""删除机器人角色"""
try:
success = robot_manager.delete_robot(robot_id)
if not success:
return jsonify({'error': '找不到该机器人或删除失败'}), 404
return jsonify({
'success': True
})
except Exception as e:
return jsonify({'error': str(e)}), 500
# ==================== TTS ====================
@app.route('/imgsearcherApi/api/tts', methods=['POST'])
def tts():
"""文本转语音接口,返回音频数据流"""
data = request.get_json()
if not data or 'text' not in data:
return jsonify({'error': '缺少text参数'}), 400
text = data['text']
prompt_text = data.get('prompt_text') or "我是威震天,我只代表月亮消灭你"
prompt_wav = data.get('prompt_wav') or "data_workspace/data/workspace_170/我是威震天.wav"
tts_base_url = "http://180.76.186.85:20099/CosyVoice/v1/zero_shot"
params = {
'tts_text': text,
'prompt_text': prompt_text,
'prompt_wav': prompt_wav,
'text_split_method': 'cut5'
}
try:
r = requests.get(tts_base_url, params=params, timeout=20)
if r.status_code == 200:
return Response(r.content, content_type=r.headers.get('Content-Type', 'audio/wav'))
else:
print(f"TTS remote error {r.status_code}: {r.text}")
return jsonify({'error': f'TTS服务错误: {r.text}'}), 502
except Exception as e:
return jsonify({'error': f'TTS请求失败: {str(e)}'}), 500
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=5001)

View File

@ -85,7 +85,7 @@ class AzureOpenAI:
print(f"获取图片类型和描述失败: {str(e)}")
return "", ""
def chat_with_image(self, image_path, message, conversation_history=None):
def chat_with_image(self, image_path, message, conversation_history=None, robot_info=None):
"""
使用图片和消息与GPT-4o多模态模型进行对话
@ -93,6 +93,7 @@ class AzureOpenAI:
image_path: 图片路径
message: 用户消息
conversation_history: 对话历史记录
robot_info: 机器人角色信息包含name和background
Returns:
dict: 模型响应
@ -107,7 +108,14 @@ class AzureOpenAI:
base64_image = self._encode_image(image_path)
# 构建系统提示
system_message = "你是一个智能助手,能够分析图片并回答问题。"
if robot_info and robot_info.get('name') and robot_info.get('background'):
# 如果有机器人角色信息,使用机器人角色
system_message = f"你是{robot_info['name']},一个能够分析图片并回答问题的角色。\n\n你的背景故事:{robot_info['background']}\n\n在对话中,你应该始终保持这个角色的身份和特点,用第一人称回答问题。"
else:
# 默认智能助手
system_message = "你是一个智能助手,能够分析图片并回答问题。"
# 添加图片类型和描述信息
if image_type and description:
system_message += f"\n\n这是一张{image_type}的图片。\n描述信息:{description}\n\n请基于这些信息和图片内容回答用户的问题。"

217
app/api/robot_manager.py Normal file
View File

@ -0,0 +1,217 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import datetime
from pymongo import MongoClient
from dotenv import load_dotenv
import base64
import uuid
# 加载环境变量
load_dotenv()
class RobotManager:
"""管理机器人角色的类"""
def __init__(self):
"""初始化MongoDB连接"""
# 从环境变量获取MongoDB连接信息
mongo_uri = os.getenv('MONGO_URI', 'mongodb://localhost:27017/')
db_name = os.getenv('MONGO_DB_NAME', 'imgsearcher')
# 连接MongoDB
self.client = MongoClient(mongo_uri)
self.db = self.client[db_name]
self.collection = self.db['robot_characters']
# 确保有索引以提高查询性能
self.collection.create_index('robot_id')
self.collection.create_index('name')
# 上传文件夹路径
self.upload_folder = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), 'uploads')
if not os.path.exists(self.upload_folder):
os.makedirs(self.upload_folder)
print(f"MongoDB RobotManager 初始化完成,连接到 {db_name}")
def add_robot(self, name, background, avatar_file=None):
"""
添加新的机器人角色
Args:
name: 机器人名称
background: 机器人背景故事
avatar_file: 机器人头像文件可选
Returns:
dict: 包含新添加的机器人信息
"""
if not name or not background:
return {"error": "机器人名称和背景故事不能为空"}
# 检查是否已存在同名机器人
if self.collection.find_one({"name": name}):
return {"error": f"已存在名为 '{name}' 的机器人"}
# 生成唯一ID
robot_id = str(uuid.uuid4())
# 处理头像文件
avatar_path = None
if avatar_file:
# 保存头像文件
filename = f"robot_avatar_{robot_id}.{avatar_file.filename.split('.')[-1]}"
file_path = os.path.join(self.upload_folder, filename)
avatar_file.save(file_path)
avatar_path = file_path
# 创建机器人记录
robot = {
"robot_id": robot_id,
"name": name,
"background": background,
"avatar_path": avatar_path,
"created_at": datetime.datetime.now()
}
# 保存到数据库
self.collection.insert_one(robot)
# 返回新添加的机器人信息不包含MongoDB的_id字段
robot.pop("_id", None)
return robot
def get_robot(self, robot_id):
"""
获取指定ID的机器人信息
Args:
robot_id: 机器人ID
Returns:
dict: 机器人信息如果不存在则返回None
"""
robot = self.collection.find_one({"robot_id": robot_id})
if robot:
# 转换MongoDB的_id为字符串
robot["_id"] = str(robot["_id"])
# 如果有头像添加base64编码的头像数据
if robot.get("avatar_path") and os.path.exists(robot["avatar_path"]):
with open(robot["avatar_path"], "rb") as f:
avatar_data = base64.b64encode(f.read()).decode('utf-8')
robot["avatar_data"] = avatar_data
return robot
return None
def update_robot(self, robot_id, name=None, background=None, avatar_file=None):
"""
更新机器人信息
Args:
robot_id: 机器人ID
name: 新的机器人名称可选
background: 新的背景故事可选
avatar_file: 新的头像文件可选
Returns:
dict: 更新后的机器人信息
"""
# 获取现有机器人
robot = self.collection.find_one({"robot_id": robot_id})
if not robot:
return {"error": f"找不到ID为 '{robot_id}' 的机器人"}
# 准备更新数据
update_data = {}
if name:
# 检查新名称是否与其他机器人冲突
existing = self.collection.find_one({"name": name, "robot_id": {"$ne": robot_id}})
if existing:
return {"error": f"已存在名为 '{name}' 的机器人"}
update_data["name"] = name
if background:
update_data["background"] = background
# 处理新头像
if avatar_file:
# 删除旧头像
old_avatar_path = robot.get("avatar_path")
if old_avatar_path and os.path.exists(old_avatar_path):
try:
os.remove(old_avatar_path)
except:
pass
# 保存新头像
filename = f"robot_avatar_{robot_id}.{avatar_file.filename.split('.')[-1]}"
file_path = os.path.join(self.upload_folder, filename)
avatar_file.save(file_path)
update_data["avatar_path"] = file_path
# 更新数据库
if update_data:
update_data["updated_at"] = datetime.datetime.now()
self.collection.update_one({"robot_id": robot_id}, {"$set": update_data})
# 返回更新后的机器人信息
return self.get_robot(robot_id)
def delete_robot(self, robot_id):
"""
删除机器人
Args:
robot_id: 机器人ID
Returns:
bool: 是否成功删除
"""
# 获取机器人信息
robot = self.collection.find_one({"robot_id": robot_id})
if not robot:
return False
# 删除头像文件
avatar_path = robot.get("avatar_path")
if avatar_path and os.path.exists(avatar_path):
try:
os.remove(avatar_path)
except:
pass
# 从数据库中删除
result = self.collection.delete_one({"robot_id": robot_id})
return result.deleted_count > 0
def get_all_robots(self):
"""
获取所有机器人列表
Returns:
list: 机器人信息列表
"""
robots = []
for robot in self.collection.find():
# 转换MongoDB的_id为字符串
robot["_id"] = str(robot["_id"])
# 如果有头像添加base64编码的头像数据
if robot.get("avatar_path") and os.path.exists(robot["avatar_path"]):
with open(robot["avatar_path"], "rb") as f:
avatar_data = base64.b64encode(f.read()).decode('utf-8')
robot["avatar_data"] = avatar_data
robots.append(robot)
return robots
def close(self):
"""关闭MongoDB连接"""
if self.client:
self.client.close()

View File

@ -1,5 +1,8 @@
// 页面加载完成后执行
document.addEventListener('DOMContentLoaded', function() {
// 加载机器人角色列表
loadRobotCharacters();
// 初始化图片上传表单
initUploadImageForm();
@ -7,6 +10,28 @@ document.addEventListener('DOMContentLoaded', function() {
initChatForm();
});
// 加载机器人角色列表
async function loadRobotCharacters() {
const robotSelect = document.getElementById('robotSelect');
try {
const response = await fetch('/imgsearcherApi/api/robots');
const data = await response.json();
if (data.robots && data.robots.length > 0) {
// 添加机器人选项
data.robots.forEach(robot => {
const option = document.createElement('option');
option.value = robot.robot_id;
option.textContent = robot.name;
robotSelect.appendChild(option);
});
}
} catch (error) {
console.error('加载机器人角色失败:', error);
}
}
// 初始化图片上传表单
function initUploadImageForm() {
const form = document.getElementById('uploadImageForm');
@ -74,14 +99,31 @@ function initUploadImageForm() {
// 显示聊天界面
chatContainer.classList.remove('d-none');
// 添加欢迎消息
// 显示欢迎消息
const chatMessages = document.getElementById('chatMessages');
chatMessages.innerHTML = '';
const welcomeMessage = document.createElement('div');
welcomeMessage.className = 'message message-assistant';
let welcomeText = '你好我是AI助手可以帮你分析这张图片并回答问题。';
let welcomeText = '';
// 如果选择了机器人角色,使用机器人的自我介绍
if (data.robot) {
welcomeText = `你好!我是${data.robot.name}`;
// 存储机器人信息到页面数据中
document.getElementById('chatContainer').dataset.robotName = data.robot.name;
// 显示机器人信息在界面上
const robotInfoElement = document.createElement('div');
robotInfoElement.className = 'alert alert-info mt-2 mb-3';
robotInfoElement.innerHTML = `<strong>当前角色:${data.robot.name}</strong><br><small>${data.robot.background}</small>`;
document.getElementById('chatContainer').prepend(robotInfoElement);
} else {
welcomeText = '你好我是AI助手';
}
welcomeText += '可以帮你分析这张图片并回答问题。';
if (data.image_type && data.description) {
welcomeText += `我看到这是一张${data.image_type}的图片,${data.description}。你有什么想问的吗?`;
} else {
@ -171,7 +213,7 @@ function initChatForm() {
}
// 添加消息到聊天界面
function addMessage(role, content) {
async function addMessage(role, content) {
const chatMessages = document.getElementById('chatMessages');
const messageElement = document.createElement('div');
@ -184,6 +226,44 @@ function addMessage(role, content) {
chatMessages.appendChild(messageElement);
// 如果是AI回复调用TTS并播放
if (role === 'assistant') {
try {
const ttsResp = await fetch('/imgsearcherApi/api/tts', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
text: content
})
});
if (ttsResp.ok) {
const blob = await ttsResp.blob();
const audioUrl = URL.createObjectURL(blob);
const audio = document.createElement('audio');
audio.src = audioUrl;
audio.autoplay = true;
// 可选:提供播放控制按钮
const controls = document.createElement('div');
controls.className = 'audio-controls mt-1';
const playBtn = document.createElement('button');
playBtn.type = 'button';
playBtn.className = 'btn btn-sm btn-outline-secondary';
playBtn.textContent = '🔊 播放';
playBtn.addEventListener('click', () => {
audio.play();
});
controls.appendChild(playBtn);
messageElement.appendChild(controls);
} else {
console.error('TTS 接口错误', await ttsResp.text());
}
} catch (err) {
console.error('TTS 播放失败', err);
}
}
// 滚动到底部
chatMessages.scrollTop = chatMessages.scrollHeight;
}

297
app/static/js/robots.js Normal file
View File

@ -0,0 +1,297 @@
// 页面加载完成后执行
document.addEventListener('DOMContentLoaded', function() {
// 初始化机器人表单
initRobotForm();
// 加载机器人列表
loadRobots();
// 初始化编辑和删除功能
initEditRobotModal();
initDeleteRobotModal();
});
// 初始化机器人表单
function initRobotForm() {
const form = document.getElementById('robotForm');
const avatarInput = document.getElementById('robotAvatar');
const avatarPreview = document.getElementById('avatarPreview');
const avatarPreviewContainer = document.querySelector('#robotForm .avatar-preview');
// 头像预览
avatarInput.addEventListener('change', function() {
if (this.files && this.files[0]) {
const reader = new FileReader();
reader.onload = function(e) {
avatarPreview.src = e.target.result;
avatarPreviewContainer.classList.remove('d-none');
};
reader.readAsDataURL(this.files[0]);
} else {
avatarPreviewContainer.classList.add('d-none');
}
});
// 表单提交
form.addEventListener('submit', async function(e) {
e.preventDefault();
const formData = new FormData(this);
// 显示提交中状态
const submitButton = form.querySelector('button[type="submit"]');
const originalButtonText = submitButton.innerHTML;
submitButton.disabled = true;
submitButton.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 添加中...';
try {
const response = await fetch('/imgsearcherApi/api/robots', {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.error) {
alert(`添加失败: ${data.error}`);
return;
}
// 重置表单
form.reset();
avatarPreviewContainer.classList.add('d-none');
// 刷新机器人列表
loadRobots();
// 显示成功消息
alert('机器人角色添加成功!');
} catch (error) {
alert(`添加失败: ${error.message}`);
} finally {
// 恢复按钮状态
submitButton.disabled = false;
submitButton.innerHTML = originalButtonText;
}
});
}
// 加载机器人列表
async function loadRobots() {
const robotsList = document.getElementById('robotsList');
const loadingIndicator = document.getElementById('loadingIndicator');
const noRobotsMessage = document.getElementById('noRobotsMessage');
// 显示加载指示器
loadingIndicator.classList.remove('d-none');
noRobotsMessage.classList.add('d-none');
// 清除现有的机器人卡片(保留标题和加载指示器)
const existingCards = robotsList.querySelectorAll('.robot-card-container');
existingCards.forEach(card => card.remove());
try {
const response = await fetch('/imgsearcherApi/api/robots');
const data = await response.json();
// 隐藏加载指示器
loadingIndicator.classList.add('d-none');
if (!data.robots || data.robots.length === 0) {
noRobotsMessage.classList.remove('d-none');
return;
}
// 渲染机器人卡片
data.robots.forEach(robot => {
const robotCard = createRobotCard(robot);
robotsList.appendChild(robotCard);
});
} catch (error) {
console.error('加载机器人列表失败:', error);
loadingIndicator.classList.add('d-none');
alert(`加载机器人列表失败: ${error.message}`);
}
}
// 创建机器人卡片
function createRobotCard(robot) {
const cardContainer = document.createElement('div');
cardContainer.className = 'col-md-4 mb-4 robot-card-container';
const avatarHtml = robot.avatar_data
? `<img src="data:image/jpeg;base64,${robot.avatar_data}" alt="${robot.name}" class="robot-avatar">`
: `<div class="robot-avatar-placeholder"><i class="bi bi-person"></i></div>`;
cardContainer.innerHTML = `
<div class="card robot-card">
<div class="card-body text-center">
${avatarHtml}
<h5 class="card-title">${robot.name}</h5>
<div class="robot-background text-start mb-3">
<small class="text-muted">${robot.background}</small>
</div>
<div class="btn-group" role="group">
<button type="button" class="btn btn-sm btn-outline-primary edit-robot" data-robot-id="${robot.robot_id}">
<i class="bi bi-pencil"></i>
</button>
<button type="button" class="btn btn-sm btn-outline-danger delete-robot" data-robot-id="${robot.robot_id}">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
</div>
`;
// 添加编辑按钮事件
cardContainer.querySelector('.edit-robot').addEventListener('click', function() {
openEditRobotModal(robot);
});
// 添加删除按钮事件
cardContainer.querySelector('.delete-robot').addEventListener('click', function() {
openDeleteRobotModal(robot.robot_id, robot.name);
});
return cardContainer;
}
// 初始化编辑机器人模态框
function initEditRobotModal() {
const editRobotForm = document.getElementById('editRobotForm');
const saveChangesButton = document.getElementById('saveRobotChanges');
const editRobotAvatar = document.getElementById('editRobotAvatar');
const editAvatarPreview = document.getElementById('editAvatarPreview');
// 头像预览
editRobotAvatar.addEventListener('change', function() {
if (this.files && this.files[0]) {
const reader = new FileReader();
reader.onload = function(e) {
editAvatarPreview.src = e.target.result;
};
reader.readAsDataURL(this.files[0]);
}
});
// 保存更改按钮点击事件
saveChangesButton.addEventListener('click', async function() {
const robotId = document.getElementById('editRobotId').value;
const formData = new FormData(editRobotForm);
// 显示保存中状态
const originalButtonText = saveChangesButton.innerHTML;
saveChangesButton.disabled = true;
saveChangesButton.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 保存中...';
try {
const response = await fetch(`/imgsearcherApi/api/robots/${robotId}`, {
method: 'PUT',
body: formData
});
const data = await response.json();
if (data.error) {
alert(`更新失败: ${data.error}`);
return;
}
// 关闭模态框
const editModal = bootstrap.Modal.getInstance(document.getElementById('editRobotModal'));
editModal.hide();
// 刷新机器人列表
loadRobots();
// 显示成功消息
alert('机器人角色更新成功!');
} catch (error) {
alert(`更新失败: ${error.message}`);
} finally {
// 恢复按钮状态
saveChangesButton.disabled = false;
saveChangesButton.innerHTML = originalButtonText;
}
});
}
// 打开编辑机器人模态框
function openEditRobotModal(robot) {
document.getElementById('editRobotId').value = robot.robot_id;
document.getElementById('editRobotName').value = robot.name;
document.getElementById('editRobotBackground').value = robot.background;
// 设置头像预览
if (robot.avatar_data) {
document.getElementById('editAvatarPreview').src = `data:image/jpeg;base64,${robot.avatar_data}`;
} else {
document.getElementById('editAvatarPreview').src = '';
}
// 打开模态框
const editModal = new bootstrap.Modal(document.getElementById('editRobotModal'));
editModal.show();
}
// 初始化删除机器人模态框
function initDeleteRobotModal() {
const confirmDeleteButton = document.getElementById('confirmDeleteRobot');
confirmDeleteButton.addEventListener('click', async function() {
const robotId = document.getElementById('deleteRobotId').value;
// 显示删除中状态
const originalButtonText = confirmDeleteButton.innerHTML;
confirmDeleteButton.disabled = true;
confirmDeleteButton.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 删除中...';
try {
const response = await fetch(`/imgsearcherApi/api/robots/${robotId}`, {
method: 'DELETE'
});
const data = await response.json();
if (data.error) {
alert(`删除失败: ${data.error}`);
return;
}
// 关闭模态框
const deleteModal = bootstrap.Modal.getInstance(document.getElementById('deleteRobotModal'));
deleteModal.hide();
// 刷新机器人列表
loadRobots();
// 显示成功消息
alert('机器人角色删除成功!');
} catch (error) {
alert(`删除失败: ${error.message}`);
} finally {
// 恢复按钮状态
confirmDeleteButton.disabled = false;
confirmDeleteButton.innerHTML = originalButtonText;
}
});
}
// 打开删除机器人模态框
function openDeleteRobotModal(robotId, robotName) {
document.getElementById('deleteRobotId').value = robotId;
document.getElementById('deleteRobotModalLabel').textContent = `确认删除 "${robotName}"`;
document.querySelector('#deleteRobotModal .modal-body p').textContent = `确定要删除机器人角色 "${robotName}" 吗?此操作不可撤销。`;
// 打开模态框
const deleteModal = new bootstrap.Modal(document.getElementById('deleteRobotModal'));
deleteModal.show();
}

View File

@ -25,12 +25,29 @@
<div class="card-body">
<h5 class="card-title">上传图片</h5>
<form id="uploadImageForm" enctype="multipart/form-data">
<div class="mb-3">
<label for="chatImageFile" class="form-label">选择图片</label>
<input class="form-control" type="file" id="chatImageFile" name="file" accept="image/*" required>
<div class="form-text">支持JPG、PNG、JPEG、GIF格式</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="chatImageFile" class="form-label">选择图片</label>
<input class="form-control" type="file" id="chatImageFile" name="file" accept="image/*" required>
<div class="form-text">支持JPG、PNG、JPEG、GIF格式</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="robotSelect" class="form-label">选择机器人角色(可选)</label>
<select class="form-select" id="robotSelect" name="robot_id">
<option value="">默认AI助手</option>
<!-- 机器人选项将在这里动态添加 -->
</select>
<div class="form-text">选择一个机器人角色进行对话</div>
</div>
</div>
</div>
<div class="d-flex justify-content-between">
<button type="submit" class="btn btn-primary">上传并开始对话</button>
<a href="/imgsearcherApi/robots" class="btn btn-outline-secondary">管理机器人角色</a>
</div>
<button type="submit" class="btn btn-primary">上传并开始对话</button>
</form>
<div id="uploadPreview" class="mt-3 text-center d-none">
<div class="preview-container">

View File

@ -29,6 +29,9 @@
<li class="nav-item" role="presentation">
<a href="/imgsearcherApi/chat-with-image" class="nav-link" style="color: #0d6efd; font-weight: bold;">图片智能对话</a>
</li>
<li class="nav-item" role="presentation">
<a href="/imgsearcherApi/robots" class="nav-link" style="color: #198754; font-weight: bold;">机器人角色管理</a>
</li>
</ul>
<div class="tab-content" id="myTabContent">
<!-- 待识别角色录入 -->
@ -43,15 +46,17 @@
<div class="form-text">支持JPG、PNG、BMP格式最短边至少50px最长边最大4096px</div>
</div>
<div class="mb-3">
<label for="imageType" class="form-label">类型这个实际就是宠物名类似小狗pet、小猫pussy之类的</label>
<label for="imageType" class="form-label">角色名称</label>
<input type="text" class="form-control" id="imageType" name="type" required>
</div>
<div class="mb-3">
<label for="imageDescription" class="form-label">描述(目前demo只支持250个字符后面正式版可以扩充这个实际是角色卡信息,也就是宠物的描述,类似小狗是金毛之类的</label>
<label for="imageDescription" class="form-label">描述(角色卡信息)</label>
<textarea class="form-control" id="imageDescription" name="description" rows="2" required></textarea>
</div>
<div class="mb-3">
<label for="imageName" class="form-label">名称(这个实际是图片名称,直接和宠物名重名即可,我会给加个名字)</label>
<label for="imageName" class="form-label">图片名称
</label>
<input type="text" class="form-control" id="imageName" name="name" required>
</div>
<div class="mb-3">

186
app/templates/robots.html Normal file
View File

@ -0,0 +1,186 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>机器人角色管理 - 地瓜机器人</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.1/font/bootstrap-icons.css">
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<style>
.robot-card {
height: 100%;
transition: transform 0.3s;
}
.robot-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
}
.robot-avatar {
width: 100px;
height: 100px;
object-fit: cover;
border-radius: 50%;
margin-bottom: 15px;
}
.robot-avatar-placeholder {
width: 100px;
height: 100px;
border-radius: 50%;
background-color: #e9ecef;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 15px;
font-size: 2rem;
color: #6c757d;
}
.robot-background {
max-height: 150px;
overflow-y: auto;
}
#robotForm .avatar-preview {
width: 100px;
height: 100px;
border-radius: 50%;
overflow: hidden;
margin-top: 10px;
}
#robotForm .avatar-preview img {
width: 100%;
height: 100%;
object-fit: cover;
}
</style>
</head>
<body>
<div class="container">
<header class="text-center my-4">
<h1>机器人角色管理</h1>
<p class="lead">创建和管理您的机器人角色</p>
<nav class="mt-3">
<a href="/imgsearcherApi" class="btn btn-outline-secondary me-2">返回首页</a>
<a href="/imgsearcherApi/chat-with-image" class="btn btn-outline-primary">图片智能对话</a>
</nav>
</header>
<div class="row mb-4">
<div class="col-md-12">
<div class="card">
<div class="card-body">
<h5 class="card-title">添加新机器人角色</h5>
<form id="robotForm" enctype="multipart/form-data">
<div class="row">
<div class="col-md-4">
<div class="mb-3">
<label for="robotName" class="form-label">机器人名称</label>
<input type="text" class="form-control" id="robotName" name="name" required>
</div>
<div class="mb-3">
<label for="robotAvatar" class="form-label">头像(可选)</label>
<input class="form-control" type="file" id="robotAvatar" name="avatar" accept="image/*">
<div class="avatar-preview d-none mt-2">
<img id="avatarPreview" src="" alt="头像预览">
</div>
</div>
</div>
<div class="col-md-8">
<div class="mb-3">
<label for="robotBackground" class="form-label">背景故事</label>
<textarea class="form-control" id="robotBackground" name="background" rows="5" required></textarea>
<div class="form-text">描述机器人的性格、背景和特点,这些信息将用于图片对话中</div>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary">
<i class="bi bi-plus-circle"></i> 添加机器人
</button>
</form>
</div>
</div>
</div>
</div>
<div class="row" id="robotsList">
<div class="col-12 mb-4">
<h3>现有机器人角色</h3>
<div class="text-center py-5 d-none" id="loadingIndicator">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">加载中...</span>
</div>
<p class="mt-2">加载机器人角色...</p>
</div>
<div class="alert alert-info d-none" id="noRobotsMessage">
暂无机器人角色,请添加一个新的机器人角色。
</div>
</div>
<!-- 机器人卡片将在这里动态生成 -->
</div>
</div>
<!-- 编辑机器人模态框 -->
<div class="modal fade" id="editRobotModal" tabindex="-1" aria-labelledby="editRobotModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editRobotModalLabel">编辑机器人角色</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="editRobotForm" enctype="multipart/form-data">
<input type="hidden" id="editRobotId">
<div class="row">
<div class="col-md-4">
<div class="mb-3">
<label for="editRobotName" class="form-label">机器人名称</label>
<input type="text" class="form-control" id="editRobotName" name="name" required>
</div>
<div class="mb-3">
<label for="editRobotAvatar" class="form-label">头像(可选)</label>
<input class="form-control" type="file" id="editRobotAvatar" name="avatar" accept="image/*">
<div class="avatar-preview mt-2">
<img id="editAvatarPreview" src="" alt="头像预览">
</div>
</div>
</div>
<div class="col-md-8">
<div class="mb-3">
<label for="editRobotBackground" class="form-label">背景故事</label>
<textarea class="form-control" id="editRobotBackground" name="background" rows="5" required></textarea>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="saveRobotChanges">保存更改</button>
</div>
</div>
</div>
</div>
<!-- 确认删除模态框 -->
<div class="modal fade" id="deleteRobotModal" tabindex="-1" aria-labelledby="deleteRobotModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteRobotModalLabel">确认删除</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>确定要删除这个机器人角色吗?此操作不可撤销。</p>
<input type="hidden" id="deleteRobotId">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-danger" id="confirmDeleteRobot">删除</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="{{ url_for('static', filename='js/robots.js') }}"></script>
</body>
</html>