'commit'
This commit is contained in:
@@ -36,8 +36,8 @@ logger.addHandler(handler)
|
|||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(_: FastAPI):
|
async def lifespan(_: FastAPI):
|
||||||
pool = await init_postgres_pool()
|
#pool = await init_postgres_pool()
|
||||||
app.state.pool = pool
|
#app.state.pool = pool
|
||||||
|
|
||||||
asyncio.create_task(train_document_task())
|
asyncio.create_task(train_document_task())
|
||||||
|
|
||||||
@@ -45,7 +45,8 @@ async def lifespan(_: FastAPI):
|
|||||||
yield
|
yield
|
||||||
finally:
|
finally:
|
||||||
# 应用关闭时销毁连接池
|
# 应用关闭时销毁连接池
|
||||||
await close_postgres_pool(pool)
|
#await close_postgres_pool(pool)
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
app = FastAPI(lifespan=lifespan)
|
app = FastAPI(lifespan=lifespan)
|
||||||
|
@@ -8,7 +8,7 @@
|
|||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
<!-- 引入 Font Awesome -->
|
<!-- 引入 Font Awesome -->
|
||||||
<link href="https://cdn.bootcdn.net/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
|
<link href="https://cdn.bootcdn.net/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
|
||||||
<!-- Tailwind 配置 -->
|
<!-- Tailwind 配置 - 保留在HTML中 -->
|
||||||
<script>
|
<script>
|
||||||
tailwind.config = {
|
tailwind.config = {
|
||||||
theme: {
|
theme: {
|
||||||
@@ -30,6 +30,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
<!-- 引入外部JS文件 -->
|
||||||
|
<script src="mj.js"></script>
|
||||||
<style type="text/tailwindcss">
|
<style type="text/tailwindcss">
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
.content-auto {
|
.content-auto {
|
||||||
@@ -57,23 +59,7 @@
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="min-h-screen bg-gradient-to-br from-light to-gray-100 dark:from-dark dark:to-gray-900 text-gray-800 dark:text-gray-100 transition-colors duration-300">
|
<body class="min-h-screen bg-gradient-to-br from-light to-gray-100 dark:from-dark dark:to-gray-900 text-gray-800 dark:text-gray-100 transition-colors duration-300">
|
||||||
<script>
|
|
||||||
function showError(message) {
|
|
||||||
const errorElement = document.getElementById('error-message');
|
|
||||||
if (errorElement) {
|
|
||||||
errorElement.textContent = message;
|
|
||||||
errorElement.classList.remove('hidden');
|
|
||||||
// Show error section if it's hidden
|
|
||||||
const statusSection = document.getElementById('statusSection');
|
|
||||||
if (statusSection) statusSection.classList.remove('hidden');
|
|
||||||
// Auto-hide after 5 seconds
|
|
||||||
setTimeout(() => errorElement.classList.add('hidden'), 5000);
|
|
||||||
} else {
|
|
||||||
console.error('Error: showError - error-message element not found');
|
|
||||||
alert(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<!-- 导航栏 -->
|
<!-- 导航栏 -->
|
||||||
<header class="sticky top-0 z-50 glass-effect dark:glass-effect-dark border-b border-gray-200 dark:border-gray-700 shadow-sm">
|
<header class="sticky top-0 z-50 glass-effect dark:glass-effect-dark border-b border-gray-200 dark:border-gray-700 shadow-sm">
|
||||||
<div class="container mx-auto px-4 py-3 flex justify-between items-center">
|
<div class="container mx-auto px-4 py-3 flex justify-between items-center">
|
||||||
@@ -85,9 +71,7 @@
|
|||||||
<nav class="hidden md:flex items-center space-x-6">
|
<nav class="hidden md:flex items-center space-x-6">
|
||||||
<a href="#generator"
|
<a href="#generator"
|
||||||
class="font-medium hover:text-primary dark:hover:text-primary transition-colors">生成器</a>
|
class="font-medium hover:text-primary dark:hover:text-primary transition-colors">生成器</a>
|
||||||
<a href="#history"
|
<a href="#gallery" class="font-medium hover:text-primary dark:hover:text-primary transition-colors">历史</a>
|
||||||
class="font-medium hover:text-primary dark:hover:text-primary transition-colors">历史记录</a>
|
|
||||||
<a href="#gallery" class="font-medium hover:text-primary dark:hover:text-primary transition-colors">画廊</a>
|
|
||||||
</nav>
|
</nav>
|
||||||
<div class="flex items-center space-x-3">
|
<div class="flex items-center space-x-3">
|
||||||
<button id="themeToggle"
|
<button id="themeToggle"
|
||||||
@@ -106,7 +90,6 @@
|
|||||||
class="hidden md:hidden glass-effect dark:glass-effect-dark border-t border-gray-200 dark:border-gray-700">
|
class="hidden md:hidden glass-effect dark:glass-effect-dark border-t border-gray-200 dark:border-gray-700">
|
||||||
<div class="container mx-auto px-4 py-2 flex flex-col space-y-3">
|
<div class="container mx-auto px-4 py-2 flex flex-col space-y-3">
|
||||||
<a href="#generator" class="font-medium hover:text-primary dark:hover:text-primary transition-colors py-2">生成器</a>
|
<a href="#generator" class="font-medium hover:text-primary dark:hover:text-primary transition-colors py-2">生成器</a>
|
||||||
<a href="#history" class="font-medium hover:text-primary dark:hover:text-primary transition-colors py-2">历史记录</a>
|
|
||||||
<a href="#gallery"
|
<a href="#gallery"
|
||||||
class="font-medium hover:text-primary dark:hover:text-primary transition-colors py-2">画廊</a>
|
class="font-medium hover:text-primary dark:hover:text-primary transition-colors py-2">画廊</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -285,7 +268,7 @@
|
|||||||
<div class="p-6 md:p-8 bg-gray-50 dark:bg-gray-900 flex flex-col md:flex-row justify-between items-center space-y-4 md:space-y-0">
|
<div class="p-6 md:p-8 bg-gray-50 dark:bg-gray-900 flex flex-col md:flex-row justify-between items-center space-y-4 md:space-y-0">
|
||||||
<div class="text-sm text-gray-500 dark:text-gray-400 flex items-center">
|
<div class="text-sm text-gray-500 dark:text-gray-400 flex items-center">
|
||||||
<i class="fa fa-lightbulb-o text-yellow-500 mr-2"></i>
|
<i class="fa fa-lightbulb-o text-yellow-500 mr-2"></i>
|
||||||
<span>提示:生成高质量图像可能需要几秒钟时间</span>
|
<span>提示:生成高质量图像可能需要几十秒钟时间</span>
|
||||||
</div>
|
</div>
|
||||||
<button id="generateBtn"
|
<button id="generateBtn"
|
||||||
class="px-8 py-3 bg-primary hover:bg-primary/90 text-white rounded-lg shadow-lg hover:shadow-xl transform hover:-translate-y-1 transition-all-300 font-medium flex items-center">
|
class="px-8 py-3 bg-primary hover:bg-primary/90 text-white rounded-lg shadow-lg hover:shadow-xl transform hover:-translate-y-1 transition-all-300 font-medium flex items-center">
|
||||||
@@ -345,58 +328,48 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 历史记录区域 -->
|
|
||||||
<section id="history" class="mb-16">
|
|
||||||
<div class="flex justify-between items-center mb-6">
|
|
||||||
<h3 class="text-2xl font-bold">历史记录</h3>
|
|
||||||
<button id="clearHistoryBtn"
|
|
||||||
class="text-sm text-gray-500 dark:text-gray-400 hover:text-red-500 transition-colors">
|
|
||||||
清除全部
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="historyGrid" class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
|
<!-- 历史图像 -->
|
||||||
<!-- 历史记录项会通过 JavaScript 动态添加 -->
|
|
||||||
<div class="col-span-full text-center py-12 text-gray-500 dark:text-gray-400">
|
|
||||||
<i class="fa fa-history text-4xl mb-4"></i>
|
|
||||||
<p>暂无历史记录</p>
|
|
||||||
<p class="text-sm mt-2">开始生成图像以保存到历史记录</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- 画廊区域 -->
|
|
||||||
<section id="gallery" class="mb-16">
|
<section id="gallery" class="mb-16">
|
||||||
<h3 class="text-2xl font-bold mb-6">精选画廊</h3>
|
<h3 class="text-2xl font-bold mb-6">历史图像</h3>
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
|
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
|
||||||
|
<!-- 添加第一张图片 -->
|
||||||
<div class="overflow-hidden rounded-xl shadow-md hover:shadow-xl transition-all-300 group">
|
<div class="overflow-hidden rounded-xl shadow-md hover:shadow-xl transition-all-300 group">
|
||||||
<img src="https://picsum.photos/seed/art1/600/400" alt="艺术作品"
|
<img src="https://ylt.oss-cn-hangzhou.aliyuncs.com/HuangHai/fu_gu_80_nian_dai_feng_ge_de_d0936ca1-6a6b-403f-b547-3ec6852411ba.png"
|
||||||
|
alt="80年代风格的复古汽车"
|
||||||
|
class="w-full h-60 object-cover transition-transform duration-500 group-hover:scale-110">
|
||||||
|
<div class="p-4 bg-white dark:bg-gray-800">
|
||||||
|
<p class="font-medium truncate">80年代风格的复古汽车</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 添加第二张图片 -->
|
||||||
|
<div class="overflow-hidden rounded-xl shadow-md hover:shadow-xl transition-all-300 group">
|
||||||
|
<img src="https://ylt.oss-cn-hangzhou.aliyuncs.com/HuangHai/ke_ai_de_ka_tong_xiong_mao_yu_c5dc58b6-5db2-4785-ab87-3ffb42e55c43.png"
|
||||||
|
alt="可爱的卡通熊猫"
|
||||||
|
class="w-full h-60 object-cover transition-transform duration-500 group-hover:scale-110">
|
||||||
|
<div class="p-4 bg-white dark:bg-gray-800">
|
||||||
|
<p class="font-medium truncate">可爱的卡通熊猫</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 添加第三张图片 -->
|
||||||
|
<div class="overflow-hidden rounded-xl shadow-md hover:shadow-xl transition-all-300 group">
|
||||||
|
<img src="https://ylt.oss-cn-hangzhou.aliyuncs.com/HuangHai/wei_lai_ke_ji_gan_de_pao_che_b3a725a5-d7b1-4cc2-8e27-cfd138aa6fbb.png"
|
||||||
|
alt="未来科技感的跑车"
|
||||||
|
class="w-full h-60 object-cover transition-transform duration-500 group-hover:scale-110">
|
||||||
|
<div class="p-4 bg-white dark:bg-gray-800">
|
||||||
|
<p class="font-medium truncate">未来科技感的跑车</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-hidden rounded-xl shadow-md hover:shadow-xl transition-all-300 group">
|
||||||
|
<img src="https://ylt.oss-cn-hangzhou.aliyuncs.com/HuangHai/zhong_shi_ji_qi_huan_cheng_bao_zuo_8e6c3267-335d-463c-8320-ba3c649140e1.png" alt="艺术作品"
|
||||||
class="w-full h-60 object-cover transition-transform duration-500 group-hover:scale-110">
|
class="w-full h-60 object-cover transition-transform duration-500 group-hover:scale-110">
|
||||||
<div class="p-4 bg-white dark:bg-gray-800">
|
<div class="p-4 bg-white dark:bg-gray-800">
|
||||||
<p class="font-medium truncate">梦幻城堡,超现实主义风格</p>
|
<p class="font-medium truncate">梦幻城堡,超现实主义风格</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="overflow-hidden rounded-xl shadow-md hover:shadow-xl transition-all-300 group">
|
|
||||||
<img src="https://picsum.photos/seed/art2/600/400" alt="艺术作品"
|
|
||||||
class="w-full h-60 object-cover transition-transform duration-500 group-hover:scale-110">
|
|
||||||
<div class="p-4 bg-white dark:bg-gray-800">
|
|
||||||
<p class="font-medium truncate">未来城市,赛博朋克风格</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="overflow-hidden rounded-xl shadow-md hover:shadow-xl transition-all-300 group">
|
|
||||||
<img src="https://picsum.photos/seed/art3/600/400" alt="艺术作品"
|
|
||||||
class="w-full h-60 object-cover transition-transform duration-500 group-hover:scale-110">
|
|
||||||
<div class="p-4 bg-white dark:bg-gray-800">
|
|
||||||
<p class="font-medium truncate">奇幻森林,童话风格</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="overflow-hidden rounded-xl shadow-md hover:shadow-xl transition-all-300 group">
|
|
||||||
<img src="https://picsum.photos/seed/art4/600/400" alt="艺术作品"
|
|
||||||
class="w-full h-60 object-cover transition-transform duration-500 group-hover:scale-110">
|
|
||||||
<div class="p-4 bg-white dark:bg-gray-800">
|
|
||||||
<p class="font-medium truncate">抽象艺术,色彩斑斓</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
@@ -429,571 +402,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
<script>
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
|
||||||
// 主题切换功能
|
|
||||||
const themeToggle = document.getElementById('themeToggle');
|
|
||||||
themeToggle.addEventListener('click', function () {
|
|
||||||
document.documentElement.classList.toggle('dark');
|
|
||||||
localStorage.setItem('theme', document.documentElement.classList.contains('dark') ? 'dark' : 'light');
|
|
||||||
});
|
|
||||||
|
|
||||||
// 检查本地存储的主题偏好
|
|
||||||
if (localStorage.getItem('theme') === 'dark' || (!localStorage.getItem('theme') && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
|
||||||
document.documentElement.classList.add('dark');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 移动端菜单切换
|
|
||||||
const menuToggle = document.getElementById('menuToggle');
|
|
||||||
const mobileMenu = document.getElementById('mobileMenu');
|
|
||||||
menuToggle.addEventListener('click', function () {
|
|
||||||
mobileMenu.classList.toggle('hidden');
|
|
||||||
});
|
|
||||||
|
|
||||||
// 选项卡切换
|
|
||||||
const textToImageTab = document.getElementById('textToImageTab');
|
|
||||||
const imageToImageTab = document.getElementById('imageToImageTab');
|
|
||||||
const textToImagePanel = document.getElementById('textToImagePanel');
|
|
||||||
const imageToImagePanel = document.getElementById('imageToImagePanel');
|
|
||||||
|
|
||||||
|
|
||||||
textToImageTab.addEventListener('click', function () {
|
|
||||||
textToImageTab.classList.add('text-primary', 'border-primary');
|
|
||||||
textToImageTab.classList.remove('text-gray-500', 'border-transparent');
|
|
||||||
imageToImageTab.classList.remove('text-primary', 'border-primary');
|
|
||||||
imageToImageTab.classList.add('text-gray-500', 'border-transparent');
|
|
||||||
textToImagePanel.classList.remove('hidden');
|
|
||||||
imageToImagePanel.classList.add('hidden');
|
|
||||||
});
|
|
||||||
|
|
||||||
imageToImageTab.addEventListener('click', function () {
|
|
||||||
imageToImageTab.classList.add('text-primary', 'border-primary');
|
|
||||||
imageToImageTab.classList.remove('text-gray-500', 'border-transparent');
|
|
||||||
textToImageTab.classList.remove('text-primary', 'border-primary');
|
|
||||||
textToImageTab.classList.add('text-gray-500', 'border-transparent');
|
|
||||||
imageToImagePanel.classList.remove('hidden');
|
|
||||||
textToImagePanel.classList.add('hidden');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
// 图生图相关功能
|
|
||||||
const browseImage = document.getElementById('browseImage');
|
|
||||||
const imageUpload = document.getElementById('imageUpload');
|
|
||||||
const imageUploadArea = document.getElementById('imageUploadArea');
|
|
||||||
const imagePreview = document.getElementById('imagePreview');
|
|
||||||
const previewImage = document.getElementById('previewImage');
|
|
||||||
const removeImage = document.getElementById('removeImage');
|
|
||||||
const imageStrength = document.getElementById('imageStrength');
|
|
||||||
const strengthValue = document.getElementById('strengthValue');
|
|
||||||
|
|
||||||
browseImage.addEventListener('click', function () {
|
|
||||||
imageUpload.click();
|
|
||||||
});
|
|
||||||
|
|
||||||
imageUploadArea.addEventListener('dragover', function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
imageUploadArea.classList.add('border-primary');
|
|
||||||
});
|
|
||||||
|
|
||||||
imageUploadArea.addEventListener('dragleave', function () {
|
|
||||||
imageUploadArea.classList.remove('border-primary');
|
|
||||||
});
|
|
||||||
|
|
||||||
imageUploadArea.addEventListener('drop', function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
imageUploadArea.classList.remove('border-primary');
|
|
||||||
if (e.dataTransfer.files.length) {
|
|
||||||
imageUpload.files = e.dataTransfer.files;
|
|
||||||
handleImageUpload();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
imageUpload.addEventListener('change', handleImageUpload);
|
|
||||||
|
|
||||||
function handleImageUpload() {
|
|
||||||
if (imageUpload.files && imageUpload.files[0]) {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = function (e) {
|
|
||||||
previewImage.src = e.target.result;
|
|
||||||
imagePreview.classList.remove('hidden');
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(imageUpload.files[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
removeImage.addEventListener('click', function () {
|
|
||||||
imagePreview.classList.add('hidden');
|
|
||||||
imageUpload.value = '';
|
|
||||||
});
|
|
||||||
|
|
||||||
imageStrength.addEventListener('input', function () {
|
|
||||||
strengthValue.textContent = this.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 生成按钮点击事件
|
|
||||||
const generateBtn = document.getElementById('generateBtn');
|
|
||||||
generateBtn.addEventListener('click', generateImage);
|
|
||||||
|
|
||||||
// 重新生成按钮点击事件
|
|
||||||
const regenerateBtn = document.getElementById('regenerateBtn');
|
|
||||||
regenerateBtn.addEventListener('click', function () {
|
|
||||||
// 隐藏结果区域
|
|
||||||
document.getElementById('resultSection').classList.add('hidden');
|
|
||||||
// 显示状态区域
|
|
||||||
document.getElementById('statusSection').classList.remove('hidden');
|
|
||||||
// 重新生成
|
|
||||||
generateImage();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 放大按钮点击事件
|
|
||||||
const upscaleBtn = document.getElementById('upscaleBtn');
|
|
||||||
upscaleBtn.addEventListener('click', function () {
|
|
||||||
alert('图像放大功能即将实现');
|
|
||||||
});
|
|
||||||
|
|
||||||
// 变体按钮点击事件
|
|
||||||
const variationBtn = document.getElementById('variationBtn');
|
|
||||||
variationBtn.addEventListener('click', function () {
|
|
||||||
alert('生成变体功能即将实现');
|
|
||||||
});
|
|
||||||
|
|
||||||
// 下载按钮点击事件
|
|
||||||
const downloadBtn = document.getElementById('downloadBtn');
|
|
||||||
downloadBtn.addEventListener('click', function () {
|
|
||||||
const resultImage = document.getElementById('resultImage');
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = resultImage.src;
|
|
||||||
link.download = 'midjourney-image.png';
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
document.body.removeChild(link);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 清除历史记录按钮点击事件
|
|
||||||
const clearHistoryBtn = document.getElementById('clearHistoryBtn');
|
|
||||||
clearHistoryBtn.addEventListener('click', function () {
|
|
||||||
if (confirm('确定要清除所有历史记录吗?')) {
|
|
||||||
localStorage.removeItem('mjHistory');
|
|
||||||
updateHistoryGrid();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 生成图像函数
|
|
||||||
async function generateImage() {
|
|
||||||
let prompt, base64Array = null;
|
|
||||||
|
|
||||||
// 检查是文生图还是图生图
|
|
||||||
if (!textToImagePanel.classList.contains('hidden')) {
|
|
||||||
// 文生图
|
|
||||||
prompt = document.getElementById('prompt').value.trim();
|
|
||||||
if (!prompt) {
|
|
||||||
alert('请输入提示词');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 图生图
|
|
||||||
prompt = document.getElementById('imagePrompt').value.trim();
|
|
||||||
if (!prompt) {
|
|
||||||
alert('请输入提示词');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!imageUpload.files || !imageUpload.files[0]) {
|
|
||||||
alert('请上传参考图片');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 转换图片为base64
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = function (e) {
|
|
||||||
base64Array = [e.target.result.split(',')[1]];
|
|
||||||
submitGenerateRequest(prompt, base64Array);
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(imageUpload.files[0]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取表单元素并进行空值检查
|
|
||||||
const promptElement = document.getElementById('prompt');
|
|
||||||
const aspectRatioElement = document.getElementById('aspectRatio');
|
|
||||||
const qualityElement = document.getElementById('quality');
|
|
||||||
const styleElement = document.getElementById('style');
|
|
||||||
|
|
||||||
// 检查是否所有必要元素都存在
|
|
||||||
if (!promptElement || !aspectRatioElement || !qualityElement || !styleElement) {
|
|
||||||
showError('表单元素缺失,请检查页面配置');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建FormData并添加值
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('prompt', promptElement.value);
|
|
||||||
formData.append('aspect_ratio', aspectRatioElement.value);
|
|
||||||
formData.append('quality', qualityElement.value);
|
|
||||||
formData.append('style', styleElement.value);
|
|
||||||
|
|
||||||
// 调用提交函数
|
|
||||||
submitGenerateRequest(formData);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提交生成请求函数
|
|
||||||
async function submitGenerateRequest(formData) {
|
|
||||||
// 验证formData是有效的FormData对象
|
|
||||||
if (!(formData instanceof FormData)) {
|
|
||||||
showError('无效的表单数据格式');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提取表单数据
|
|
||||||
const prompt = formData.get('prompt');
|
|
||||||
const aspect_ratio = formData.get('aspect_ratio');
|
|
||||||
const quality = formData.get('quality');
|
|
||||||
const style = formData.get('style');
|
|
||||||
|
|
||||||
// 验证必填字段
|
|
||||||
if (!prompt) {
|
|
||||||
showError('请输入提示词');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从formData提取值(确保表单元素有正确的name属性)
|
|
||||||
const promptText = formData.get('prompt')?.trim();
|
|
||||||
const selectedAspectRatio = formData.get('aspect_ratio');
|
|
||||||
const selectedQuality = formData.get('quality');
|
|
||||||
const selectedStyle = formData.get('style');
|
|
||||||
|
|
||||||
// 验证必要参数
|
|
||||||
if (!promptText) {
|
|
||||||
showError('提示词不能为空');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!selectedAspectRatio || !selectedQuality || !selectedStyle) {
|
|
||||||
showError('请选择完整的生成参数');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/mj/imagine', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
prompt: promptText,
|
|
||||||
aspect_ratio: selectedAspectRatio,
|
|
||||||
quality: selectedQuality,
|
|
||||||
style: selectedStyle
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorData = await response.json().catch(() => null);
|
|
||||||
throw new Error(`生成请求失败: ${errorData?.message || response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
console.log('Backend response:', result); // Add this line
|
|
||||||
|
|
||||||
if (!result || !result.task_id) {
|
|
||||||
showError('生成失败: 未获取到任务ID');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示状态区域并开始轮询
|
|
||||||
document.getElementById('statusSection').classList.remove('hidden');
|
|
||||||
pollTaskStatus(result.task_id, promptText);
|
|
||||||
return result.task_id;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('提交生成请求失败:', error);
|
|
||||||
showError('提交请求失败: ' + error.message);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 轮询任务状态函数
|
|
||||||
// 轮询任务状态函数
|
|
||||||
function pollTaskStatus(taskId, prompt) {
|
|
||||||
// 轮询配置
|
|
||||||
const queryInterval = 3000; // 3秒查询一次
|
|
||||||
const maxQueries = 120; // 最多查询120次(6分钟)
|
|
||||||
let queryCount = 0; // 当前查询次数
|
|
||||||
const startTime = new Date().getTime(); // 开始时间
|
|
||||||
let timerId = null; // 用于存储setTimeout的ID
|
|
||||||
|
|
||||||
// 定义内部轮询函数
|
|
||||||
function checkStatus() {
|
|
||||||
// 检查是否超过最大查询次数
|
|
||||||
if (queryCount >= maxQueries) {
|
|
||||||
const elapsedTime = Math.round((new Date().getTime() - startTime) / 1000);
|
|
||||||
document.getElementById('status-display').innerHTML = `<i class="fa fa-exclamation-circle"></i> 图像生成超时(已等待${elapsedTime}秒),您可以稍后在历史记录中查看,或重新生成。`;
|
|
||||||
document.getElementById('progress-text').textContent = '生成超时';
|
|
||||||
// 恢复生成按钮
|
|
||||||
generateBtn.disabled = false;
|
|
||||||
generateBtn.innerHTML = '<i class="fa fa-magic mr-2"></i>生成图像';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发起状态查询请求
|
|
||||||
fetch(`/api/mj/task_status?task_id=${taskId}`)
|
|
||||||
.then(response => {
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
|
||||||
return response.json();
|
|
||||||
})
|
|
||||||
.then(data => {
|
|
||||||
queryCount++;
|
|
||||||
// 获取DOM元素
|
|
||||||
const statusMessage = document.getElementById('status-display');
|
|
||||||
const progressFill = document.getElementById('progress-bar');
|
|
||||||
const progressText = document.getElementById('progress-text');
|
|
||||||
|
|
||||||
// 处理错误状态
|
|
||||||
if (data.error) {
|
|
||||||
statusMessage.innerHTML = `<i class="fa fa-exclamation-circle"></i> 生成失败: ${data.error}`;
|
|
||||||
statusMessage.style.color = '#ef4444';
|
|
||||||
// 恢复生成按钮
|
|
||||||
generateBtn.disabled = false;
|
|
||||||
generateBtn.innerHTML = '<i class="fa fa-magic mr-2"></i>生成图像';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新状态信息
|
|
||||||
statusMessage.innerHTML = `<i class="fa fa-spinner fa-spin"></i> AI 正在创作中...`;
|
|
||||||
statusMessage.style.color = ''; // 重置颜色
|
|
||||||
|
|
||||||
// 更新进度条
|
|
||||||
let progress = data.progress || 0;
|
|
||||||
if (progress > 100) progress = 100;
|
|
||||||
const elapsedTime = Math.round((new Date().getTime() - startTime) / 1000);
|
|
||||||
progressFill.style.width = `${progress}%`;
|
|
||||||
progressText.textContent = `${Math.round(progress)}%`;
|
|
||||||
statusMessage.innerHTML += ` (已等待${elapsedTime}秒)`;
|
|
||||||
|
|
||||||
// 任务完成
|
|
||||||
if (data.status === 'completed') {
|
|
||||||
statusMessage.innerHTML = '<i class="fa fa-check-circle"></i> 图像生成完成!';
|
|
||||||
statusMessage.style.color = '#10b981';
|
|
||||||
|
|
||||||
// 确保进度为100%
|
|
||||||
progressFill.style.width = '100%';
|
|
||||||
progressText.textContent = '100%';
|
|
||||||
|
|
||||||
// 显示结果
|
|
||||||
setTimeout(() => {
|
|
||||||
showResult(data.image_url, taskId, prompt);
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
// 清除定时器
|
|
||||||
if (timerId) {
|
|
||||||
clearTimeout(timerId);
|
|
||||||
timerId = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 任务失败
|
|
||||||
else if (data.status === 'failed') {
|
|
||||||
statusMessage.innerHTML = `<i class="fa fa-exclamation-circle"></i> 生成失败: ${data.error || '未知错误'}`;
|
|
||||||
statusMessage.style.color = '#ef4444';
|
|
||||||
// 恢复生成按钮
|
|
||||||
generateBtn.disabled = false;
|
|
||||||
generateBtn.innerHTML = '<i class="fa fa-magic mr-2"></i>生成图像';
|
|
||||||
}
|
|
||||||
// 继续轮询
|
|
||||||
else {
|
|
||||||
timerId = setTimeout(checkStatus, queryInterval);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('检查任务状态失败:', error);
|
|
||||||
// 在错误情况下仍继续轮询直到达到最大次数
|
|
||||||
if (queryCount < maxQueries) {
|
|
||||||
queryCount++;
|
|
||||||
timerId = setTimeout(checkStatus, queryInterval);
|
|
||||||
} else {
|
|
||||||
document.getElementById('status-display').innerHTML = `<i class="fa fa-exclamation-circle"></i> 连接服务器失败,请重试`;
|
|
||||||
document.getElementById('status-display').style.color = '#ef4444';
|
|
||||||
// 恢复生成按钮
|
|
||||||
generateBtn.disabled = false;
|
|
||||||
generateBtn.innerHTML = '<i class="fa fa-magic mr-2"></i>生成图像';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 立即开始第一次检查
|
|
||||||
checkStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示结果函数
|
|
||||||
function showResult(imageUrl, taskId, prompt) {
|
|
||||||
// 隐藏状态区域
|
|
||||||
document.getElementById('statusSection').classList.add('hidden');
|
|
||||||
|
|
||||||
// 显示结果区域
|
|
||||||
const resultSection = document.getElementById('resultSection');
|
|
||||||
resultSection.classList.remove('hidden');
|
|
||||||
|
|
||||||
// 设置结果图像
|
|
||||||
const resultImage = document.getElementById('resultImage');
|
|
||||||
resultImage.src = imageUrl;
|
|
||||||
|
|
||||||
// 恢复生成按钮
|
|
||||||
generateBtn.disabled = false;
|
|
||||||
generateBtn.innerHTML = '<i class="fa fa-magic mr-2"></i>生成图像';
|
|
||||||
|
|
||||||
// 保存到历史记录
|
|
||||||
saveToHistory(taskId, prompt, imageUrl);
|
|
||||||
|
|
||||||
// 更新历史记录网格
|
|
||||||
updateHistoryGrid();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存到历史记录函数
|
|
||||||
function saveToHistory(taskId, prompt, imageUrl) {
|
|
||||||
let history = JSON.parse(localStorage.getItem('mjHistory') || '[]');
|
|
||||||
history.unshift({
|
|
||||||
id: taskId,
|
|
||||||
prompt: prompt,
|
|
||||||
imageUrl: imageUrl,
|
|
||||||
timestamp: new Date().getTime()
|
|
||||||
});
|
|
||||||
// 只保留最近20条记录
|
|
||||||
if (history.length > 20) {
|
|
||||||
history = history.slice(0, 20);
|
|
||||||
}
|
|
||||||
localStorage.setItem('mjHistory', JSON.stringify(history));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新历史记录网格函数
|
|
||||||
function updateHistoryGrid() {
|
|
||||||
const historyGrid = document.getElementById('historyGrid');
|
|
||||||
const history = JSON.parse(localStorage.getItem('mjHistory') || '[]');
|
|
||||||
|
|
||||||
if (history.length === 0) {
|
|
||||||
historyGrid.innerHTML = `
|
|
||||||
<div class="col-span-full text-center py-12 text-gray-500 dark:text-gray-400">
|
|
||||||
<i class="fa fa-history text-4xl mb-4"></i>
|
|
||||||
<p>暂无历史记录</p>
|
|
||||||
<p class="text-sm mt-2">开始生成图像以保存到历史记录</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
historyGrid.innerHTML = '';
|
|
||||||
history.forEach(item => {
|
|
||||||
const date = new Date(item.timestamp);
|
|
||||||
const formattedDate = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
|
|
||||||
|
|
||||||
const historyItem = document.createElement('div');
|
|
||||||
historyItem.className = 'overflow-hidden rounded-xl shadow-md hover:shadow-xl transition-all-300 group bg-white dark:bg-gray-800';
|
|
||||||
historyItem.innerHTML = `
|
|
||||||
<div class="relative h-48 overflow-hidden">
|
|
||||||
<img src="${item.imageUrl}" alt="生成的图像" class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110">
|
|
||||||
<div class="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-end p-4">
|
|
||||||
<div class="text-white text-sm truncate w-full">${item.prompt}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="p-4 flex justify-between items-center">
|
|
||||||
<span class="text-xs text-gray-500 dark:text-gray-400">${formattedDate}</span>
|
|
||||||
<button class="view-history-item text-primary hover:text-primary/80 transition-colors" data-image-url="${item.imageUrl}">
|
|
||||||
<i class="fa fa-eye"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
historyGrid.appendChild(historyItem);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 添加历史记录项点击事件
|
|
||||||
document.querySelectorAll('.view-history-item').forEach(button => {
|
|
||||||
button.addEventListener('click', function () {
|
|
||||||
const imageUrl = this.getAttribute('data-image-url');
|
|
||||||
// 显示结果区域
|
|
||||||
const resultSection = document.getElementById('resultSection');
|
|
||||||
resultSection.classList.remove('hidden');
|
|
||||||
// 设置结果图像
|
|
||||||
const resultImage = document.getElementById('resultImage');
|
|
||||||
resultImage.src = imageUrl;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// 初始化历史记录网格
|
|
||||||
updateHistoryGrid();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加 showError function after checkTaskStatus
|
|
||||||
function showError(message) {
|
|
||||||
const errorElement = document.getElementById('error-message');
|
|
||||||
if (errorElement) {
|
|
||||||
errorElement.textContent = message;
|
|
||||||
errorElement.classList.remove('hidden');
|
|
||||||
// Auto-hide after 5 seconds
|
|
||||||
setTimeout(() => errorElement.classList.add('hidden'), 5000);
|
|
||||||
} else {
|
|
||||||
// Fallback if error element not found
|
|
||||||
alert(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 文生图示例提示词库
|
|
||||||
const textPromptExamples = [
|
|
||||||
"一只穿着太空服的柯基犬在火星表面行走,背景是红色星球和远处的地球,科幻风格,高清细节",
|
|
||||||
"中国传统水墨画风格的山间小屋,雾气缭绕,远处有瀑布,意境悠远,留白艺术",
|
|
||||||
"蒸汽朋克风格的城市景观,空中漂浮着飞艇,机械结构细节丰富,黄昏光影,8K分辨率",
|
|
||||||
"可爱的卡通熊猫宇航员在太空中吃竹子,周围有星星和行星,迪士尼风格,明亮色彩",
|
|
||||||
"赛博朋克风格的东京雨夜,霓虹灯闪烁,全息广告,湿润的街道反射, Blade Runner 氛围",
|
|
||||||
"中世纪奇幻城堡坐落在山顶,周围有龙和魔法光芒,日落时分,史诗电影场景",
|
|
||||||
"极简主义风格的室内设计,白色为主色调,原木家具,大窗户,自然光,北欧风格",
|
|
||||||
"未来科技感的跑车设计,流线型车身,透明车顶,悬浮效果,银色金属质感,未来城市背景",
|
|
||||||
"水彩风格的森林动物聚会,兔子、狐狸和松鼠围坐在蘑菇旁,童话风格,柔和色彩",
|
|
||||||
"复古80年代风格的街机游戏厅,霓虹灯光,老式游戏机,像素艺术,怀旧氛围"
|
|
||||||
];
|
|
||||||
|
|
||||||
// 图生图示例提示词库
|
|
||||||
const imagePromptExamples = [
|
|
||||||
"将图片转换为梵高风格的油画,星月夜笔触,鲜艳色彩",
|
|
||||||
"变成卡通风格,线条简洁,色彩明亮,适合儿童绘本",
|
|
||||||
"转换为赛博朋克风格,添加霓虹灯效果和未来科技元素",
|
|
||||||
"模拟水墨画效果,黑白灰三色,强调意境和笔触",
|
|
||||||
"变成3D渲染风格,添加立体效果和真实光影",
|
|
||||||
"转换为像素艺术,8-bit风格,适合复古游戏场景",
|
|
||||||
"模拟水彩画效果,柔和的色彩过渡和纹理",
|
|
||||||
"变成低多边形风格,几何图形构成,简约美学",
|
|
||||||
"转换为蒸汽朋克风格,添加齿轮、铜制元素和机械结构",
|
|
||||||
"模拟素描效果,铅笔线条,强调轮廓和阴影"
|
|
||||||
];
|
|
||||||
|
|
||||||
// 页面加载完成后绑定事件
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
|
||||||
// 文生图随机按钮
|
|
||||||
const randomPromptBtn = document.getElementById('randomPromptBtn');
|
|
||||||
const promptInput = document.getElementById('prompt');
|
|
||||||
|
|
||||||
// 图生图随机按钮
|
|
||||||
const randomImagePromptBtn = document.getElementById('randomImagePromptBtn');
|
|
||||||
const imagePromptInput = document.getElementById('imagePrompt');
|
|
||||||
|
|
||||||
// 文生图随机功能
|
|
||||||
if (randomPromptBtn && promptInput) {
|
|
||||||
randomPromptBtn.addEventListener('click', function () {
|
|
||||||
const randomIndex = Math.floor(Math.random() * textPromptExamples.length);
|
|
||||||
promptInput.value = textPromptExamples[randomIndex];
|
|
||||||
// 添加输入框焦点效果
|
|
||||||
promptInput.focus();
|
|
||||||
setTimeout(() => promptInput.blur(), 100);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 图生图随机功能
|
|
||||||
if (randomImagePromptBtn && imagePromptInput) {
|
|
||||||
randomImagePromptBtn.addEventListener('click', function () {
|
|
||||||
const randomIndex = Math.floor(Math.random() * imagePromptExamples.length);
|
|
||||||
imagePromptInput.value = imagePromptExamples[randomIndex];
|
|
||||||
// 添加输入框焦点效果
|
|
||||||
imagePromptInput.focus();
|
|
||||||
setTimeout(() => imagePromptInput.blur(), 100);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
506
dsLightRag/static/Midjourney/mj.js
Normal file
506
dsLightRag/static/Midjourney/mj.js
Normal file
@@ -0,0 +1,506 @@
|
|||||||
|
// DOM加载完成后执行
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
// 主题切换功能
|
||||||
|
const themeToggle = document.getElementById('themeToggle');
|
||||||
|
themeToggle.addEventListener('click', function () {
|
||||||
|
document.documentElement.classList.toggle('dark');
|
||||||
|
localStorage.setItem('theme', document.documentElement.classList.contains('dark') ? 'dark' : 'light');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 检查本地存储的主题偏好
|
||||||
|
if (localStorage.getItem('theme') === 'dark' || (!localStorage.getItem('theme') && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移动端菜单切换
|
||||||
|
const menuToggle = document.getElementById('menuToggle');
|
||||||
|
const mobileMenu = document.getElementById('mobileMenu');
|
||||||
|
menuToggle.addEventListener('click', function () {
|
||||||
|
mobileMenu.classList.toggle('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 选项卡切换
|
||||||
|
const textToImageTab = document.getElementById('textToImageTab');
|
||||||
|
const imageToImageTab = document.getElementById('imageToImageTab');
|
||||||
|
const textToImagePanel = document.getElementById('textToImagePanel');
|
||||||
|
const imageToImagePanel = document.getElementById('imageToImagePanel');
|
||||||
|
|
||||||
|
textToImageTab.addEventListener('click', function () {
|
||||||
|
textToImageTab.classList.add('text-primary', 'border-primary');
|
||||||
|
textToImageTab.classList.remove('text-gray-500', 'border-transparent');
|
||||||
|
imageToImageTab.classList.remove('text-primary', 'border-primary');
|
||||||
|
imageToImageTab.classList.add('text-gray-500', 'border-transparent');
|
||||||
|
textToImagePanel.classList.remove('hidden');
|
||||||
|
imageToImagePanel.classList.add('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
imageToImageTab.addEventListener('click', function () {
|
||||||
|
imageToImageTab.classList.add('text-primary', 'border-primary');
|
||||||
|
imageToImageTab.classList.remove('text-gray-500', 'border-transparent');
|
||||||
|
textToImageTab.classList.remove('text-primary', 'border-primary');
|
||||||
|
textToImageTab.classList.add('text-gray-500', 'border-transparent');
|
||||||
|
imageToImagePanel.classList.remove('hidden');
|
||||||
|
textToImagePanel.classList.add('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 图生图相关功能
|
||||||
|
const browseImage = document.getElementById('browseImage');
|
||||||
|
const imageUpload = document.getElementById('imageUpload');
|
||||||
|
const imageUploadArea = document.getElementById('imageUploadArea');
|
||||||
|
const imagePreview = document.getElementById('imagePreview');
|
||||||
|
const previewImage = document.getElementById('previewImage');
|
||||||
|
const removeImage = document.getElementById('removeImage');
|
||||||
|
const imageStrength = document.getElementById('imageStrength');
|
||||||
|
const strengthValue = document.getElementById('strengthValue');
|
||||||
|
|
||||||
|
browseImage.addEventListener('click', function () {
|
||||||
|
imageUpload.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
imageUploadArea.addEventListener('dragover', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
imageUploadArea.classList.add('border-primary');
|
||||||
|
});
|
||||||
|
|
||||||
|
imageUploadArea.addEventListener('dragleave', function () {
|
||||||
|
imageUploadArea.classList.remove('border-primary');
|
||||||
|
});
|
||||||
|
|
||||||
|
imageUploadArea.addEventListener('drop', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
imageUploadArea.classList.remove('border-primary');
|
||||||
|
if (e.dataTransfer.files.length) {
|
||||||
|
imageUpload.files = e.dataTransfer.files;
|
||||||
|
handleImageUpload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
imageUpload.addEventListener('change', handleImageUpload);
|
||||||
|
|
||||||
|
function handleImageUpload() {
|
||||||
|
if (imageUpload.files && imageUpload.files[0]) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = function (e) {
|
||||||
|
previewImage.src = e.target.result;
|
||||||
|
imagePreview.classList.remove('hidden');
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(imageUpload.files[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeImage.addEventListener('click', function () {
|
||||||
|
imagePreview.classList.add('hidden');
|
||||||
|
imageUpload.value = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
imageStrength.addEventListener('input', function () {
|
||||||
|
strengthValue.textContent = this.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 随机提示词功能 - 移到顶层DOMContentLoaded回调中
|
||||||
|
const randomPromptBtn = document.getElementById('randomPromptBtn');
|
||||||
|
const randomImagePromptBtn = document.getElementById('randomImagePromptBtn');
|
||||||
|
const promptInput = document.getElementById('prompt');
|
||||||
|
const imagePromptInput = document.getElementById('imagePrompt');
|
||||||
|
|
||||||
|
if (randomPromptBtn && promptInput) {
|
||||||
|
randomPromptBtn.addEventListener('click', function () {
|
||||||
|
const randomPrompt = textPromptExamples[Math.floor(Math.random() * textPromptExamples.length)];
|
||||||
|
promptInput.value = randomPrompt;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (randomImagePromptBtn && imagePromptInput) {
|
||||||
|
randomImagePromptBtn.addEventListener('click', function () {
|
||||||
|
const randomPrompt = imagePromptExamples[Math.floor(Math.random() * imagePromptExamples.length)];
|
||||||
|
imagePromptInput.value = randomPrompt;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成按钮点击事件
|
||||||
|
const generateBtn = document.getElementById('generateBtn');
|
||||||
|
generateBtn.addEventListener('click', generateImage);
|
||||||
|
|
||||||
|
// 重新生成按钮点击事件
|
||||||
|
const regenerateBtn = document.getElementById('regenerateBtn');
|
||||||
|
regenerateBtn.addEventListener('click', function () {
|
||||||
|
// 隐藏结果区域
|
||||||
|
document.getElementById('resultSection').classList.add('hidden');
|
||||||
|
// 显示状态区域
|
||||||
|
document.getElementById('statusSection').classList.remove('hidden');
|
||||||
|
// 重新生成
|
||||||
|
generateImage();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 放大按钮点击事件
|
||||||
|
const upscaleBtn = document.getElementById('upscaleBtn');
|
||||||
|
upscaleBtn.addEventListener('click', function () {
|
||||||
|
alert('图像放大功能即将实现');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 变体按钮点击事件
|
||||||
|
const variationBtn = document.getElementById('variationBtn');
|
||||||
|
variationBtn.addEventListener('click', function () {
|
||||||
|
alert('生成变体功能即将实现');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 下载按钮点击事件
|
||||||
|
const downloadBtn = document.getElementById('downloadBtn');
|
||||||
|
downloadBtn.addEventListener('click', function () {
|
||||||
|
const resultImage = document.getElementById('resultImage');
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = resultImage.src;
|
||||||
|
link.download = 'midjourney-image.png';
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// 生成图像函数
|
||||||
|
async function generateImage() {
|
||||||
|
let prompt, base64Array = null;
|
||||||
|
|
||||||
|
// 检查是文生图还是图生图
|
||||||
|
if (!textToImagePanel.classList.contains('hidden')) {
|
||||||
|
// 文生图
|
||||||
|
prompt = document.getElementById('prompt').value.trim();
|
||||||
|
if (!prompt) {
|
||||||
|
alert('请输入提示词');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 图生图
|
||||||
|
prompt = document.getElementById('imagePrompt').value.trim();
|
||||||
|
if (!prompt) {
|
||||||
|
alert('请输入提示词');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!imageUpload.files || !imageUpload.files[0]) {
|
||||||
|
alert('请上传参考图片');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换图片为base64
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = function (e) {
|
||||||
|
base64Array = [e.target.result.split(',')[1]];
|
||||||
|
submitGenerateRequest(prompt, base64Array);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(imageUpload.files[0]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取表单元素并进行空值检查
|
||||||
|
const promptElement = document.getElementById('prompt');
|
||||||
|
const aspectRatioElement = document.getElementById('aspectRatio');
|
||||||
|
const qualityElement = document.getElementById('quality');
|
||||||
|
const styleElement = document.getElementById('style');
|
||||||
|
|
||||||
|
// 检查是否所有必要元素都存在
|
||||||
|
if (!promptElement || !aspectRatioElement || !qualityElement || !styleElement) {
|
||||||
|
showError('表单元素缺失,请检查页面配置');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建FormData并添加值
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('prompt', promptElement.value);
|
||||||
|
formData.append('aspect_ratio', aspectRatioElement.value);
|
||||||
|
formData.append('quality', qualityElement.value);
|
||||||
|
formData.append('style', styleElement.value);
|
||||||
|
|
||||||
|
// 调用提交函数
|
||||||
|
submitGenerateRequest(formData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交生成请求函数
|
||||||
|
async function submitGenerateRequest(formData) {
|
||||||
|
// 验证formData是有效的FormData对象
|
||||||
|
if (!(formData instanceof FormData)) {
|
||||||
|
showError('无效的表单数据格式');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取表单数据
|
||||||
|
const prompt = formData.get('prompt');
|
||||||
|
|
||||||
|
// 验证必填字段
|
||||||
|
if (!prompt) {
|
||||||
|
showError('请输入提示词');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从formData提取值(确保表单元素有正确的name属性)
|
||||||
|
const promptText = formData.get('prompt')?.trim();
|
||||||
|
const selectedAspectRatio = formData.get('aspect_ratio');
|
||||||
|
const selectedQuality = formData.get('quality');
|
||||||
|
const selectedStyle = formData.get('style');
|
||||||
|
|
||||||
|
// 验证必要参数
|
||||||
|
if (!promptText) {
|
||||||
|
showError('提示词不能为空');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedAspectRatio || !selectedQuality || !selectedStyle) {
|
||||||
|
showError('请选择完整的生成参数');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/mj/imagine', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
prompt: promptText,
|
||||||
|
aspect_ratio: selectedAspectRatio,
|
||||||
|
quality: selectedQuality,
|
||||||
|
style: selectedStyle
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => null);
|
||||||
|
throw new Error(`生成请求失败: ${errorData?.message || response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
console.log('Backend response:', result); // Add this line
|
||||||
|
|
||||||
|
if (!result || !result.task_id) {
|
||||||
|
showError('生成失败: 未获取到任务ID');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示状态区域并开始轮询
|
||||||
|
document.getElementById('statusSection').classList.remove('hidden');
|
||||||
|
pollTaskStatus(result.task_id, promptText);
|
||||||
|
return result.task_id;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('提交生成请求失败:', error);
|
||||||
|
showError('提交请求失败: ' + error.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 轮询任务状态函数
|
||||||
|
function pollTaskStatus(taskId, prompt) {
|
||||||
|
// 轮询配置
|
||||||
|
const queryInterval = 3000; // 3秒查询一次
|
||||||
|
const maxQueries = 120; // 最多查询120次(6分钟)
|
||||||
|
let queryCount = 0; // 当前查询次数
|
||||||
|
const startTime = new Date().getTime(); // 开始时间
|
||||||
|
let timerId = null; // 用于存储setTimeout的ID
|
||||||
|
|
||||||
|
// 定义内部轮询函数
|
||||||
|
function checkStatus() {
|
||||||
|
// 检查是否超过最大查询次数
|
||||||
|
if (queryCount >= maxQueries) {
|
||||||
|
const elapsedTime = Math.round((new Date().getTime() - startTime) / 1000);
|
||||||
|
document.getElementById('status-display').innerHTML = `<i class="fa fa-exclamation-circle"></i> 图像生成超时(已等待${elapsedTime}秒),您可以稍后在历史记录中查看,或重新生成。`;
|
||||||
|
document.getElementById('progress-text').textContent = '生成超时';
|
||||||
|
// 恢复生成按钮
|
||||||
|
generateBtn.disabled = false;
|
||||||
|
generateBtn.innerHTML = '<i class="fa fa-magic mr-2"></i>生成图像';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发起状态查询请求
|
||||||
|
fetch(`/api/mj/task_status?task_id=${taskId}`)
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
queryCount++;
|
||||||
|
// 获取DOM元素
|
||||||
|
const statusMessage = document.getElementById('status-display');
|
||||||
|
const progressFill = document.getElementById('progress-bar');
|
||||||
|
const progressText = document.getElementById('progress-text');
|
||||||
|
|
||||||
|
// 处理错误状态
|
||||||
|
if (data.error) {
|
||||||
|
statusMessage.innerHTML = `<i class="fa fa-exclamation-circle"></i> 生成失败: ${data.error}`;
|
||||||
|
statusMessage.style.color = '#ef4444';
|
||||||
|
// 恢复生成按钮
|
||||||
|
generateBtn.disabled = false;
|
||||||
|
generateBtn.innerHTML = '<i class="fa fa-magic mr-2"></i>生成图像';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新状态信息
|
||||||
|
statusMessage.innerHTML = `<i class="fa fa-spinner fa-spin"></i> AI 正在创作中...`;
|
||||||
|
statusMessage.style.color = ''; // 重置颜色
|
||||||
|
|
||||||
|
// 更新进度条
|
||||||
|
let progress = data.progress || 0;
|
||||||
|
// 正确解析进度值,处理"32%"这样的字符串
|
||||||
|
if (typeof progress === 'string' && progress.endsWith('%')) {
|
||||||
|
progress = parseFloat(progress);
|
||||||
|
} else if (typeof progress === 'string') {
|
||||||
|
progress = parseFloat(progress) || 0;
|
||||||
|
}
|
||||||
|
// 添加NaN检查
|
||||||
|
if (isNaN(progress) || progress < 0) progress = 0;
|
||||||
|
if (progress > 100) progress = 100;
|
||||||
|
const elapsedTime = Math.round((new Date().getTime() - startTime) / 1000);
|
||||||
|
progressFill.style.width = `${progress}%`;
|
||||||
|
progressText.textContent = `${Math.round(progress)}%`;
|
||||||
|
statusMessage.innerHTML += ` (已等待${elapsedTime}秒)`;
|
||||||
|
|
||||||
|
// 任务完成
|
||||||
|
if (data.status === 'completed') {
|
||||||
|
statusMessage.innerHTML = '<i class="fa fa-check-circle"></i> 图像生成完成!';
|
||||||
|
statusMessage.style.color = '#10b981';
|
||||||
|
|
||||||
|
// 确保进度为100%
|
||||||
|
progressFill.style.width = '100%';
|
||||||
|
progressText.textContent = '100%';
|
||||||
|
|
||||||
|
// 显示结果
|
||||||
|
setTimeout(() => {
|
||||||
|
showResult(data.image_url, taskId, prompt);
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
// 清除定时器
|
||||||
|
if (timerId) {
|
||||||
|
clearTimeout(timerId);
|
||||||
|
timerId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 任务失败
|
||||||
|
else if (data.status === 'failed') {
|
||||||
|
statusMessage.innerHTML = `<i class="fa fa-exclamation-circle"></i> 生成失败: ${data.error || '未知错误'}`;
|
||||||
|
statusMessage.style.color = '#ef4444';
|
||||||
|
// 恢复生成按钮
|
||||||
|
generateBtn.disabled = false;
|
||||||
|
generateBtn.innerHTML = '<i class="fa fa-magic mr-2"></i>生成图像';
|
||||||
|
}
|
||||||
|
// 继续轮询
|
||||||
|
else {
|
||||||
|
timerId = setTimeout(checkStatus, queryInterval);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('检查任务状态失败:', error);
|
||||||
|
// 在错误情况下仍继续轮询直到达到最大次数
|
||||||
|
if (queryCount < maxQueries) {
|
||||||
|
queryCount++;
|
||||||
|
timerId = setTimeout(checkStatus, queryInterval);
|
||||||
|
} else {
|
||||||
|
document.getElementById('status-display').innerHTML = `<i class="fa fa-exclamation-circle"></i> 连接服务器失败,请重试`;
|
||||||
|
document.getElementById('status-display').style.color = '#ef4444';
|
||||||
|
// 恢复生成按钮
|
||||||
|
generateBtn.disabled = false;
|
||||||
|
generateBtn.innerHTML = '<i class="fa fa-magic mr-2"></i>生成图像';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 立即开始第一次检查
|
||||||
|
checkStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示结果函数
|
||||||
|
function showResult(imageUrl, taskId, prompt) {
|
||||||
|
// 隐藏状态区域
|
||||||
|
document.getElementById('statusSection').classList.add('hidden');
|
||||||
|
|
||||||
|
// 显示结果区域
|
||||||
|
const resultSection = document.getElementById('resultSection');
|
||||||
|
resultSection.classList.remove('hidden');
|
||||||
|
|
||||||
|
// 设置结果图像
|
||||||
|
const resultImage = document.getElementById('resultImage');
|
||||||
|
resultImage.src = imageUrl;
|
||||||
|
|
||||||
|
// 恢复生成按钮
|
||||||
|
generateBtn.disabled = false;
|
||||||
|
generateBtn.innerHTML = '<i class="fa fa-magic mr-2"></i>生成图像';
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加 showError function after checkTaskStatus
|
||||||
|
function showError(message) {
|
||||||
|
const errorElement = document.getElementById('error-message');
|
||||||
|
if (errorElement) {
|
||||||
|
errorElement.textContent = message;
|
||||||
|
errorElement.classList.remove('hidden');
|
||||||
|
// Auto-hide after 5 seconds
|
||||||
|
setTimeout(() => errorElement.classList.add('hidden'), 5000);
|
||||||
|
} else {
|
||||||
|
// Fallback if error element not found
|
||||||
|
alert(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文生图示例提示词库
|
||||||
|
const textPromptExamples = [
|
||||||
|
"一只穿着太空服的柯基犬在火星表面行走,背景是红色星球和远处的地球,科幻风格,高清细节",
|
||||||
|
"中国传统水墨画风格的山间小屋,雾气缭绕,远处有瀑布,意境悠远,留白艺术",
|
||||||
|
"蒸汽朋克风格的城市景观,空中漂浮着飞艇,机械结构细节丰富,黄昏光影,8K分辨率",
|
||||||
|
"可爱的卡通熊猫宇航员在太空中吃竹子,周围有星星和行星,迪士尼风格,明亮色彩",
|
||||||
|
"赛博朋克风格的东京雨夜,霓虹灯闪烁,全息广告,湿润的街道反射, Blade Runner 氛围",
|
||||||
|
"中世纪奇幻城堡坐落在山顶,周围有龙和魔法光芒,日落时分,史诗电影场景",
|
||||||
|
"极简主义风格的室内设计,白色为主色调,原木家具,大窗户,自然光,北欧风格",
|
||||||
|
"未来科技感的跑车设计,流线型车身,透明车顶,悬浮效果,银色金属质感,未来城市背景",
|
||||||
|
"水彩风格的森林动物聚会,兔子、狐狸和松鼠围坐在蘑菇旁,童话风格,柔和色彩",
|
||||||
|
"复古80年代风格的街机游戏厅,霓虹灯光,老式游戏机,像素艺术,怀旧氛围"
|
||||||
|
];
|
||||||
|
|
||||||
|
// 图生图示例提示词库
|
||||||
|
const imagePromptExamples = [
|
||||||
|
"将图片转换为梵高风格的油画,星月夜笔触,鲜艳色彩",
|
||||||
|
"变成卡通风格,线条简洁,色彩明亮,适合儿童绘本",
|
||||||
|
"转换为赛博朋克风格,添加霓虹灯效果和未来科技元素",
|
||||||
|
"模拟水墨画效果,黑白灰三色,强调意境和笔触",
|
||||||
|
"变成3D渲染风格,添加立体效果和真实光影",
|
||||||
|
"转换为像素艺术,8-bit风格,适合复古游戏场景",
|
||||||
|
"模拟水彩画效果,柔和的色彩过渡和纹理",
|
||||||
|
"变成低多边形风格,几何图形构成,简约美学",
|
||||||
|
"转换为蒸汽朋克风格,添加齿轮、铜制元素和机械结构",
|
||||||
|
"模拟素描效果,铅笔线条,强调轮廓和阴影"
|
||||||
|
];
|
||||||
|
|
||||||
|
// 页面加载完成后绑定事件
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
// 文生图随机按钮
|
||||||
|
const randomPromptBtn = document.getElementById('randomPromptBtn');
|
||||||
|
const promptInput = document.getElementById('prompt');
|
||||||
|
|
||||||
|
// 图生图随机按钮
|
||||||
|
const randomImagePromptBtn = document.getElementById('randomImagePromptBtn');
|
||||||
|
const imagePromptInput = document.getElementById('imagePrompt');
|
||||||
|
|
||||||
|
// 文生图随机功能
|
||||||
|
if (randomPromptBtn && promptInput) {
|
||||||
|
randomPromptBtn.addEventListener('click', function () {
|
||||||
|
const randomIndex = Math.floor(Math.random() * textPromptExamples.length);
|
||||||
|
promptInput.value = textPromptExamples[randomIndex];
|
||||||
|
// 添加输入框焦点效果
|
||||||
|
promptInput.focus();
|
||||||
|
setTimeout(() => promptInput.blur(), 100);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 图生图随机功能
|
||||||
|
if (randomImagePromptBtn && imagePromptInput) {
|
||||||
|
randomImagePromptBtn.addEventListener('click', function () {
|
||||||
|
const randomIndex = Math.floor(Math.random() * imagePromptExamples.length);
|
||||||
|
imagePromptInput.value = imagePromptExamples[randomIndex];
|
||||||
|
// 添加输入框焦点效果
|
||||||
|
imagePromptInput.focus();
|
||||||
|
setTimeout(() => imagePromptInput.blur(), 100);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化历史记录网格
|
||||||
|
updateHistoryGrid();
|
||||||
|
});
|
||||||
|
});
|
Reference in New Issue
Block a user