1609 lines
62 KiB
JavaScript
1609 lines
62 KiB
JavaScript
// OpenRouter Image Generator - 主要JavaScript逻辑
|
||
|
||
// 全局变量
|
||
let uploadedImages = [];
|
||
let chatHistory = [];
|
||
let generatedImages = [];
|
||
let currentSettings = {
|
||
apiKey: '',
|
||
baseUrl: 'https://openrouter.ai/api/v1',
|
||
model: 'google/gemini-2.5-flash-image-preview:free',
|
||
timeout: 600,
|
||
proxy: ''
|
||
};
|
||
|
||
// 配置常量
|
||
const CONFIG = {
|
||
MAX_FILE_SIZE: 10485760, // 10MB
|
||
MAX_FILES_PER_UPLOAD: 1, // 限制为只上传一张图片
|
||
SUPPORTED_IMAGE_FORMATS: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
|
||
MAX_CHAT_HISTORY: 50, // 减少聊天历史记录数量以节省存储空间
|
||
MAX_GENERATED_IMAGES: 20, // 减少存储的图像数量以防止存储空间溢出
|
||
STORAGE_KEYS: {
|
||
SETTINGS: 'openRouterSettings',
|
||
CHAT_HISTORY: 'openRouterChatHistory',
|
||
GENERATED_IMAGES: 'openRouterGeneratedImages'
|
||
},
|
||
INDEXED_DB: {
|
||
NAME: 'OpenRouterImageDB',
|
||
VERSION: 1,
|
||
STORES: {
|
||
SETTINGS: 'settings',
|
||
CHAT_HISTORY: 'chatHistory',
|
||
GENERATED_IMAGES: 'generatedImages'
|
||
}
|
||
}
|
||
};
|
||
|
||
// 错误消息
|
||
const ERROR_MESSAGES = {
|
||
NO_API_KEY: '请先输入API Key',
|
||
CONNECTION_FAILED: '连接失败,请检查网络和API设置',
|
||
INVALID_RESPONSE: 'API响应格式错误',
|
||
IMAGE_GENERATION_FAILED: '图像生成失败',
|
||
FILE_TOO_LARGE: '文件大小超过限制',
|
||
UNSUPPORTED_FORMAT: '不支持的文件格式',
|
||
UPLOAD_FAILED: '文件上传失败'
|
||
};
|
||
|
||
// 工具函数
|
||
const utils = {
|
||
// 格式化文件大小
|
||
formatFileSize: function(bytes) {
|
||
if (bytes === 0) return '0 Bytes';
|
||
const k = 1024;
|
||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||
},
|
||
|
||
// 显示通知
|
||
showNotification: function(message, type = 'info') {
|
||
const notification = document.createElement('div');
|
||
notification.className = `alert alert-${type} alert-dismissible fade show position-fixed`;
|
||
notification.style.cssText = 'top: 20px; right: 20px; z-index: 9999; max-width: 300px;';
|
||
notification.innerHTML = `
|
||
${message}
|
||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||
`;
|
||
document.body.appendChild(notification);
|
||
|
||
// 自动移除
|
||
setTimeout(() => {
|
||
if (notification.parentNode) {
|
||
notification.parentNode.removeChild(notification);
|
||
}
|
||
}, 5000);
|
||
},
|
||
|
||
// 验证文件
|
||
validateFile: function(file) {
|
||
if (file.size > CONFIG.MAX_FILE_SIZE) {
|
||
throw new Error(ERROR_MESSAGES.FILE_TOO_LARGE);
|
||
}
|
||
if (!CONFIG.SUPPORTED_IMAGE_FORMATS.includes(file.type)) {
|
||
throw new Error(ERROR_MESSAGES.UNSUPPORTED_FORMAT);
|
||
}
|
||
return true;
|
||
},
|
||
|
||
// 防抖函数
|
||
debounce: function(func, wait) {
|
||
let timeout;
|
||
return function executedFunction(...args) {
|
||
const later = () => {
|
||
clearTimeout(timeout);
|
||
func(...args);
|
||
};
|
||
clearTimeout(timeout);
|
||
timeout = setTimeout(later, wait);
|
||
};
|
||
},
|
||
|
||
// 检查和清理存储空间
|
||
checkAndCleanStorage: async function() {
|
||
if (!indexedDBStorage.isSupported) {
|
||
console.warn('IndexedDB 不支持,无法进行存储检查');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// 检查 IndexedDB 存储并清理过期数据
|
||
await indexedDBStorage.cleanupOldMessages();
|
||
await indexedDBStorage.cleanupOldImages();
|
||
|
||
console.log('IndexedDB 存储空间检查和清理完成');
|
||
} catch (error) {
|
||
console.error('存储空间检查失败:', error);
|
||
}
|
||
},
|
||
|
||
// 检查存储状态 - 调试用
|
||
checkStorageStatus: async function() {
|
||
console.log('=== 存储状态检查 ===');
|
||
console.log('IndexedDB 支持:', indexedDBStorage.isSupported);
|
||
console.log('IndexedDB 数据库:', indexedDBStorage.db);
|
||
|
||
if (indexedDBStorage.isSupported) {
|
||
try {
|
||
const settings = await indexedDBStorage.getSettings();
|
||
const chatHistory = await indexedDBStorage.getChatHistory();
|
||
const images = await indexedDBStorage.getGeneratedImages();
|
||
|
||
console.log('IndexedDB 数据统计:');
|
||
console.log(`- 设置: ${settings ? '已保存' : '未保存'}`);
|
||
console.log(`- 聊天记录: ${chatHistory.length} 条`);
|
||
console.log(`- 生成图像: ${images.length} 个`);
|
||
|
||
chatHistory.forEach((msg, idx) => {
|
||
console.log(` 聊天 ${idx}: ${msg.role} - ${msg.content.substring(0, 50)}...`);
|
||
});
|
||
|
||
images.forEach((img, idx) => {
|
||
console.log(` 图像 ${idx}: URL长度 ${img.url ? img.url.length : 0}`);
|
||
});
|
||
} catch (error) {
|
||
console.error('读取 IndexedDB 数据失败:', error);
|
||
}
|
||
}
|
||
},
|
||
|
||
// 检查事件绑定 - 调试用
|
||
checkEventBindings: function() {
|
||
console.log('=== 事件绑定检查 ===');
|
||
const containers = document.querySelectorAll('.image-clickable-container');
|
||
console.log(`找到 ${containers.length} 个可点击图像容器`);
|
||
|
||
containers.forEach((container, idx) => {
|
||
const img = container.querySelector('img');
|
||
const tooltip = container.querySelector('.image-preview-tooltip');
|
||
console.log(`容器 ${idx}:`, {
|
||
src: img ? img.src.substring(0, 50) + '...' : '无图片',
|
||
hasTooltip: !!tooltip,
|
||
tooltipText: tooltip ? tooltip.textContent : '无',
|
||
classList: container.classList.toString()
|
||
});
|
||
});
|
||
|
||
// 测试点击第一个容器
|
||
if (containers.length > 0) {
|
||
console.log('尝试点击第一个图像容器...');
|
||
containers[0].click();
|
||
}
|
||
}
|
||
};
|
||
|
||
// IndexedDB存储管理模块
|
||
const indexedDBStorage = {
|
||
db: null,
|
||
initPromise: null,
|
||
isSupported: false,
|
||
|
||
// 初始化数据库
|
||
init: function() {
|
||
if (this.initPromise) {
|
||
return this.initPromise;
|
||
}
|
||
|
||
this.initPromise = new Promise((resolve, reject) => {
|
||
if (!window.indexedDB) {
|
||
console.warn('IndexedDB 不支持,将使用 localStorage 作为后备');
|
||
this.db = null;
|
||
this.isSupported = false;
|
||
resolve(false);
|
||
return;
|
||
}
|
||
|
||
const request = indexedDB.open(CONFIG.INDEXED_DB.NAME, CONFIG.INDEXED_DB.VERSION);
|
||
|
||
request.onerror = (event) => {
|
||
console.error('IndexedDB 初始化失败:', event.target.error);
|
||
this.db = null;
|
||
this.isSupported = false;
|
||
resolve(false);
|
||
};
|
||
|
||
request.onsuccess = (event) => {
|
||
this.db = event.target.result;
|
||
this.isSupported = true;
|
||
console.log('IndexedDB 初始化成功');
|
||
resolve(true);
|
||
};
|
||
|
||
request.onupgradeneeded = (event) => {
|
||
const db = event.target.result;
|
||
console.log('正在创建/升级数据库结构...');
|
||
|
||
// 创建设置存储
|
||
if (!db.objectStoreNames.contains(CONFIG.INDEXED_DB.STORES.SETTINGS)) {
|
||
const settingsStore = db.createObjectStore(CONFIG.INDEXED_DB.STORES.SETTINGS, { keyPath: 'id' });
|
||
settingsStore.createIndex('timestamp', 'timestamp', { unique: false });
|
||
}
|
||
|
||
// 创建聊天历史存储
|
||
if (!db.objectStoreNames.contains(CONFIG.INDEXED_DB.STORES.CHAT_HISTORY)) {
|
||
const chatStore = db.createObjectStore(CONFIG.INDEXED_DB.STORES.CHAT_HISTORY, { keyPath: 'id', autoIncrement: true });
|
||
chatStore.createIndex('timestamp', 'timestamp', { unique: false });
|
||
chatStore.createIndex('role', 'role', { unique: false });
|
||
}
|
||
|
||
// 创建图像记录存储
|
||
if (!db.objectStoreNames.contains(CONFIG.INDEXED_DB.STORES.GENERATED_IMAGES)) {
|
||
const imagesStore = db.createObjectStore(CONFIG.INDEXED_DB.STORES.GENERATED_IMAGES, { keyPath: 'id', autoIncrement: true });
|
||
imagesStore.createIndex('timestamp', 'timestamp', { unique: false });
|
||
imagesStore.createIndex('url', 'url', { unique: false });
|
||
}
|
||
};
|
||
});
|
||
|
||
return this.initPromise;
|
||
},
|
||
|
||
// 保存设置
|
||
saveSettings: async function(settings) {
|
||
if (!this.isSupported) {
|
||
return false;
|
||
}
|
||
|
||
try {
|
||
const transaction = this.db.transaction([CONFIG.INDEXED_DB.STORES.SETTINGS], 'readwrite');
|
||
const store = transaction.objectStore(CONFIG.INDEXED_DB.STORES.SETTINGS);
|
||
const data = { id: 'current', ...settings, timestamp: Date.now() };
|
||
|
||
await new Promise((resolve, reject) => {
|
||
const request = store.put(data);
|
||
request.onsuccess = () => resolve(true);
|
||
request.onerror = () => reject(request.error);
|
||
});
|
||
|
||
return true;
|
||
} catch (error) {
|
||
console.error('IndexedDB 保存设置失败:', error);
|
||
return false;
|
||
}
|
||
},
|
||
|
||
// 获取设置
|
||
getSettings: async function() {
|
||
if (!this.isSupported) {
|
||
return null;
|
||
}
|
||
|
||
try {
|
||
const transaction = this.db.transaction([CONFIG.INDEXED_DB.STORES.SETTINGS], 'readonly');
|
||
const store = transaction.objectStore(CONFIG.INDEXED_DB.STORES.SETTINGS);
|
||
|
||
return new Promise((resolve, reject) => {
|
||
const request = store.get('current');
|
||
request.onsuccess = () => {
|
||
const result = request.result;
|
||
if (result) {
|
||
// 移除 id 和 timestamp 字段
|
||
const { id, timestamp, ...settings } = result;
|
||
resolve(settings);
|
||
} else {
|
||
resolve(null);
|
||
}
|
||
};
|
||
request.onerror = () => reject(request.error);
|
||
});
|
||
} catch (error) {
|
||
console.error('IndexedDB 获取设置失败:', error);
|
||
return null;
|
||
}
|
||
},
|
||
|
||
// 添加聊天消息
|
||
addChatMessage: async function(role, content) {
|
||
if (!this.isSupported) {
|
||
return false;
|
||
}
|
||
|
||
try {
|
||
const transaction = this.db.transaction([CONFIG.INDEXED_DB.STORES.CHAT_HISTORY], 'readwrite');
|
||
const store = transaction.objectStore(CONFIG.INDEXED_DB.STORES.CHAT_HISTORY);
|
||
|
||
const message = {
|
||
role: role,
|
||
content: content,
|
||
timestamp: Date.now()
|
||
};
|
||
|
||
await new Promise((resolve, reject) => {
|
||
const request = store.add(message);
|
||
request.onsuccess = () => resolve(true);
|
||
request.onerror = () => reject(request.error);
|
||
});
|
||
|
||
// 清理旧消息
|
||
await this.cleanupOldMessages();
|
||
return true;
|
||
} catch (error) {
|
||
console.error('IndexedDB 添加聊天消息失败:', error);
|
||
return false;
|
||
}
|
||
},
|
||
|
||
// 获取聊天历史
|
||
getChatHistory: async function() {
|
||
if (!this.isSupported) {
|
||
return [];
|
||
}
|
||
|
||
try {
|
||
const transaction = this.db.transaction([CONFIG.INDEXED_DB.STORES.CHAT_HISTORY], 'readonly');
|
||
const store = transaction.objectStore(CONFIG.INDEXED_DB.STORES.CHAT_HISTORY);
|
||
const index = store.index('timestamp');
|
||
|
||
return new Promise((resolve, reject) => {
|
||
const request = index.getAll();
|
||
request.onsuccess = () => {
|
||
const messages = request.result || [];
|
||
// 按时间戳排序
|
||
messages.sort((a, b) => a.timestamp - b.timestamp);
|
||
resolve(messages);
|
||
};
|
||
request.onerror = () => reject(request.error);
|
||
});
|
||
} catch (error) {
|
||
console.error('IndexedDB 获取聊天历史失败:', error);
|
||
return [];
|
||
}
|
||
},
|
||
|
||
// 添加生成的图像
|
||
addGeneratedImage: async function(imageId, imageUrl) {
|
||
if (!this.isSupported) {
|
||
return false;
|
||
}
|
||
|
||
try {
|
||
const transaction = this.db.transaction([CONFIG.INDEXED_DB.STORES.GENERATED_IMAGES], 'readwrite');
|
||
const store = transaction.objectStore(CONFIG.INDEXED_DB.STORES.GENERATED_IMAGES);
|
||
|
||
const image = {
|
||
imageId: imageId,
|
||
url: imageUrl,
|
||
timestamp: Date.now()
|
||
};
|
||
|
||
await new Promise((resolve, reject) => {
|
||
const request = store.add(image);
|
||
request.onsuccess = () => resolve(true);
|
||
request.onerror = () => reject(request.error);
|
||
});
|
||
|
||
// 清理旧图像
|
||
await this.cleanupOldImages();
|
||
return true;
|
||
} catch (error) {
|
||
console.error('IndexedDB 添加图像记录失败:', error);
|
||
return false;
|
||
}
|
||
},
|
||
|
||
// 获取生成的图像
|
||
getGeneratedImages: async function() {
|
||
if (!this.isSupported) {
|
||
return [];
|
||
}
|
||
|
||
try {
|
||
const transaction = this.db.transaction([CONFIG.INDEXED_DB.STORES.GENERATED_IMAGES], 'readonly');
|
||
const store = transaction.objectStore(CONFIG.INDEXED_DB.STORES.GENERATED_IMAGES);
|
||
const index = store.index('timestamp');
|
||
|
||
return new Promise((resolve, reject) => {
|
||
const request = index.getAll();
|
||
request.onsuccess = () => {
|
||
const images = request.result || [];
|
||
// 按时间戳排序
|
||
images.sort((a, b) => a.timestamp - b.timestamp);
|
||
resolve(images.map(img => ({
|
||
id: img.imageId,
|
||
url: img.url
|
||
})));
|
||
};
|
||
request.onerror = () => reject(request.error);
|
||
});
|
||
} catch (error) {
|
||
console.error('IndexedDB 获取图像记录失败:', error);
|
||
return [];
|
||
}
|
||
},
|
||
|
||
// 清理旧聊天消息
|
||
cleanupOldMessages: async function() {
|
||
try {
|
||
const transaction = this.db.transaction([CONFIG.INDEXED_DB.STORES.CHAT_HISTORY], 'readwrite');
|
||
const store = transaction.objectStore(CONFIG.INDEXED_DB.STORES.CHAT_HISTORY);
|
||
const countRequest = store.count();
|
||
|
||
countRequest.onsuccess = () => {
|
||
const count = countRequest.result;
|
||
if (count > CONFIG.MAX_CHAT_HISTORY) {
|
||
const index = store.index('timestamp');
|
||
const request = index.openCursor();
|
||
let deleteCount = count - CONFIG.MAX_CHAT_HISTORY;
|
||
|
||
request.onsuccess = (event) => {
|
||
const cursor = event.target.result;
|
||
if (cursor && deleteCount > 0) {
|
||
cursor.delete();
|
||
deleteCount--;
|
||
cursor.continue();
|
||
}
|
||
};
|
||
}
|
||
};
|
||
} catch (error) {
|
||
console.error('清理旧聊天消息失败:', error);
|
||
}
|
||
},
|
||
|
||
// 清理旧图像记录
|
||
cleanupOldImages: async function() {
|
||
try {
|
||
const transaction = this.db.transaction([CONFIG.INDEXED_DB.STORES.GENERATED_IMAGES], 'readwrite');
|
||
const store = transaction.objectStore(CONFIG.INDEXED_DB.STORES.GENERATED_IMAGES);
|
||
const countRequest = store.count();
|
||
|
||
countRequest.onsuccess = () => {
|
||
const count = countRequest.result;
|
||
if (count > CONFIG.MAX_GENERATED_IMAGES) {
|
||
const index = store.index('timestamp');
|
||
const request = index.openCursor();
|
||
let deleteCount = count - CONFIG.MAX_GENERATED_IMAGES;
|
||
|
||
request.onsuccess = (event) => {
|
||
const cursor = event.target.result;
|
||
if (cursor && deleteCount > 0) {
|
||
cursor.delete();
|
||
deleteCount--;
|
||
cursor.continue();
|
||
}
|
||
};
|
||
}
|
||
};
|
||
} catch (error) {
|
||
console.error('清理旧图像记录失败:', error);
|
||
}
|
||
},
|
||
|
||
// 清除所有数据
|
||
clearAllData: async function() {
|
||
if (!this.isSupported) {
|
||
return false;
|
||
}
|
||
|
||
try {
|
||
const transaction = this.db.transaction([
|
||
CONFIG.INDEXED_DB.STORES.SETTINGS,
|
||
CONFIG.INDEXED_DB.STORES.CHAT_HISTORY,
|
||
CONFIG.INDEXED_DB.STORES.GENERATED_IMAGES
|
||
], 'readwrite');
|
||
|
||
const promises = [
|
||
new Promise((resolve) => {
|
||
const request = transaction.objectStore(CONFIG.INDEXED_DB.STORES.SETTINGS).clear();
|
||
request.onsuccess = () => resolve();
|
||
}),
|
||
new Promise((resolve) => {
|
||
const request = transaction.objectStore(CONFIG.INDEXED_DB.STORES.CHAT_HISTORY).clear();
|
||
request.onsuccess = () => resolve();
|
||
}),
|
||
new Promise((resolve) => {
|
||
const request = transaction.objectStore(CONFIG.INDEXED_DB.STORES.GENERATED_IMAGES).clear();
|
||
request.onsuccess = () => resolve();
|
||
})
|
||
];
|
||
|
||
await Promise.all(promises);
|
||
return true;
|
||
} catch (error) {
|
||
console.error('IndexedDB 清除所有数据失败:', error);
|
||
return false;
|
||
}
|
||
},
|
||
|
||
// 数据迁移:从localStorage到IndexedDB
|
||
migrateFromLocalStorage: async function() {
|
||
if (!this.isSupported) {
|
||
return false;
|
||
}
|
||
|
||
try {
|
||
console.log('开始从 localStorage 迁移数据到 IndexedDB...');
|
||
|
||
// 迁移设置
|
||
const savedSettings = localStorage.getItem(CONFIG.STORAGE_KEYS.SETTINGS);
|
||
if (savedSettings) {
|
||
const settings = JSON.parse(savedSettings);
|
||
await this.saveSettings(settings);
|
||
console.log('设置已迁移到 IndexedDB');
|
||
}
|
||
|
||
// 迁移聊天历史
|
||
const savedChatHistory = localStorage.getItem(CONFIG.STORAGE_KEYS.CHAT_HISTORY);
|
||
if (savedChatHistory) {
|
||
const messages = JSON.parse(savedChatHistory);
|
||
for (const msg of messages) {
|
||
await this.addChatMessage(msg.role, msg.content);
|
||
}
|
||
console.log(`已迁移 ${messages.length} 条聊天记录到 IndexedDB`);
|
||
}
|
||
|
||
// 迁移图像记录
|
||
const savedImages = localStorage.getItem(CONFIG.STORAGE_KEYS.GENERATED_IMAGES);
|
||
if (savedImages) {
|
||
const images = JSON.parse(savedImages);
|
||
for (const img of images) {
|
||
await this.addGeneratedImage(img.id, img.url);
|
||
}
|
||
console.log(`已迁移 ${images.length} 条图像记录到 IndexedDB`);
|
||
}
|
||
|
||
console.log('数据迁移完成');
|
||
return true;
|
||
} catch (error) {
|
||
console.error('数据迁移失败:', error);
|
||
return false;
|
||
}
|
||
}
|
||
};
|
||
|
||
// API服务
|
||
const apiService = {
|
||
// 测试连接
|
||
testConnection: async function(apiKey, baseUrl) {
|
||
try {
|
||
const response = await fetch(`${baseUrl}/models`, {
|
||
headers: {
|
||
'Authorization': `Bearer ${apiKey}`,
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
if (response.ok) {
|
||
return { success: true, data: await response.json() };
|
||
} else {
|
||
return { success: false, error: response.statusText };
|
||
}
|
||
} catch (error) {
|
||
return { success: false, error: error.message };
|
||
}
|
||
},
|
||
|
||
// 生成图像
|
||
generateImage: async function(message, images, settings) {
|
||
const messages = [
|
||
{
|
||
role: 'user',
|
||
content: [
|
||
{ type: 'text', text: message }
|
||
]
|
||
}
|
||
];
|
||
|
||
// 添加上传的图像
|
||
images.forEach(img => {
|
||
messages[0].content.push({
|
||
type: 'image_url',
|
||
image_url: { url: img.data }
|
||
});
|
||
});
|
||
|
||
const payload = {
|
||
model: settings.model,
|
||
messages: messages,
|
||
stream: false
|
||
};
|
||
|
||
const controller = new AbortController();
|
||
const timeoutId = setTimeout(() => controller.abort(), settings.timeout * 1000);
|
||
|
||
try {
|
||
const response = await fetch(`${settings.baseUrl}/chat/completions`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': `Bearer ${settings.apiKey}`,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(payload),
|
||
signal: controller.signal
|
||
});
|
||
|
||
clearTimeout(timeoutId);
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP error! status: ${response.status}`);
|
||
}
|
||
|
||
const data = await response.json();
|
||
|
||
// 解析响应
|
||
const choice = data.choices[0];
|
||
const messageContent = choice.message.content;
|
||
const images = [];
|
||
|
||
// 提取图像数据
|
||
if (choice.message.images) {
|
||
choice.message.images.forEach(img => {
|
||
images.push(img.image_url.url);
|
||
});
|
||
}
|
||
|
||
return {
|
||
success: true,
|
||
content: messageContent,
|
||
images: images,
|
||
usage: data.usage
|
||
};
|
||
} catch (error) {
|
||
clearTimeout(timeoutId);
|
||
throw error;
|
||
}
|
||
}
|
||
};
|
||
|
||
// UI控制器
|
||
const uiController = {
|
||
// 更新连接状态
|
||
updateConnectionStatus: function(connected) {
|
||
const statusIndicator = document.getElementById('connectionStatus');
|
||
const connectionText = document.getElementById('connectionText');
|
||
|
||
if (connected) {
|
||
statusIndicator.className = 'status-indicator status-connected';
|
||
connectionText.textContent = '已连接';
|
||
} else {
|
||
statusIndicator.className = 'status-indicator status-disconnected';
|
||
connectionText.textContent = '未连接';
|
||
}
|
||
},
|
||
|
||
// 显示加载状态
|
||
showLoading: function(show) {
|
||
const loadingSpinner = document.getElementById('loadingSpinner');
|
||
loadingSpinner.style.display = show ? 'block' : 'none';
|
||
},
|
||
|
||
// 隐藏传统加载状态,用于批量生成模式
|
||
hideTraditionalLoading: function() {
|
||
const loadingSpinner = document.getElementById('loadingSpinner');
|
||
loadingSpinner.style.display = 'none';
|
||
},
|
||
|
||
// 添加聊天消息
|
||
addChatMessage: async function(role, content) {
|
||
const chatHistoryDiv = document.getElementById('chatHistory');
|
||
const messageDiv = document.createElement('div');
|
||
messageDiv.className = `chat-message ${role} fade-in`;
|
||
|
||
const timestamp = new Date().toLocaleTimeString();
|
||
messageDiv.innerHTML = `
|
||
<div class="d-flex justify-content-between align-items-start">
|
||
<div>
|
||
<strong>${role === 'user' ? '用户' : '助手'}</strong>
|
||
<small class="text-muted ms-2">${timestamp}</small>
|
||
</div>
|
||
</div>
|
||
<div class="mt-2">${this.escapeHtml(content)}</div>
|
||
`;
|
||
|
||
chatHistoryDiv.appendChild(messageDiv);
|
||
chatHistoryDiv.scrollTop = chatHistoryDiv.scrollHeight;
|
||
|
||
// 添加到内存中的聊天历史
|
||
chatHistory.push({ role: role, content: content });
|
||
|
||
// 保存到存储 - 优先使用 IndexedDB
|
||
await this.saveChatHistory(role, content);
|
||
},
|
||
|
||
// 转义HTML
|
||
escapeHtml: function(text) {
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
},
|
||
|
||
// 添加生成的图像
|
||
addGeneratedImage: async function(imageUrl) {
|
||
const gallery = document.getElementById('imageGallery');
|
||
const imageDiv = document.createElement('div');
|
||
imageDiv.className = 'image-item fade-in';
|
||
|
||
const imageId = Date.now() + Math.random();
|
||
imageDiv.innerHTML = `
|
||
<img src="${imageUrl}" alt="Generated Image" loading="lazy" class="generated-image">
|
||
<div class="image-overlay">
|
||
<div class="btn-grid">
|
||
<div class="btn-grid-row">
|
||
<button class="btn btn-sm btn-outline-light view-large-btn" data-image-url="${imageUrl}" title="查看大图">
|
||
<i class="fas fa-search-plus"></i>
|
||
</button>
|
||
<button class="btn btn-sm btn-outline-light download-btn" data-image-url="${imageUrl}" title="下载图像">
|
||
<i class="fas fa-download"></i>
|
||
</button>
|
||
</div>
|
||
<div class="btn-grid-row">
|
||
<button class="btn btn-sm btn-outline-light copy-url-btn" data-image-url="${imageUrl}" title="复制URL">
|
||
<i class="fas fa-copy"></i>
|
||
</button>
|
||
<button class="btn btn-sm btn-outline-light remove-btn" data-image-id="${imageId}" title="删除">
|
||
<i class="fas fa-trash"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
gallery.appendChild(imageDiv);
|
||
|
||
// 添加事件监听器
|
||
const viewLargeBtn = imageDiv.querySelector('.view-large-btn');
|
||
const downloadBtn = imageDiv.querySelector('.download-btn');
|
||
const copyBtn = imageDiv.querySelector('.copy-url-btn');
|
||
const removeBtn = imageDiv.querySelector('.remove-btn');
|
||
|
||
if (viewLargeBtn) {
|
||
viewLargeBtn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
this.viewLargeImage(imageUrl);
|
||
});
|
||
}
|
||
|
||
if (downloadBtn) {
|
||
downloadBtn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
app.downloadImage(imageUrl);
|
||
});
|
||
}
|
||
|
||
if (copyBtn) {
|
||
copyBtn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
app.copyImageUrl(imageUrl);
|
||
});
|
||
}
|
||
|
||
if (removeBtn) {
|
||
removeBtn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
app.removeGeneratedImage(imageId);
|
||
});
|
||
}
|
||
|
||
// 添加到内存中的图像记录
|
||
generatedImages.push({ id: imageId, url: imageUrl });
|
||
|
||
// 保存到存储 - 优先使用 IndexedDB
|
||
await this.saveGeneratedImages(imageId, imageUrl);
|
||
},
|
||
|
||
// 添加占位图像
|
||
addImagePlaceholder: function(index, total) {
|
||
const gallery = document.getElementById('imageGallery');
|
||
const placeholderDiv = document.createElement('div');
|
||
placeholderDiv.className = 'image-placeholder fade-in';
|
||
placeholderDiv.id = `placeholder-${index}`;
|
||
|
||
placeholderDiv.innerHTML = `
|
||
<div class="image-placeholder-content">
|
||
<div class="image-placeholder-spinner"></div>
|
||
<i class="fas fa-image image-placeholder-icon"></i>
|
||
<div class="image-placeholder-text">
|
||
正在生成图像 ${index + 1}/${total}
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
gallery.appendChild(placeholderDiv);
|
||
return placeholderDiv;
|
||
},
|
||
|
||
// 替换占位图像为实际图像
|
||
replacePlaceholderWithImage: function(placeholderId, imageUrl) {
|
||
const placeholder = document.getElementById(placeholderId);
|
||
if (!placeholder) return;
|
||
|
||
const imageId = Date.now() + Math.random();
|
||
placeholder.className = 'image-item fade-in';
|
||
placeholder.id = '';
|
||
|
||
placeholder.innerHTML = `
|
||
<img src="${imageUrl}" alt="Generated Image" loading="lazy" class="generated-image">
|
||
<div class="image-overlay">
|
||
<div class="btn-grid">
|
||
<div class="btn-grid-row">
|
||
<button class="btn btn-sm btn-outline-light view-large-btn" data-image-url="${imageUrl}" title="查看大图">
|
||
<i class="fas fa-search-plus"></i>
|
||
</button>
|
||
<button class="btn btn-sm btn-outline-light download-btn" data-image-url="${imageUrl}" title="下载图像">
|
||
<i class="fas fa-download"></i>
|
||
</button>
|
||
</div>
|
||
<div class="btn-grid-row">
|
||
<button class="btn btn-sm btn-outline-light copy-url-btn" data-image-url="${imageUrl}" title="复制URL">
|
||
<i class="fas fa-copy"></i>
|
||
</button>
|
||
<button class="btn btn-sm btn-outline-light remove-btn" data-image-id="${imageId}" title="删除">
|
||
<i class="fas fa-trash"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// 添加事件监听器
|
||
const viewLargeBtn = placeholder.querySelector('.view-large-btn');
|
||
const downloadBtn = placeholder.querySelector('.download-btn');
|
||
const copyBtn = placeholder.querySelector('.copy-url-btn');
|
||
const removeBtn = placeholder.querySelector('.remove-btn');
|
||
|
||
if (viewLargeBtn) {
|
||
viewLargeBtn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
this.viewLargeImage(imageUrl);
|
||
});
|
||
}
|
||
|
||
if (downloadBtn) {
|
||
downloadBtn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
app.downloadImage(imageUrl);
|
||
});
|
||
}
|
||
|
||
if (copyBtn) {
|
||
copyBtn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
app.copyImageUrl(imageUrl);
|
||
});
|
||
}
|
||
|
||
if (removeBtn) {
|
||
removeBtn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
app.removeGeneratedImage(imageId);
|
||
});
|
||
}
|
||
|
||
// 添加到内存中的图像记录
|
||
generatedImages.push({ id: imageId, url: imageUrl });
|
||
|
||
// 保存到存储 - 优先使用 IndexedDB
|
||
this.saveGeneratedImages(imageId, imageUrl);
|
||
},
|
||
|
||
// 显示批量生成状态
|
||
showBatchStatus: function(message, show = true) {
|
||
let statusDiv = document.getElementById('batchStatus');
|
||
if (!statusDiv) {
|
||
statusDiv = document.createElement('div');
|
||
statusDiv.id = 'batchStatus';
|
||
statusDiv.className = 'batch-status';
|
||
document.body.appendChild(statusDiv);
|
||
}
|
||
|
||
statusDiv.textContent = message;
|
||
if (show) {
|
||
statusDiv.classList.add('show');
|
||
} else {
|
||
statusDiv.classList.remove('show');
|
||
}
|
||
},
|
||
|
||
|
||
// 新的查看大图功能
|
||
viewLargeImage: function(imageUrl) {
|
||
try {
|
||
const modalElement = document.getElementById('imageViewerModal');
|
||
if (!modalElement) {
|
||
utils.showNotification('无法打开图像查看:找不到模态框元素', 'danger');
|
||
return;
|
||
}
|
||
|
||
// 检查 Bootstrap 是否可用
|
||
if (typeof bootstrap === 'undefined') {
|
||
utils.showNotification('无法打开图像查看:Bootstrap 未加载', 'danger');
|
||
return;
|
||
}
|
||
|
||
const modalImage = document.getElementById('viewerModalImage');
|
||
const downloadButton = document.getElementById('viewerDownloadImage');
|
||
|
||
if (!modalImage) {
|
||
utils.showNotification('无法打开图像查看:找不到图像元素', 'danger');
|
||
return;
|
||
}
|
||
|
||
// 设置图像源
|
||
modalImage.src = imageUrl;
|
||
modalImage.onerror = function() {
|
||
utils.showNotification('图像加载失败', 'danger');
|
||
};
|
||
|
||
// 设置下载按钮功能
|
||
if (downloadButton) {
|
||
downloadButton.onclick = function() {
|
||
app.downloadImage(imageUrl);
|
||
};
|
||
}
|
||
|
||
// 创建并显示模态框
|
||
const modal = new bootstrap.Modal(modalElement);
|
||
modal.show();
|
||
|
||
} catch (error) {
|
||
utils.showNotification(`无法打开图像查看: ${error.message}`, 'danger');
|
||
|
||
// 备用方案:在新窗口中打开图像
|
||
try {
|
||
window.open(imageUrl, '_blank');
|
||
} catch (e) {
|
||
// 静默处理错误
|
||
}
|
||
}
|
||
},
|
||
|
||
// 显示图像预览
|
||
displayImagePreview: function(imageData) {
|
||
const preview = document.getElementById('imagePreview');
|
||
preview.innerHTML = `
|
||
<div class="card h-100">
|
||
<img src="${imageData.data}" class="card-img-top image-preview h-75" alt="${imageData.name}" loading="lazy" style="object-fit: contain;">
|
||
<div class="card-body p-2">
|
||
<div class="d-flex justify-content-between align-items-center">
|
||
<small class="text-muted">${utils.formatFileSize(imageData.size)}</small>
|
||
<button class="btn btn-sm btn-outline-danger" onclick="app.removeImage(${imageData.id})" title="删除">
|
||
<i class="fas fa-trash"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
},
|
||
|
||
// 保存聊天历史 - 新增消息时调用
|
||
saveChatHistory: async function(role, content) {
|
||
if (!indexedDBStorage.isSupported) {
|
||
console.warn('IndexedDB 不支持,无法保存聊天历史');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const saved = await indexedDBStorage.addChatMessage(role, content);
|
||
if (!saved) {
|
||
console.error('IndexedDB 保存聊天历史失败');
|
||
}
|
||
} catch (error) {
|
||
console.error('保存聊天历史失败:', error);
|
||
}
|
||
},
|
||
|
||
// 保存生成的图像 - 新增图像时调用
|
||
saveGeneratedImages: async function(imageId, imageUrl) {
|
||
if (!indexedDBStorage.isSupported) {
|
||
console.warn('IndexedDB 不支持,无法保存图像记录');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const saved = await indexedDBStorage.addGeneratedImage(imageId, imageUrl);
|
||
if (!saved) {
|
||
console.error('IndexedDB 保存图像记录失败');
|
||
}
|
||
} catch (error) {
|
||
console.error('保存生成的图像失败:', error);
|
||
}
|
||
},
|
||
|
||
// 加载设置
|
||
loadSettings: async function() {
|
||
if (!indexedDBStorage.isSupported) {
|
||
console.warn('IndexedDB 不支持,无法加载设置');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// 从IndexedDB加载设置
|
||
const indexedSettings = await indexedDBStorage.getSettings();
|
||
if (indexedSettings) {
|
||
currentSettings = { ...currentSettings, ...indexedSettings };
|
||
}
|
||
|
||
// 更新UI
|
||
document.getElementById('apiKey').value = currentSettings.apiKey;
|
||
document.getElementById('baseUrl').value = currentSettings.baseUrl;
|
||
document.getElementById('model').value = currentSettings.model;
|
||
document.getElementById('timeout').value = currentSettings.timeout;
|
||
document.getElementById('proxy').value = currentSettings.proxy;
|
||
|
||
// 加载聊天历史
|
||
const indexedChatHistory = await indexedDBStorage.getChatHistory();
|
||
if (indexedChatHistory && indexedChatHistory.length > 0) {
|
||
chatHistory = indexedChatHistory;
|
||
}
|
||
|
||
// 重建聊天UI
|
||
const self = this;
|
||
chatHistory.forEach(msg => {
|
||
const chatHistoryDiv = document.getElementById('chatHistory');
|
||
const messageDiv = document.createElement('div');
|
||
messageDiv.className = `chat-message ${msg.role} fade-in`;
|
||
|
||
const timestamp = new Date(msg.timestamp || Date.now()).toLocaleTimeString();
|
||
messageDiv.innerHTML = `
|
||
<div class="d-flex justify-content-between align-items-start">
|
||
<div>
|
||
<strong>${msg.role === 'user' ? '用户' : '助手'}</strong>
|
||
<small class="text-muted ms-2">${timestamp}</small>
|
||
</div>
|
||
</div>
|
||
<div class="mt-2">${self.escapeHtml(msg.content)}</div>
|
||
`;
|
||
|
||
chatHistoryDiv.appendChild(messageDiv);
|
||
});
|
||
|
||
// 加载生成的图像
|
||
const indexedImages = await indexedDBStorage.getGeneratedImages();
|
||
if (indexedImages && indexedImages.length > 0) {
|
||
generatedImages = indexedImages;
|
||
}
|
||
|
||
// 重建图像UI
|
||
generatedImages.forEach(img => {
|
||
const gallery = document.getElementById('imageGallery');
|
||
const imageDiv = document.createElement('div');
|
||
imageDiv.className = 'image-item fade-in';
|
||
|
||
imageDiv.innerHTML = `
|
||
<img src="${img.url}" alt="Generated Image" loading="lazy" class="generated-image">
|
||
<div class="image-overlay">
|
||
<div class="btn-grid">
|
||
<div class="btn-grid-row">
|
||
<button class="btn btn-sm btn-outline-light view-large-btn" data-image-url="${img.url}" title="查看大图">
|
||
<i class="fas fa-search-plus"></i>
|
||
</button>
|
||
<button class="btn btn-sm btn-outline-light download-btn" data-image-url="${img.url}" title="下载图像">
|
||
<i class="fas fa-download"></i>
|
||
</button>
|
||
</div>
|
||
<div class="btn-grid-row">
|
||
<button class="btn btn-sm btn-outline-light copy-url-btn" data-image-url="${img.url}" title="复制URL">
|
||
<i class="fas fa-copy"></i>
|
||
</button>
|
||
<button class="btn btn-sm btn-outline-light remove-btn" data-image-id="${img.id}" title="删除">
|
||
<i class="fas fa-trash"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
gallery.appendChild(imageDiv);
|
||
|
||
// 添加事件监听器
|
||
const viewLargeBtn = imageDiv.querySelector('.view-large-btn');
|
||
const downloadBtn = imageDiv.querySelector('.download-btn');
|
||
const copyBtn = imageDiv.querySelector('.copy-url-btn');
|
||
const removeBtn = imageDiv.querySelector('.remove-btn');
|
||
|
||
if (viewLargeBtn) {
|
||
viewLargeBtn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
uiController.viewLargeImage(img.url);
|
||
});
|
||
}
|
||
|
||
if (downloadBtn) {
|
||
downloadBtn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
app.downloadImage(img.url);
|
||
});
|
||
}
|
||
|
||
if (copyBtn) {
|
||
copyBtn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
app.copyImageUrl(img.url);
|
||
});
|
||
}
|
||
|
||
if (removeBtn) {
|
||
removeBtn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
app.removeGeneratedImage(img.id);
|
||
});
|
||
}
|
||
});
|
||
} catch (error) {
|
||
console.error('加载设置失败:', error);
|
||
}
|
||
},
|
||
|
||
// 移除生成的图像
|
||
removeGeneratedImage: function(imageId) {
|
||
// 从内存中移除
|
||
generatedImages = generatedImages.filter(img => img.id !== imageId);
|
||
|
||
// 从DOM中移除
|
||
const gallery = document.getElementById('imageGallery');
|
||
const imageElements = gallery.querySelectorAll('.image-item');
|
||
imageElements.forEach(element => {
|
||
const removeBtn = element.querySelector('.remove-btn');
|
||
if (removeBtn && removeBtn.dataset.imageId == imageId) {
|
||
element.remove();
|
||
}
|
||
});
|
||
|
||
// 不需要再更新存储,因为 IndexedDB 会自动管理
|
||
},
|
||
|
||
// 保存设置
|
||
saveSettings: async function() {
|
||
if (!indexedDBStorage.isSupported) {
|
||
console.warn('IndexedDB 不支持,无法保存设置');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
currentSettings.apiKey = document.getElementById('apiKey').value;
|
||
currentSettings.baseUrl = document.getElementById('baseUrl').value;
|
||
currentSettings.model = document.getElementById('model').value;
|
||
currentSettings.timeout = parseInt(document.getElementById('timeout').value);
|
||
currentSettings.proxy = document.getElementById('proxy').value;
|
||
|
||
const saved = await indexedDBStorage.saveSettings(currentSettings);
|
||
if (!saved) {
|
||
console.error('IndexedDB 保存设置失败');
|
||
}
|
||
} catch (error) {
|
||
console.error('保存设置失败:', error);
|
||
}
|
||
}
|
||
};
|
||
|
||
// 文件处理
|
||
const fileHandler = {
|
||
// 处理拖拽
|
||
handleDragOver: function(e) {
|
||
e.preventDefault();
|
||
e.currentTarget.classList.add('dragover');
|
||
},
|
||
|
||
handleDragLeave: function(e) {
|
||
e.currentTarget.classList.remove('dragover');
|
||
},
|
||
|
||
handleDrop: function(e) {
|
||
e.preventDefault();
|
||
e.currentTarget.classList.remove('dragover');
|
||
const files = e.dataTransfer.files;
|
||
app.handleFiles(files);
|
||
},
|
||
|
||
// 处理文件选择
|
||
handleFileSelect: function(e) {
|
||
const files = e.target.files;
|
||
app.handleFiles(files);
|
||
},
|
||
|
||
// 处理文件
|
||
handleFiles: function(files) {
|
||
const preview = document.getElementById('imagePreview');
|
||
preview.innerHTML = '';
|
||
|
||
// 限制文件数量
|
||
const filesToProcess = Array.from(files).slice(0, CONFIG.MAX_FILES_PER_UPLOAD);
|
||
|
||
filesToProcess.forEach(file => {
|
||
try {
|
||
utils.validateFile(file);
|
||
|
||
if (file.type.startsWith('image/')) {
|
||
const reader = new FileReader();
|
||
reader.onload = function(e) {
|
||
const imageData = {
|
||
id: Date.now() + Math.random(),
|
||
data: e.target.result,
|
||
name: file.name,
|
||
type: file.type,
|
||
size: file.size
|
||
};
|
||
uploadedImages.push(imageData);
|
||
uiController.displayImagePreview(imageData);
|
||
};
|
||
reader.readAsDataURL(file);
|
||
}
|
||
} catch (error) {
|
||
utils.showNotification(`${file.name}: ${error.message}`, 'danger');
|
||
}
|
||
});
|
||
}
|
||
};
|
||
|
||
// 主应用对象
|
||
const app = {
|
||
// 初始化
|
||
init: async function() {
|
||
this.initializeEventListeners();
|
||
|
||
// 初始化 IndexedDB
|
||
try {
|
||
console.log('正在初始化 IndexedDB...');
|
||
const indexedDBSupported = await indexedDBStorage.init();
|
||
if (indexedDBSupported) {
|
||
console.log('IndexedDB 初始化成功,将优先使用 IndexedDB 存储');
|
||
// 尝试迁移数据
|
||
await indexedDBStorage.migrateFromLocalStorage();
|
||
} else {
|
||
console.warn('IndexedDB 不可用,将使用 localStorage 作为后备');
|
||
}
|
||
} catch (error) {
|
||
console.error('IndexedDB 初始化失败:', error);
|
||
}
|
||
|
||
// 启动时检查存储空间
|
||
await utils.checkAndCleanStorage();
|
||
await utils.checkStorageStatus();
|
||
await uiController.loadSettings();
|
||
console.log('OpenRouter Image Generator initialized');
|
||
},
|
||
|
||
// 初始化事件监听器
|
||
initializeEventListeners: function() {
|
||
// 文件拖拽
|
||
const dropZone = document.getElementById('dropZone');
|
||
dropZone.addEventListener('dragover', fileHandler.handleDragOver);
|
||
dropZone.addEventListener('dragleave', fileHandler.handleDragLeave);
|
||
dropZone.addEventListener('drop', fileHandler.handleDrop);
|
||
|
||
// 文件选择
|
||
document.getElementById('imageInput').addEventListener('change', fileHandler.handleFileSelect);
|
||
|
||
// 回车发送消息
|
||
const messageInput = document.getElementById('messageInput');
|
||
const self = this;
|
||
messageInput.addEventListener('keypress', function(e) {
|
||
if (e.key === 'Enter' && !e.shiftKey) {
|
||
e.preventDefault();
|
||
self.sendMessage();
|
||
}
|
||
});
|
||
|
||
// 设置变化时自动保存
|
||
const inputs = ['apiKey', 'baseUrl', 'model', 'timeout', 'proxy'];
|
||
inputs.forEach(id => {
|
||
document.getElementById(id).addEventListener('change', utils.debounce(() => {
|
||
uiController.saveSettings();
|
||
}, 1000));
|
||
});
|
||
|
||
// 页面卸载时保存设置
|
||
window.addEventListener('beforeunload', () => {
|
||
uiController.saveSettings();
|
||
});
|
||
},
|
||
|
||
// 测试连接
|
||
testConnection: async function() {
|
||
const apiKey = document.getElementById('apiKey').value;
|
||
const baseUrl = document.getElementById('baseUrl').value;
|
||
|
||
if (!apiKey) {
|
||
utils.showNotification(ERROR_MESSAGES.NO_API_KEY, 'warning');
|
||
return;
|
||
}
|
||
|
||
uiController.showLoading(true);
|
||
|
||
try {
|
||
const result = await apiService.testConnection(apiKey, baseUrl);
|
||
|
||
if (result.success) {
|
||
uiController.updateConnectionStatus(true);
|
||
utils.showNotification('连接成功!', 'success');
|
||
} else {
|
||
uiController.updateConnectionStatus(false);
|
||
utils.showNotification(`连接失败:${result.error}`, 'danger');
|
||
}
|
||
} catch (error) {
|
||
uiController.updateConnectionStatus(false);
|
||
utils.showNotification(`连接失败:${error.message}`, 'danger');
|
||
} finally {
|
||
uiController.showLoading(false);
|
||
}
|
||
},
|
||
|
||
// 发送消息 - 生成一张图像(使用批量生成方法)
|
||
sendMessage: async function() {
|
||
// 临时设置批量数量为1
|
||
const batchCountInput = document.getElementById('batchCount');
|
||
const originalBatchCount = batchCountInput.value;
|
||
batchCountInput.value = 1;
|
||
|
||
// 调用批量生成方法
|
||
await this.sendBatchMessage();
|
||
|
||
// 恢复原始批量数量
|
||
batchCountInput.value = originalBatchCount;
|
||
},
|
||
|
||
// 批量发送消息
|
||
sendBatchMessage: async function() {
|
||
const messageInput = document.getElementById('messageInput');
|
||
const batchCountInput = document.getElementById('batchCount');
|
||
const message = messageInput.value.trim();
|
||
const batchCount = parseInt(batchCountInput.value) || 6;
|
||
|
||
if (!message) {
|
||
utils.showNotification('请输入消息', 'warning');
|
||
return;
|
||
}
|
||
|
||
if (batchCount < 1 || batchCount > 10) {
|
||
utils.showNotification('请输入有效的生成数量(1-10)', 'warning');
|
||
return;
|
||
}
|
||
|
||
const apiKey = document.getElementById('apiKey').value;
|
||
if (!apiKey) {
|
||
utils.showNotification(ERROR_MESSAGES.NO_API_KEY, 'warning');
|
||
return;
|
||
}
|
||
|
||
// 隐藏传统加载状态,使用占位图像代替
|
||
uiController.hideTraditionalLoading();
|
||
|
||
// 添加用户消息到聊天历史
|
||
uiController.addChatMessage('user', message);
|
||
|
||
// 创建占位图像
|
||
const placeholders = [];
|
||
for (let i = 0; i < batchCount; i++) {
|
||
const placeholder = uiController.addImagePlaceholder(i, batchCount);
|
||
placeholders.push(placeholder);
|
||
}
|
||
|
||
// 显示批量生成状态
|
||
uiController.showBatchStatus(`正在批量生成 ${batchCount} 张图像...`);
|
||
|
||
let successCount = 0;
|
||
let failCount = 0;
|
||
let firstResponse = null;
|
||
let completedCount = 0;
|
||
|
||
// 同时发送所有请求,但每个请求完成后立即处理
|
||
const promises = [];
|
||
for (let i = 0; i < batchCount; i++) {
|
||
promises.push(
|
||
this.generateImageWithRetry(message, uploadedImages, currentSettings, i, batchCount)
|
||
.then(result => {
|
||
if (result.success) {
|
||
// 保存第一个响应用于添加聊天消息
|
||
if (!firstResponse) {
|
||
firstResponse = result.response;
|
||
uiController.addChatMessage('assistant', result.response.content);
|
||
}
|
||
|
||
// 显示生成的图像
|
||
if (result.response.images && result.response.images.length > 0) {
|
||
// 如果返回多个图像,为每个图像创建新的占位符
|
||
if (result.response.images.length > 1) {
|
||
// 移除原始占位符
|
||
const originalPlaceholder = document.getElementById(`placeholder-${i}`);
|
||
if (originalPlaceholder) {
|
||
originalPlaceholder.remove();
|
||
}
|
||
|
||
// 为每个图像添加新的图像项
|
||
result.response.images.forEach((img, imgIndex) => {
|
||
uiController.addGeneratedImage(img);
|
||
});
|
||
} else {
|
||
// 只有一个图像,直接替换占位符
|
||
uiController.replacePlaceholderWithImage(`placeholder-${i}`, result.response.images[0]);
|
||
}
|
||
successCount++;
|
||
} else {
|
||
// 没有图像数据,但请求成功,这种情况应该不会发生,因为generateImageWithRetry已经检查了
|
||
failCount++;
|
||
}
|
||
} else {
|
||
// 生成失败,检查是否需要保留占位符
|
||
if (!result.keepPlaceholder) {
|
||
// 移除占位符(旧的行为)
|
||
const placeholder = document.getElementById(`placeholder-${i}`);
|
||
if (placeholder) {
|
||
placeholder.remove();
|
||
}
|
||
}
|
||
failCount++;
|
||
}
|
||
|
||
// 更新完成计数和状态
|
||
completedCount++;
|
||
uiController.showBatchStatus(`已完成 ${completedCount}/${batchCount} 张图像...`);
|
||
|
||
return { index: i, success: result.success };
|
||
})
|
||
);
|
||
}
|
||
|
||
// 等待所有请求完成
|
||
await Promise.all(promises);
|
||
|
||
// 检查存储空间
|
||
await utils.checkAndCleanStorage();
|
||
|
||
// 清空输入框
|
||
messageInput.value = '';
|
||
|
||
// 隐藏批量生成状态
|
||
setTimeout(() => {
|
||
uiController.showBatchStatus('', false);
|
||
}, 3000);
|
||
|
||
// 显示最终结果
|
||
if (failCount === 0) {
|
||
utils.showNotification(`批量生成完成!成功生成 ${successCount} 张图像`, 'success');
|
||
} else {
|
||
utils.showNotification(`批量生成完成!成功 ${successCount} 张,失败 ${failCount} 张`, 'warning');
|
||
}
|
||
},
|
||
|
||
// 带重试机制的图像生成方法
|
||
generateImageWithRetry: async function(message, images, settings, imageIndex, totalImages) {
|
||
const maxRetries = 5;
|
||
let retryCount = 0;
|
||
|
||
while (retryCount <= maxRetries) {
|
||
try {
|
||
const response = await apiService.generateImage(message, images, settings);
|
||
|
||
// 检查响应中是否包含图像
|
||
if (!response.images || response.images.length === 0) {
|
||
throw new Error('生成响应中没有图像数据');
|
||
}
|
||
|
||
return { success: true, response: response };
|
||
} catch (error) {
|
||
retryCount++;
|
||
console.error(`生成第 ${imageIndex + 1} 张图像失败,重试次数 ${retryCount}/${maxRetries}:`, error);
|
||
|
||
if (retryCount <= maxRetries) {
|
||
// 更新占位符状态,显示重试信息
|
||
const placeholder = document.getElementById(`placeholder-${imageIndex}`);
|
||
if (placeholder) {
|
||
const placeholderText = placeholder.querySelector('.image-placeholder-text');
|
||
if (placeholderText) {
|
||
placeholderText.textContent = `正在重试生成图像 ${imageIndex + 1}/${totalImages} (${retryCount}/${maxRetries})`;
|
||
}
|
||
}
|
||
|
||
// 等待一段时间后重试,使用指数退避策略
|
||
const delayTime = 1000 * Math.pow(2, retryCount - 1);
|
||
await new Promise(resolve => setTimeout(resolve, delayTime));
|
||
} else {
|
||
// 重试次数用完,返回失败,但不要移除占位符
|
||
// 更新占位符显示最终失败状态
|
||
const placeholder = document.getElementById(`placeholder-${imageIndex}`);
|
||
if (placeholder) {
|
||
const placeholderText = placeholder.querySelector('.image-placeholder-text');
|
||
if (placeholderText) {
|
||
placeholderText.textContent = `生成图像 ${imageIndex + 1}/${totalImages} 失败,已重试 ${maxRetries} 次`;
|
||
}
|
||
const placeholderIcon = placeholder.querySelector('.image-placeholder-icon');
|
||
if (placeholderIcon) {
|
||
placeholderIcon.className = 'fas fa-exclamation-triangle image-placeholder-icon';
|
||
placeholderIcon.style.color = '#dc3545';
|
||
}
|
||
}
|
||
return { success: false, error: error, keepPlaceholder: true };
|
||
}
|
||
}
|
||
}
|
||
|
||
// 重试次数用完,返回失败,但不要移除占位符
|
||
return { success: false, error: new Error('重试次数用完'), keepPlaceholder: true };
|
||
},
|
||
|
||
// 移除上传的图像
|
||
removeImage: function(imageId) {
|
||
uploadedImages = uploadedImages.filter(img => img.id !== imageId);
|
||
const preview = document.getElementById('imagePreview');
|
||
if (uploadedImages.length === 0) {
|
||
preview.innerHTML = '<p class="text-muted">未选择图像</p>';
|
||
} else {
|
||
preview.innerHTML = '';
|
||
uploadedImages.forEach(img => uiController.displayImagePreview(img));
|
||
}
|
||
},
|
||
|
||
// 移除生成的图像
|
||
removeGeneratedImage: function(imageId) {
|
||
uiController.removeGeneratedImage(imageId);
|
||
},
|
||
|
||
// 下载图像
|
||
downloadImage: function(imageUrl) {
|
||
const link = document.createElement('a');
|
||
link.href = imageUrl;
|
||
link.download = `generated-image-${Date.now()}.png`;
|
||
link.click();
|
||
},
|
||
|
||
// 复制图像URL
|
||
copyImageUrl: function(imageUrl) {
|
||
navigator.clipboard.writeText(imageUrl).then(() => {
|
||
utils.showNotification('图像URL已复制到剪贴板', 'success');
|
||
}).catch(() => {
|
||
utils.showNotification('复制失败,请手动复制', 'warning');
|
||
});
|
||
},
|
||
|
||
// 处理文件
|
||
handleFiles: function(files) {
|
||
fileHandler.handleFiles(files);
|
||
},
|
||
|
||
|
||
// 清除存储数据
|
||
clearStorage: async function() {
|
||
if (confirm('确定要清除所有存储的数据吗?这将删除所有聊天记录和生成的图像记录。')) {
|
||
try {
|
||
if (indexedDBStorage.isSupported) {
|
||
// 清除 IndexedDB 数据
|
||
const cleared = await indexedDBStorage.clearAllData();
|
||
if (cleared) {
|
||
console.log('IndexedDB 数据已清除');
|
||
} else {
|
||
console.error('IndexedDB 清除失败');
|
||
}
|
||
}
|
||
|
||
// 清空内存中的数据
|
||
chatHistory = [];
|
||
generatedImages = [];
|
||
uploadedImages = [];
|
||
|
||
// 更新UI
|
||
document.getElementById('chatHistory').innerHTML = '';
|
||
document.getElementById('imageGallery').innerHTML = '';
|
||
document.getElementById('imagePreview').innerHTML = '';
|
||
|
||
utils.showNotification('已成功清除所有存储数据', 'success');
|
||
} catch (error) {
|
||
console.error('清除存储数据失败:', error);
|
||
utils.showNotification('清除存储数据失败', 'danger');
|
||
}
|
||
}
|
||
}
|
||
};
|
||
|
||
// 页面加载完成后初始化
|
||
document.addEventListener('DOMContentLoaded', async () => {
|
||
await app.init();
|
||
});
|
||
|
||
// 全局函数(供HTML调用)
|
||
window.testConnection = () => app.testConnection();
|
||
window.sendMessage = () => app.sendMessage();
|
||
window.sendBatchMessage = () => app.sendBatchMessage();
|
||
window.downloadImage = (url) => app.downloadImage(url);
|
||
window.copyImageUrl = (url) => app.copyImageUrl(url);
|
||
window.removeImage = (id) => app.removeImage(id);
|
||
window.removeGeneratedImage = (id) => app.removeGeneratedImage(id);
|
||
window.clearStorage = () => app.clearStorage();
|
||
// 调试函数
|
||
window.checkStorageStatus = () => utils.checkStorageStatus();
|
||
window.checkEventBindings = () => utils.checkEventBindings();
|
||
window.clearIndexedDB = async () => {
|
||
if (indexedDBStorage.isSupported) {
|
||
const cleared = await indexedDBStorage.clearAllData();
|
||
console.log('IndexedDB 已清空:', cleared);
|
||
}
|
||
}; |