Add files via upload
Browse files- static/js/db.js +153 -0
- static/js/main.js +703 -0
- static/js/state.js +204 -0
static/js/db.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// static/js/db.js
|
| 2 |
+
|
| 3 |
+
const DB_NAME = 'AlphaChatFileStore';
|
| 4 |
+
const STORE_NAME = 'files';
|
| 5 |
+
// *** قدم 1: نسخه دیتابیس را افزایش میدهیم تا فرآیند ارتقا فعال شود ***
|
| 6 |
+
const DB_VERSION = 2;
|
| 7 |
+
|
| 8 |
+
let db;
|
| 9 |
+
|
| 10 |
+
// 1. مقداردهی اولیه و باز کردن دیتابیس
|
| 11 |
+
export function initDB() {
|
| 12 |
+
return new Promise((resolve, reject) => {
|
| 13 |
+
console.log(`در حال تلاش برای باز کردن دیتابیس ${DB_NAME} با نسخه ${DB_VERSION}...`);
|
| 14 |
+
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
| 15 |
+
|
| 16 |
+
request.onerror = (event) => {
|
| 17 |
+
console.error("خطای حیاتی در باز کردن IndexedDB:", event.target.error);
|
| 18 |
+
reject("خطای دیتابیس. لطفا از فعال بودن کوکیها و فضای ذخیرهسازی مرورگر اطمینان حاصل کنید.");
|
| 19 |
+
};
|
| 20 |
+
|
| 21 |
+
request.onsuccess = (event) => {
|
| 22 |
+
db = event.target.result;
|
| 23 |
+
console.log("IndexedDB با موفقیت باز و آماده استفاده است.");
|
| 24 |
+
resolve(db);
|
| 25 |
+
};
|
| 26 |
+
|
| 27 |
+
// این تابع فقط زمانی اجرا میشود که دیتابیس برای اولین بار ساخته شود یا نسخه آن (DB_VERSION) افزایش یابد.
|
| 28 |
+
request.onupgradeneeded = (event) => {
|
| 29 |
+
console.log("رویداد onupgradeneeded فعال شد. در حال ارتقا ساختار دیتابیس...");
|
| 30 |
+
const db = event.target.result;
|
| 31 |
+
|
| 32 |
+
// *** قدم 2: منطق ارتقا را هوشمند میکنیم ***
|
| 33 |
+
// ابتدا بررسی میکنیم که آیا ساختار قدیمی (و اشتباه) وجود دارد یا نه
|
| 34 |
+
if (db.objectStoreNames.contains(STORE_NAME)) {
|
| 35 |
+
// اگر وجود داشت، آن را حذف میکنیم تا راه برای ساختار جدید باز شود.
|
| 36 |
+
db.deleteObjectStore(STORE_NAME);
|
| 37 |
+
console.log("Object store قدیمی 'files' پیدا و برای ارتقا حذف شد.");
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
// حالا ساختار جدید و صحیح را ایجاد میکنیم.
|
| 41 |
+
// با حذف keyPath، به IndexedDB اجازه میدهیم کلید را به صورت خودکار و خارج از آبجکت مدیریت کند.
|
| 42 |
+
// این کار مشکل اصلی شما را به طور قطعی حل میکند.
|
| 43 |
+
const store = db.createObjectStore(STORE_NAME, { autoIncrement: true });
|
| 44 |
+
|
| 45 |
+
// یک ایندکس برای جستجوی فایلها بر اساس زمان ایجاد میکنیم.
|
| 46 |
+
store.createIndex('timestamp', 'timestamp', { unique: false });
|
| 47 |
+
console.log("Object store جدید و صحیح 'files' با موفقیت ساخته شد.");
|
| 48 |
+
};
|
| 49 |
+
});
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
// 2. ذخیره کردن یک فایل در دیتابیس
|
| 53 |
+
export function storeFile(file) {
|
| 54 |
+
return new Promise((resolve, reject) => {
|
| 55 |
+
if (!db) {
|
| 56 |
+
// این حالت نباید رخ دهد چون main.js منتظر اتمام initDB میماند.
|
| 57 |
+
reject("دیتابیس هنوز مقداردهی اولیه نشده است.");
|
| 58 |
+
return;
|
| 59 |
+
}
|
| 60 |
+
const transaction = db.transaction([STORE_NAME], 'readwrite');
|
| 61 |
+
const store = transaction.objectStore(STORE_NAME);
|
| 62 |
+
const fileRecord = {
|
| 63 |
+
file: file,
|
| 64 |
+
name: file.name,
|
| 65 |
+
type: file.type,
|
| 66 |
+
timestamp: Date.now()
|
| 67 |
+
};
|
| 68 |
+
|
| 69 |
+
const request = store.add(fileRecord);
|
| 70 |
+
|
| 71 |
+
request.onsuccess = (event) => {
|
| 72 |
+
// event.target.result حاوی کلید (ID) اختصاص داده شده به رکورد جدید است.
|
| 73 |
+
resolve(event.target.result);
|
| 74 |
+
};
|
| 75 |
+
|
| 76 |
+
request.onerror = (event) => {
|
| 77 |
+
console.error("خطا در ذخیره فایل در IndexedDB:", event.target.error);
|
| 78 |
+
reject("ذخیره فایل در دیتابیس محلی ناموفق بود.");
|
| 79 |
+
};
|
| 80 |
+
});
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
// 3. بازیابی یک فایل از دیتابیس با استفاده از ID
|
| 84 |
+
export function getFile(id) {
|
| 85 |
+
return new Promise((resolve, reject) => {
|
| 86 |
+
if (!db) {
|
| 87 |
+
reject("دیتابیس مقداردهی اولیه نشده است.");
|
| 88 |
+
return;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
// کلیدهای تولید شده توسط autoIncrement همیشه عددی هستند.
|
| 92 |
+
// این تبدیل، اطمینان میدهد که حتی اگر ID به صورت رشته ذخیره شده باشد، به درستی بازیابی شود.
|
| 93 |
+
const numericId = typeof id === 'string' ? parseInt(id, 10) : id;
|
| 94 |
+
if (isNaN(numericId)) {
|
| 95 |
+
reject("ID فایل برای بازیابی نامعتبر است.");
|
| 96 |
+
return;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
const transaction = db.transaction([STORE_NAME], 'readonly');
|
| 100 |
+
const store = transaction.objectStore(STORE_NAME);
|
| 101 |
+
const request = store.get(numericId);
|
| 102 |
+
|
| 103 |
+
request.onsuccess = (event) => {
|
| 104 |
+
if (event.target.result) {
|
| 105 |
+
// خود فایل که یک Blob است را باز میگردانیم.
|
| 106 |
+
resolve(event.target.result.file);
|
| 107 |
+
} else {
|
| 108 |
+
reject("فایلی با این ID در دیتابیس محلی یافت نشد.");
|
| 109 |
+
}
|
| 110 |
+
};
|
| 111 |
+
|
| 112 |
+
request.onerror = (event) => {
|
| 113 |
+
console.error("خطا در بازیابی فایل:", event.target.error);
|
| 114 |
+
reject("بازیابی فایل ناموفق بود.");
|
| 115 |
+
};
|
| 116 |
+
});
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
// 4. پاک کردن فایلهای قدیمیتر از یک هفته
|
| 120 |
+
export function cleanupOldFiles() {
|
| 121 |
+
return new Promise((resolve, reject) => {
|
| 122 |
+
if (!db) {
|
| 123 |
+
reject("دیتابیس مقداردهی اولیه نشده است.");
|
| 124 |
+
return;
|
| 125 |
+
}
|
| 126 |
+
const transaction = db.transaction([STORE_NAME], 'readwrite');
|
| 127 |
+
const store = transaction.objectStore(STORE_NAME);
|
| 128 |
+
const index = store.index('timestamp');
|
| 129 |
+
|
| 130 |
+
const oneWeekAgo = Date.now() - (7 * 24 * 60 * 60 * 1000);
|
| 131 |
+
const range = IDBKeyRange.upperBound(oneWeekAgo);
|
| 132 |
+
|
| 133 |
+
const request = index.openCursor(range);
|
| 134 |
+
let deletedCount = 0;
|
| 135 |
+
|
| 136 |
+
request.onsuccess = (event) => {
|
| 137 |
+
const cursor = event.target.result;
|
| 138 |
+
if (cursor) {
|
| 139 |
+
cursor.delete();
|
| 140 |
+
deletedCount++;
|
| 141 |
+
cursor.continue();
|
| 142 |
+
} else {
|
| 143 |
+
if(deletedCount > 0) console.log(`${deletedCount} فایل قدیمی و منقضی شده از IndexedDB پاک شد.`);
|
| 144 |
+
resolve();
|
| 145 |
+
}
|
| 146 |
+
};
|
| 147 |
+
|
| 148 |
+
request.onerror = (event) => {
|
| 149 |
+
console.error("خطا در پاکسازی فایلهای قدیمی:", event.target.error);
|
| 150 |
+
reject("پاکسازی فایلهای قدیمی ناموفق بود.");
|
| 151 |
+
};
|
| 152 |
+
});
|
| 153 |
+
}
|
static/js/main.js
ADDED
|
@@ -0,0 +1,703 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// static/js/main.js
|
| 2 |
+
|
| 3 |
+
import * as state from './state.js';
|
| 4 |
+
import * as api from './api.js';
|
| 5 |
+
import * as db from './db.js';
|
| 6 |
+
|
| 7 |
+
// New UI module imports
|
| 8 |
+
import { dom } from './ui/dom.js';
|
| 9 |
+
import * as chatUI from './ui/chat.js';
|
| 10 |
+
import * as modalUI from './ui/modals.js';
|
| 11 |
+
import * as toolUI from './ui/tools.js';
|
| 12 |
+
import * as ttsUI from './ui/tts.js';
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
let currentUserStatus = {
|
| 16 |
+
isPremium: false,
|
| 17 |
+
hasBeenChecked: false,
|
| 18 |
+
fingerprint: null
|
| 19 |
+
};
|
| 20 |
+
|
| 21 |
+
const MAX_CHAT_SESSIONS = 150;
|
| 22 |
+
|
| 23 |
+
function checkUserPremiumStatus() {
|
| 24 |
+
return currentUserStatus.isPremium;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
async function handleFileSelection(event) {
|
| 28 |
+
const file = event.target.files[0];
|
| 29 |
+
if (!file) return;
|
| 30 |
+
|
| 31 |
+
chatUI.showFileUploading(file.name);
|
| 32 |
+
dom.submitButton.disabled = true;
|
| 33 |
+
|
| 34 |
+
try {
|
| 35 |
+
const uploadedFileData = await api.processAndUploadFile(file);
|
| 36 |
+
state.setAttachedFile(uploadedFileData);
|
| 37 |
+
chatUI.showFileReady(file.name, file.type, uploadedFileData.blobUrl);
|
| 38 |
+
} catch (error) {
|
| 39 |
+
console.error("خطا در پردازش فایل:", error);
|
| 40 |
+
chatUI.showFileError(error.message);
|
| 41 |
+
} finally {
|
| 42 |
+
event.target.value = '';
|
| 43 |
+
toolUI.toggleFilePopupMenu(false);
|
| 44 |
+
dom.submitButton.disabled = false;
|
| 45 |
+
dom.messageInput.dispatchEvent(new Event('input'));
|
| 46 |
+
}
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
function handleNewChat() {
|
| 50 |
+
if (state.chatSessions.length >= MAX_CHAT_SESSIONS) {
|
| 51 |
+
state.chatSessions.pop();
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
const newSession = { id: Date.now().toString(), title: 'چت جدید', messages: [], showThoughts: false };
|
| 55 |
+
state.chatSessions.unshift(newSession);
|
| 56 |
+
state.setActiveChatId(newSession.id);
|
| 57 |
+
chatUI.renderActiveChat();
|
| 58 |
+
chatUI.renderHistoryList();
|
| 59 |
+
toolUI.updateToolsButton(null);
|
| 60 |
+
state.setActiveToolPrefix(null);
|
| 61 |
+
state.setActiveTool(null);
|
| 62 |
+
state.saveSessions();
|
| 63 |
+
ttsUI.clearAllCache();
|
| 64 |
+
ttsUI.stopAudio();
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
function getFullChatText(session) {
|
| 68 |
+
if (!session || !session.messages) return "";
|
| 69 |
+
return session.messages
|
| 70 |
+
.map(msg => {
|
| 71 |
+
const prefix = msg.role === 'user' ? 'کاربر' : 'مدل';
|
| 72 |
+
const textContent = msg.parts?.find(p => p.text)?.text || '[محتوای غیر متنی]';
|
| 73 |
+
return `${prefix}:\n${textContent}`;
|
| 74 |
+
})
|
| 75 |
+
.join('\n\n---\n\n');
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
function handlePremiumFeatureClick(element) {
|
| 79 |
+
if (checkUserPremiumStatus()) {
|
| 80 |
+
return true;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
const parentContainer = element.closest('.premium-locked-item');
|
| 84 |
+
if (parentContainer) {
|
| 85 |
+
parentContainer.classList.add('animate-premium-lock');
|
| 86 |
+
setTimeout(() => {
|
| 87 |
+
parentContainer.classList.remove('animate-premium-lock');
|
| 88 |
+
}, 800);
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
modalUI.togglePremiumFeatureModal(true);
|
| 92 |
+
|
| 93 |
+
return false;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
document.addEventListener('DOMContentLoaded', async () => {
|
| 97 |
+
await db.initDB();
|
| 98 |
+
await db.cleanupOldFiles();
|
| 99 |
+
|
| 100 |
+
chatUI.initTheme();
|
| 101 |
+
ttsUI.initTtsPlayer();
|
| 102 |
+
state.loadSessions();
|
| 103 |
+
|
| 104 |
+
currentUserStatus.fingerprint = await api.getBrowserFingerprint();
|
| 105 |
+
|
| 106 |
+
if (state.chatSessions.length > MAX_CHAT_SESSIONS) {
|
| 107 |
+
state.chatSessions.length = MAX_CHAT_SESSIONS;
|
| 108 |
+
state.saveSessions();
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
if (state.chatSessions.length === 0 || !state.getActiveChat()) {
|
| 112 |
+
handleNewChat();
|
| 113 |
+
} else {
|
| 114 |
+
state.setActiveChatId(state.activeChatId || state.chatSessions[0].id);
|
| 115 |
+
chatUI.renderActiveChat();
|
| 116 |
+
chatUI.renderHistoryList();
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
chatUI.setupMobileKeyboardFix();
|
| 120 |
+
|
| 121 |
+
dom.newChatButton.addEventListener('click', handleNewChat);
|
| 122 |
+
dom.menuButton.addEventListener('click', () => modalUI.toggleSidebar(true));
|
| 123 |
+
dom.sidebarOverlay.addEventListener('click', () => modalUI.toggleSidebar(false));
|
| 124 |
+
|
| 125 |
+
dom.deleteAllChatsButton.addEventListener('click', () => {
|
| 126 |
+
modalUI.showConfirmModal('آیا از حذف تمام چتها مطمئن هستید؟', () => {
|
| 127 |
+
state.setChatSessions([]);
|
| 128 |
+
state.setActiveChatId(null);
|
| 129 |
+
state.saveSessions();
|
| 130 |
+
handleNewChat();
|
| 131 |
+
modalUI.toggleSidebar(false);
|
| 132 |
+
});
|
| 133 |
+
});
|
| 134 |
+
|
| 135 |
+
dom.settingsButton.addEventListener('click', () => {
|
| 136 |
+
modalUI.updateSettingsUI(checkUserPremiumStatus());
|
| 137 |
+
modalUI.toggleSettingsModal(true);
|
| 138 |
+
});
|
| 139 |
+
dom.settingsModal.addEventListener('click', (e) => {
|
| 140 |
+
if (e.target === dom.settingsModal) modalUI.toggleSettingsModal(false);
|
| 141 |
+
});
|
| 142 |
+
dom.themeToggle.addEventListener('change', (e) => {
|
| 143 |
+
const newTheme = e.target.checked ? 'dark' : 'light';
|
| 144 |
+
localStorage.setItem('theme', newTheme);
|
| 145 |
+
chatUI.applyTheme(newTheme);
|
| 146 |
+
});
|
| 147 |
+
|
| 148 |
+
dom.toolsButton.addEventListener('click', (e) => {
|
| 149 |
+
if (dom.toolsButton.classList.contains('tool-selected')) return;
|
| 150 |
+
e.stopPropagation();
|
| 151 |
+
const activeChat = state.getActiveChat();
|
| 152 |
+
const toggleSwitch = dom.toolsMenu.querySelector('.toggle-switch');
|
| 153 |
+
if (activeChat && toggleSwitch) {
|
| 154 |
+
toggleSwitch.classList.toggle('active', activeChat.showThoughts);
|
| 155 |
+
}
|
| 156 |
+
toolUI.toggleToolsMenu(!dom.toolsMenu.classList.contains('active'));
|
| 157 |
+
});
|
| 158 |
+
dom.attachFileButton.addEventListener('click', (e) => {
|
| 159 |
+
e.stopPropagation();
|
| 160 |
+
toolUI.toggleFilePopupMenu(!dom.filePopupMenu.classList.contains('active'));
|
| 161 |
+
});
|
| 162 |
+
|
| 163 |
+
dom.toolsMenu.addEventListener('click', (e) => {
|
| 164 |
+
const toggleSwitch = e.target.closest('.toggle-switch');
|
| 165 |
+
if (toggleSwitch) {
|
| 166 |
+
e.stopPropagation();
|
| 167 |
+
if (!handlePremiumFeatureClick(toggleSwitch)) return;
|
| 168 |
+
toggleSwitch.classList.toggle('active');
|
| 169 |
+
const activeChat = state.getActiveChat();
|
| 170 |
+
if (activeChat) {
|
| 171 |
+
activeChat.showThoughts = toggleSwitch.classList.contains('active');
|
| 172 |
+
state.saveSessions();
|
| 173 |
+
}
|
| 174 |
+
return;
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
const toolItem = e.target.closest('.tool-item');
|
| 178 |
+
if (!toolItem) return;
|
| 179 |
+
if (toolItem.parentElement.classList.contains('premium-locked-item') && !handlePremiumFeatureClick(toolItem)) {
|
| 180 |
+
toolUI.toggleToolsMenu(false);
|
| 181 |
+
return;
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
const tool = toolItem.dataset.tool;
|
| 185 |
+
const toolName = toolItem.dataset.toolName;
|
| 186 |
+
|
| 187 |
+
toolUI.toggleToolsMenu(false);
|
| 188 |
+
toolUI.updateToolsButton(toolName);
|
| 189 |
+
state.setActiveTool(tool);
|
| 190 |
+
|
| 191 |
+
if (tool === 'deep-think') {
|
| 192 |
+
state.setActiveToolPrefix("شما یک محقق حرفهای هستید. با بررسی عمیق و جامع، به سوال زیر یک پاسخ کامل، ساختاریافته و دقیق بدهید: ");
|
| 193 |
+
dom.messageInput.placeholder = "موضوع برای تفکر عمیق...";
|
| 194 |
+
} else if (tool === 'reasoning') {
|
| 195 |
+
state.setActiveToolPrefix("شما یک استدلالگر منطقی هستید. با تحلیل گام به گام و ارائه دلایل روشن، به سوال زیر پاسخ دهید: ");
|
| 196 |
+
dom.messageInput.placeholder = "موضوع برای استدلال...";
|
| 197 |
+
} else {
|
| 198 |
+
state.setActiveToolPrefix(null);
|
| 199 |
+
state.setActiveTool(null);
|
| 200 |
+
}
|
| 201 |
+
dom.messageInput.focus();
|
| 202 |
+
});
|
| 203 |
+
|
| 204 |
+
dom.clearToolSelection.addEventListener('click', (e) => {
|
| 205 |
+
e.stopPropagation();
|
| 206 |
+
state.setActiveToolPrefix(null);
|
| 207 |
+
state.setActiveTool(null);
|
| 208 |
+
toolUI.updateToolsButton(null);
|
| 209 |
+
dom.messageInput.focus();
|
| 210 |
+
});
|
| 211 |
+
|
| 212 |
+
window.addEventListener('click', (e) => {
|
| 213 |
+
if (dom.toolsMenu.classList.contains('active') && !dom.toolsMenu.contains(e.target) && !dom.toolsButton.contains(e.target)) {
|
| 214 |
+
toolUI.toggleToolsMenu(false);
|
| 215 |
+
}
|
| 216 |
+
if (dom.filePopupMenu.classList.contains('active') && !dom.filePopupMenu.contains(e.target) && !dom.attachFileButton.contains(e.target)) {
|
| 217 |
+
toolUI.toggleFilePopupMenu(false);
|
| 218 |
+
}
|
| 219 |
+
});
|
| 220 |
+
|
| 221 |
+
dom.selectImageOption.addEventListener('click', (e) => {
|
| 222 |
+
toolUI.toggleFilePopupMenu(false);
|
| 223 |
+
if (handlePremiumFeatureClick(e.currentTarget)) dom.imageFileInput.click();
|
| 224 |
+
});
|
| 225 |
+
|
| 226 |
+
dom.selectFileOption.addEventListener('click', (e) => {
|
| 227 |
+
toolUI.toggleFilePopupMenu(false);
|
| 228 |
+
if (handlePremiumFeatureClick(e.currentTarget)) dom.generalFileInput.click();
|
| 229 |
+
});
|
| 230 |
+
|
| 231 |
+
dom.premiumModalCloseBtn.addEventListener('click', () => modalUI.togglePremiumFeatureModal(false));
|
| 232 |
+
dom.premiumFeatureModal.addEventListener('click', (e) => {
|
| 233 |
+
if(e.target === dom.premiumFeatureModal) modalUI.togglePremiumFeatureModal(false);
|
| 234 |
+
});
|
| 235 |
+
dom.premiumModalUpgradeBtn.addEventListener('click', () => {
|
| 236 |
+
parent.postMessage({ type: 'NAVIGATE_TO_PREMIUM', payload: { url: chatUI.PREMIUM_URL } }, '*');
|
| 237 |
+
modalUI.togglePremiumFeatureModal(false);
|
| 238 |
+
});
|
| 239 |
+
|
| 240 |
+
dom.plusModalCloseBtn.addEventListener('click', () => modalUI.togglePlusRequiredModal(false));
|
| 241 |
+
dom.plusRequiredModal.addEventListener('click', (e) => {
|
| 242 |
+
if(e.target === dom.plusRequiredModal) modalUI.togglePlusRequiredModal(false);
|
| 243 |
+
});
|
| 244 |
+
|
| 245 |
+
dom.imageFileInput.addEventListener('change', handleFileSelection);
|
| 246 |
+
dom.generalFileInput.addEventListener('change', handleFileSelection);
|
| 247 |
+
|
| 248 |
+
dom.removeImageButton.addEventListener('click', () => {
|
| 249 |
+
state.setAttachedFile(null);
|
| 250 |
+
chatUI.hideFilePreview();
|
| 251 |
+
dom.messageInput.dispatchEvent(new Event('input'));
|
| 252 |
+
});
|
| 253 |
+
|
| 254 |
+
dom.htmlPreviewCloseBtn.addEventListener('click', () => modalUI.toggleHtmlPreviewModal(false));
|
| 255 |
+
dom.htmlPreviewOverlay.addEventListener('click', () => modalUI.toggleHtmlPreviewModal(false));
|
| 256 |
+
|
| 257 |
+
dom.messageForm.addEventListener('submit', async (e) => {
|
| 258 |
+
e.preventDefault();
|
| 259 |
+
|
| 260 |
+
if (state.isGenerating) {
|
| 261 |
+
if (state.globalAbortController) state.globalAbortController.abort();
|
| 262 |
+
return;
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
// *** START: ADDED MESSAGE LIMIT CHECK ***
|
| 266 |
+
if (state.isMessageLimitReached(checkUserPremiumStatus())) {
|
| 267 |
+
chatUI.showLimitReachedUpgrade();
|
| 268 |
+
return;
|
| 269 |
+
}
|
| 270 |
+
// *** END: ADDED MESSAGE LIMIT CHECK ***
|
| 271 |
+
|
| 272 |
+
const activeChat = state.getActiveChat();
|
| 273 |
+
if (!activeChat) return;
|
| 274 |
+
|
| 275 |
+
const userMessageText = dom.messageInput.value.trim();
|
| 276 |
+
if (!userMessageText && !state.attachedFile) return;
|
| 277 |
+
|
| 278 |
+
chatUI.setGeneratingState(true);
|
| 279 |
+
let modelBubbleOuterDiv;
|
| 280 |
+
|
| 281 |
+
try {
|
| 282 |
+
const isFirstMessageOfChat = activeChat.messages.length === 0;
|
| 283 |
+
if (isFirstMessageOfChat && dom.chatWindow.querySelector('.welcome-screen')) {
|
| 284 |
+
dom.chatWindow.querySelector('.welcome-screen').remove();
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
const userParts = [];
|
| 288 |
+
let fileAttachedInThisTurn = null;
|
| 289 |
+
if (state.attachedFile) {
|
| 290 |
+
fileAttachedInThisTurn = { ...state.attachedFile };
|
| 291 |
+
userParts.push({
|
| 292 |
+
id: fileAttachedInThisTurn.id, blobUrl: fileAttachedInThisTurn.blobUrl,
|
| 293 |
+
mimeType: fileAttachedInThisTurn.mimeType, name: fileAttachedInThisTurn.name,
|
| 294 |
+
base64Data: fileAttachedInThisTurn.base64Data
|
| 295 |
+
});
|
| 296 |
+
chatUI.hideFilePreview();
|
| 297 |
+
state.setAttachedFile(null);
|
| 298 |
+
}
|
| 299 |
+
if (userMessageText) userParts.push({ text: userMessageText });
|
| 300 |
+
|
| 301 |
+
const newUserMessage = { role: 'user', parts: userParts, tool: state.getActiveTool() };
|
| 302 |
+
activeChat.messages.push(newUserMessage);
|
| 303 |
+
|
| 304 |
+
// *** START: ADDED MESSAGE COUNT INCREMENT ***
|
| 305 |
+
// Increment count immediately after adding the message to the state,
|
| 306 |
+
// before making the API call.
|
| 307 |
+
state.incrementMessageCount(checkUserPremiumStatus());
|
| 308 |
+
// *** END: ADDED MESSAGE COUNT INCREMENT ***
|
| 309 |
+
|
| 310 |
+
await chatUI.addMessageToUI(newUserMessage, activeChat.messages.length - 1, {isLastUser: true, animate: true});
|
| 311 |
+
|
| 312 |
+
const modelPlaceholderMessage = { role: 'assistant', isTemporary: true, parts: [] };
|
| 313 |
+
activeChat.messages.push(modelPlaceholderMessage);
|
| 314 |
+
modelBubbleOuterDiv = await chatUI.addMessageToUI(modelPlaceholderMessage, activeChat.messages.length - 1, {animate: true});
|
| 315 |
+
|
| 316 |
+
// *** START: MODIFIED - منطق پاکسازی تاریخچه اینجا اضافه شده است ***
|
| 317 |
+
const historyForApi = activeChat.messages.map((msg, index) => {
|
| 318 |
+
const isLastMessage = index === activeChat.messages.length - 1; // این پیام placeholder مدل است
|
| 319 |
+
const isUserMessageJustSent = index === activeChat.messages.length - 2; // این پیام کاربر است
|
| 320 |
+
|
| 321 |
+
// برای پیام کاربر که همین الان ارسال شده، تمام اطلاعات را نگه دار
|
| 322 |
+
if (isUserMessageJustSent) {
|
| 323 |
+
return JSON.parse(JSON.stringify(msg));
|
| 324 |
+
}
|
| 325 |
+
// برای پیام placeholder مدل نیز کاری انجام نده
|
| 326 |
+
if (isLastMessage) {
|
| 327 |
+
return msg;
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
// برای تمام پیامهای قدیمیتر در تاریخچه...
|
| 331 |
+
const cleanMsg = { role: msg.role, parts: [] };
|
| 332 |
+
if (msg.parts) {
|
| 333 |
+
msg.parts.forEach(part => {
|
| 334 |
+
// ... فقط و فقط بخش متنی را نگه دار.
|
| 335 |
+
if (part.text) {
|
| 336 |
+
cleanMsg.parts.push({ text: part.text });
|
| 337 |
+
}
|
| 338 |
+
});
|
| 339 |
+
}
|
| 340 |
+
return cleanMsg;
|
| 341 |
+
});
|
| 342 |
+
// *** END: MODIFIED ***
|
| 343 |
+
|
| 344 |
+
const toolPrefix = state.getActiveToolPrefix();
|
| 345 |
+
const lastUserMsgInHistory = historyForApi.findLast(m => m.role === 'user');
|
| 346 |
+
const textPartInHistory = lastUserMsgInHistory.parts.find(p => p.text);
|
| 347 |
+
if (textPartInHistory) {
|
| 348 |
+
textPartInHistory.text = (toolPrefix ? toolPrefix : '') + (textPartInHistory.text || '');
|
| 349 |
+
} else if (toolPrefix) {
|
| 350 |
+
lastUserMsgInHistory.parts.push({ text: toolPrefix });
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
if (isFirstMessageOfChat && userMessageText) {
|
| 354 |
+
activeChat.title = userMessageText.substring(0, 30);
|
| 355 |
+
chatUI.renderHistoryList();
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
dom.messageInput.value = '';
|
| 359 |
+
dom.messageInput.dispatchEvent(new Event('input'));
|
| 360 |
+
|
| 361 |
+
const activeTool = state.getActiveTool();
|
| 362 |
+
if (activeTool === 'deep-think' || activeTool === 'reasoning') {
|
| 363 |
+
if (activeTool === 'deep-think') toolUI.updateDeepThinkPanel({ topic: userMessageText || 'فایل ضمیمه شده' }, modelBubbleOuterDiv);
|
| 364 |
+
else if (activeTool === 'reasoning') toolUI.updateReasoningPanel({ topic: userMessageText || 'فایل ضمیمه شده' }, modelBubbleOuterDiv);
|
| 365 |
+
|
| 366 |
+
const progressBar = modelBubbleOuterDiv.querySelector('.bar');
|
| 367 |
+
|
| 368 |
+
if (progressBar) {
|
| 369 |
+
requestAnimationFrame(() => {
|
| 370 |
+
progressBar.style.transition = 'width 30s ease-out';
|
| 371 |
+
progressBar.style.width = '100%';
|
| 372 |
+
});
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
const response = await api.getChatStream(historyForApi, state.globalAbortController.signal);
|
| 376 |
+
await api.readStreamAndDisplay(response, modelBubbleOuterDiv);
|
| 377 |
+
|
| 378 |
+
} else {
|
| 379 |
+
const MAX_RETRIES = 2;
|
| 380 |
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
| 381 |
+
if (state.globalAbortController.signal.aborted) throw new DOMException('Aborted by user', 'AbortError');
|
| 382 |
+
|
| 383 |
+
const attemptController = new AbortController();
|
| 384 |
+
const onGlobalAbort = () => attemptController.abort();
|
| 385 |
+
state.globalAbortController.signal.addEventListener('abort', onGlobalAbort);
|
| 386 |
+
|
| 387 |
+
try {
|
| 388 |
+
const timeoutDuration = fileAttachedInThisTurn ? 8000 : 3000;
|
| 389 |
+
if (attempt > 0) console.warn(`تلاش ${attempt + 1}/${MAX_RETRIES + 1}: سرور پاسخ نداد. تلاش مجدد...`);
|
| 390 |
+
|
| 391 |
+
const timeoutPromise = new Promise((_, reject) => {
|
| 392 |
+
const id = setTimeout(() => {
|
| 393 |
+
clearTimeout(id);
|
| 394 |
+
reject(new Error('ClientTimeout'));
|
| 395 |
+
}, timeoutDuration);
|
| 396 |
+
});
|
| 397 |
+
|
| 398 |
+
const responsePromise = api.getChatStream(historyForApi, attemptController.signal);
|
| 399 |
+
|
| 400 |
+
const response = await Promise.race([responsePromise, timeoutPromise]);
|
| 401 |
+
|
| 402 |
+
await api.readStreamAndDisplay(response, modelBubbleOuterDiv);
|
| 403 |
+
|
| 404 |
+
break;
|
| 405 |
+
} catch (error) {
|
| 406 |
+
attemptController.abort();
|
| 407 |
+
if (error.name === 'AbortError') {
|
| 408 |
+
throw error;
|
| 409 |
+
}
|
| 410 |
+
if (error.message === 'ClientTimeout') {
|
| 411 |
+
if (attempt === MAX_RETRIES) {
|
| 412 |
+
throw new Error('سرور پس از چندین تلاش پاسخگو نبود.');
|
| 413 |
+
}
|
| 414 |
+
} else {
|
| 415 |
+
throw error;
|
| 416 |
+
}
|
| 417 |
+
} finally {
|
| 418 |
+
state.globalAbortController.signal.removeEventListener('abort', onGlobalAbort);
|
| 419 |
+
}
|
| 420 |
+
}
|
| 421 |
+
}
|
| 422 |
+
|
| 423 |
+
} catch (error) {
|
| 424 |
+
if (error.name !== 'AbortError') {
|
| 425 |
+
console.error("خطا در هنگام تولید پیام:", error);
|
| 426 |
+
if (modelBubbleOuterDiv) {
|
| 427 |
+
chatUI.displayError(modelBubbleOuterDiv, error.message || 'یک خطای ناشناخته رخ داد.');
|
| 428 |
+
}
|
| 429 |
+
} else {
|
| 430 |
+
if (modelBubbleOuterDiv && !modelBubbleOuterDiv.querySelector('.message-content')?.innerText.includes('متوقف شد')) {
|
| 431 |
+
const contentArea = modelBubbleOuterDiv.querySelector('.message-content') || modelBubbleOuterDiv;
|
| 432 |
+
contentArea.innerHTML += '<p class="text-xs text-slate-500 mt-2 text-center p-4">-- عملیات متوقف شد --</p>';
|
| 433 |
+
}
|
| 434 |
+
}
|
| 435 |
+
} finally {
|
| 436 |
+
chatUI.resetState();
|
| 437 |
+
state.saveSessions();
|
| 438 |
+
state.setActiveToolPrefix(null);
|
| 439 |
+
state.setActiveTool(null);
|
| 440 |
+
toolUI.updateToolsButton(null);
|
| 441 |
+
}
|
| 442 |
+
});
|
| 443 |
+
|
| 444 |
+
dom.chatWindow.addEventListener('click', async (e) => {
|
| 445 |
+
const button = e.target.closest('.action-button');
|
| 446 |
+
if (!button) return;
|
| 447 |
+
const action = button.dataset.action;
|
| 448 |
+
const messageEntry = button.closest('.message-entry');
|
| 449 |
+
if (!messageEntry) return;
|
| 450 |
+
const messageIndex = parseInt(messageEntry.dataset.index, 10);
|
| 451 |
+
const activeChat = state.getActiveChat();
|
| 452 |
+
if (!activeChat || isNaN(messageIndex)) return;
|
| 453 |
+
const message = activeChat.messages[messageIndex];
|
| 454 |
+
if (action === 'copy') {
|
| 455 |
+
const textToCopy = message.parts?.find(p => p.text)?.text || '';
|
| 456 |
+
if (textToCopy) navigator.clipboard.writeText(textToCopy).then(() => chatUI.showCopyFeedback(button));
|
| 457 |
+
} else if (action === 'like' || action === 'dislike') {
|
| 458 |
+
chatUI.handleLikeDislike(button, messageEntry);
|
| 459 |
+
} else if (action === 'speak') {
|
| 460 |
+
const audioState = ttsUI.getAudioState();
|
| 461 |
+
if (audioState.messageIndex === messageIndex && (audioState.status === 'running' || audioState.status === 'suspended')) {
|
| 462 |
+
ttsUI.stopAudio();
|
| 463 |
+
return;
|
| 464 |
+
}
|
| 465 |
+
if (ttsUI.hasCacheForMessage(messageIndex)) {
|
| 466 |
+
ttsUI.playFromCache(messageIndex, button);
|
| 467 |
+
return;
|
| 468 |
+
}
|
| 469 |
+
const fullText = message.parts?.find(p => p.text)?.text;
|
| 470 |
+
if (!fullText) return;
|
| 471 |
+
const codeBlockRegex = /```[\s\S]*?```/g;
|
| 472 |
+
const textToSpeak = fullText.replace(codeBlockRegex, '').trim();
|
| 473 |
+
if (!textToSpeak) {
|
| 474 |
+
alert("محتوای متنی برای خواندن وجود ندارد (فقط کد شناسایی شد).");
|
| 475 |
+
return;
|
| 476 |
+
}
|
| 477 |
+
ttsUI.stream(messageIndex, textToSpeak, button);
|
| 478 |
+
} else if (action === 'regenerate') {
|
| 479 |
+
if (state.isGenerating) return;
|
| 480 |
+
chatUI.setGeneratingState(true);
|
| 481 |
+
const lastModelMessageIndex = state.findLastIndex(activeChat.messages, msg => msg.role === 'assistant');
|
| 482 |
+
if (messageIndex === lastModelMessageIndex) {
|
| 483 |
+
ttsUI.clearCacheForMessage(messageIndex);
|
| 484 |
+
activeChat.messages.length = messageIndex;
|
| 485 |
+
messageEntry.remove();
|
| 486 |
+
const lastUserMessageIndex = state.findLastIndex(activeChat.messages, msg => msg.role === 'user');
|
| 487 |
+
if (lastUserMessageIndex !== -1) {
|
| 488 |
+
const lastUserMessageElement = dom.chatWindow.querySelector(`.message-entry[data-index="${lastUserMessageIndex}"]`);
|
| 489 |
+
if (lastUserMessageElement) chatUI.updateMessageActions(lastUserMessageElement, activeChat.messages[lastUserMessageIndex], true, false);
|
| 490 |
+
}
|
| 491 |
+
const modelPlaceholderMessage = { role: 'assistant', isTemporary: true, parts: [] };
|
| 492 |
+
activeChat.messages.push(modelPlaceholderMessage);
|
| 493 |
+
const newModelBubble = await chatUI.addMessageToUI(modelPlaceholderMessage, activeChat.messages.length - 1, { animate: true });
|
| 494 |
+
try {
|
| 495 |
+
const lastUserMessage = activeChat.messages.findLast(m => m.role === 'user');
|
| 496 |
+
const lastTool = lastUserMessage?.tool;
|
| 497 |
+
|
| 498 |
+
// *** START: MODIFIED - اعمال منطق پاکسازی به بازسازی ***
|
| 499 |
+
const historyForApi = activeChat.messages.map((msg, index) => {
|
| 500 |
+
if (index === activeChat.messages.length - 1) return msg; // placeholder
|
| 501 |
+
if (index === activeChat.messages.length - 2) return JSON.parse(JSON.stringify(msg)); // user message
|
| 502 |
+
const cleanMsg = { role: msg.role, parts: [] };
|
| 503 |
+
if (msg.parts) msg.parts.forEach(part => { if (part.text) cleanMsg.parts.push({ text: part.text }); });
|
| 504 |
+
return cleanMsg;
|
| 505 |
+
});
|
| 506 |
+
// *** END: MODIFIED ***
|
| 507 |
+
|
| 508 |
+
if (lastTool === 'deep-think' || lastTool === 'reasoning') {
|
| 509 |
+
if (lastTool === 'deep-think') toolUI.updateDeepThinkPanel({ topic: lastUserMessage.parts.find(p=>p.text)?.text || 'فایل ضمیمه شده' }, newModelBubble);
|
| 510 |
+
if (lastTool === 'reasoning') toolUI.updateReasoningPanel({ topic: lastUserMessage.parts.find(p=>p.text)?.text || 'فایل ضمیمه شده' }, newModelBubble);
|
| 511 |
+
const progressBar = newModelBubble.querySelector('.bar');
|
| 512 |
+
if (progressBar) {
|
| 513 |
+
requestAnimationFrame(() => {
|
| 514 |
+
progressBar.style.transition = 'width 30s ease-out';
|
| 515 |
+
progressBar.style.width = '100%';
|
| 516 |
+
});
|
| 517 |
+
}
|
| 518 |
+
const response = await api.getChatStream(historyForApi, state.globalAbortController.signal);
|
| 519 |
+
await api.readStreamAndDisplay(response, newModelBubble);
|
| 520 |
+
} else {
|
| 521 |
+
const response = await api.getChatStream(historyForApi, state.globalAbortController.signal);
|
| 522 |
+
await api.readStreamAndDisplay(response, newModelBubble);
|
| 523 |
+
}
|
| 524 |
+
} catch(error) {
|
| 525 |
+
if (error.name !== 'AbortError') console.error("Regeneration failed:", error);
|
| 526 |
+
} finally {
|
| 527 |
+
chatUI.resetState();
|
| 528 |
+
state.saveSessions();
|
| 529 |
+
}
|
| 530 |
+
} else {
|
| 531 |
+
chatUI.resetState();
|
| 532 |
+
}
|
| 533 |
+
} else if (action === 'edit') {
|
| 534 |
+
if (state.isGenerating) return;
|
| 535 |
+
const lastUserMessageIndex = state.findLastIndex(activeChat.messages, msg => msg.role === 'user');
|
| 536 |
+
if (messageIndex === lastUserMessageIndex) {
|
| 537 |
+
const textPart = message.parts.find(p => p.text);
|
| 538 |
+
const filePart = message.parts.find(p => p.id);
|
| 539 |
+
if (textPart || filePart) {
|
| 540 |
+
modalUI.showEditModal(textPart ? textPart.text : '', async (newText) => {
|
| 541 |
+
chatUI.setGeneratingState(true);
|
| 542 |
+
try {
|
| 543 |
+
const allMessagesInDOM = dom.chatWindow.querySelectorAll('.message-entry');
|
| 544 |
+
allMessagesInDOM.forEach(msgEl => {
|
| 545 |
+
const idx = parseInt(msgEl.dataset.index, 10);
|
| 546 |
+
if (idx >= messageIndex) {
|
| 547 |
+
ttsUI.clearCacheForMessage(idx);
|
| 548 |
+
msgEl.remove();
|
| 549 |
+
}
|
| 550 |
+
});
|
| 551 |
+
activeChat.messages.length = messageIndex;
|
| 552 |
+
const newParts = [];
|
| 553 |
+
if (filePart) {
|
| 554 |
+
const file = await db.getFile(filePart.id);
|
| 555 |
+
const blobUrl = URL.createObjectURL(file);
|
| 556 |
+
const base64 = await api.processAndUploadFile(file).then(d => d.base64Data);
|
| 557 |
+
newParts.push({ ...filePart, blobUrl, base64Data: base64 });
|
| 558 |
+
}
|
| 559 |
+
if (newText.trim()) newParts.push({ text: newText });
|
| 560 |
+
if (newParts.length > 0) {
|
| 561 |
+
const editedUserMessage = { role: 'user', parts: newParts };
|
| 562 |
+
activeChat.messages.push(editedUserMessage);
|
| 563 |
+
await chatUI.addMessageToUI(editedUserMessage, activeChat.messages.length - 1, { isLastUser: true, animate: true });
|
| 564 |
+
}
|
| 565 |
+
const modelPlaceholderMessage = { role: 'assistant', isTemporary: true, parts: [] };
|
| 566 |
+
activeChat.messages.push(modelPlaceholderMessage);
|
| 567 |
+
const newModelBubble = await chatUI.addMessageToUI(modelPlaceholderMessage, activeChat.messages.length - 1, { animate: true });
|
| 568 |
+
|
| 569 |
+
// *** START: MODIFIED - اعمال منطق پاکسازی به ویرایش ***
|
| 570 |
+
const historyForApi = activeChat.messages.map((msg, index) => {
|
| 571 |
+
if (index >= activeChat.messages.length - 2) return JSON.parse(JSON.stringify(msg));
|
| 572 |
+
const cleanMsg = { role: msg.role, parts: [] };
|
| 573 |
+
if (msg.parts) msg.parts.forEach(part => { if (part.text) cleanMsg.parts.push({ text: part.text }); });
|
| 574 |
+
return cleanMsg;
|
| 575 |
+
});
|
| 576 |
+
// *** END: MODIFIED ***
|
| 577 |
+
|
| 578 |
+
const response = await api.getChatStream(historyForApi, state.globalAbortController.signal);
|
| 579 |
+
await api.readStreamAndDisplay(response, newModelBubble);
|
| 580 |
+
} catch (error) {
|
| 581 |
+
if (error.name !== 'AbortError') console.error("Edit failed:", error);
|
| 582 |
+
} finally {
|
| 583 |
+
chatUI.resetState();
|
| 584 |
+
state.saveSessions();
|
| 585 |
+
}
|
| 586 |
+
});
|
| 587 |
+
}
|
| 588 |
+
}
|
| 589 |
+
}
|
| 590 |
+
else if (action === 'show-message-menu') {
|
| 591 |
+
modalUI.showMessageMenu(e, messageIndex, activeChat, chatUI.escapeHTML);
|
| 592 |
+
}
|
| 593 |
+
});
|
| 594 |
+
|
| 595 |
+
dom.historyItemMenu.addEventListener('click', (e) => {
|
| 596 |
+
const button = e.target.closest('.menu-item');
|
| 597 |
+
if (!button) return;
|
| 598 |
+
const action = button.dataset.action;
|
| 599 |
+
const format = button.dataset.format;
|
| 600 |
+
const sessionId = dom.historyItemMenu.dataset.sessionId;
|
| 601 |
+
const session = state.chatSessions.find(s => s.id === sessionId);
|
| 602 |
+
if (!session) return;
|
| 603 |
+
if (action === 'rename') {
|
| 604 |
+
modalUI.showRenameModal(session.title, (newTitle) => {
|
| 605 |
+
session.title = newTitle;
|
| 606 |
+
state.saveSessions();
|
| 607 |
+
chatUI.renderHistoryList();
|
| 608 |
+
});
|
| 609 |
+
} else if (action === 'delete') {
|
| 610 |
+
modalUI.showConfirmModal(`آیا از حذف گفتگوی "${session.title}" مطمئن هستید؟`, () => {
|
| 611 |
+
state.setChatSessions(state.chatSessions.filter(s => s.id !== sessionId));
|
| 612 |
+
state.saveSessions();
|
| 613 |
+
if (state.activeChatId === sessionId) {
|
| 614 |
+
if (state.chatSessions.length > 0) {
|
| 615 |
+
state.setActiveChatId(state.chatSessions[0].id);
|
| 616 |
+
chatUI.renderActiveChat();
|
| 617 |
+
} else {
|
| 618 |
+
handleNewChat();
|
| 619 |
+
}
|
| 620 |
+
}
|
| 621 |
+
chatUI.renderHistoryList();
|
| 622 |
+
});
|
| 623 |
+
} else if (action === 'convert-chat') {
|
| 624 |
+
const fullText = getFullChatText(session);
|
| 625 |
+
api.convertTextToFile(fullText, format, button);
|
| 626 |
+
}
|
| 627 |
+
dom.historyItemMenu.classList.remove('visible');
|
| 628 |
+
});
|
| 629 |
+
|
| 630 |
+
dom.messageItemMenu.addEventListener('click', (e) => {
|
| 631 |
+
const menu = dom.messageItemMenu;
|
| 632 |
+
const closeMenu = () => {
|
| 633 |
+
menu.classList.remove('visible');
|
| 634 |
+
setTimeout(() => { menu.classList.add('hidden'); }, 300);
|
| 635 |
+
};
|
| 636 |
+
if (e.target === dom.messageItemMenuOverlay) {
|
| 637 |
+
closeMenu();
|
| 638 |
+
return;
|
| 639 |
+
}
|
| 640 |
+
const button = e.target.closest('.menu-item');
|
| 641 |
+
if (!button) return;
|
| 642 |
+
const action = button.dataset.action;
|
| 643 |
+
const format = button.dataset.format;
|
| 644 |
+
const messageIndex = parseInt(menu.dataset.messageIndex, 10);
|
| 645 |
+
const activeChat = state.getActiveChat();
|
| 646 |
+
if (!activeChat || isNaN(messageIndex)) {
|
| 647 |
+
closeMenu();
|
| 648 |
+
return;
|
| 649 |
+
}
|
| 650 |
+
const message = activeChat.messages[messageIndex];
|
| 651 |
+
if (action === 'delete-message') {
|
| 652 |
+
modalUI.showConfirmModal('آیا از حذف این پیام مطمئن هستید؟', () => {
|
| 653 |
+
state.deleteMessage(activeChat.id, messageIndex);
|
| 654 |
+
ttsUI.clearCacheForMessage(messageIndex);
|
| 655 |
+
chatUI.renderActiveChat();
|
| 656 |
+
});
|
| 657 |
+
} else if (action === 'convert-message') {
|
| 658 |
+
const textContent = message.parts?.find(p => p.text)?.text || '';
|
| 659 |
+
if (textContent) api.convertTextToFile(textContent, format, button);
|
| 660 |
+
else alert('محتوای متنی برای تبدیل وجود ندارد.');
|
| 661 |
+
}
|
| 662 |
+
closeMenu();
|
| 663 |
+
});
|
| 664 |
+
|
| 665 |
+
dom.messageInput.addEventListener('input', () => {
|
| 666 |
+
chatUI.adjustTextareaHeight(dom.messageInput);
|
| 667 |
+
if (dom.messageInput.value.trim().length > 0 || state.attachedFile) {
|
| 668 |
+
dom.submitButton.classList.add('active');
|
| 669 |
+
} else {
|
| 670 |
+
dom.submitButton.classList.remove('active');
|
| 671 |
+
}
|
| 672 |
+
});
|
| 673 |
+
|
| 674 |
+
dom.editInput.addEventListener('input', () => {
|
| 675 |
+
chatUI.adjustTextareaHeight(dom.editInput);
|
| 676 |
+
});
|
| 677 |
+
|
| 678 |
+
window.addEventListener('message', (event) => {
|
| 679 |
+
if (event.data && event.data.type === 'USER_DATA_RESPONSE_SIMPLE_CHECK') {
|
| 680 |
+
const PREMIUM_PAGE_ID = '1149636';
|
| 681 |
+
let isUserPremium = false;
|
| 682 |
+
if (event.data.payload) {
|
| 683 |
+
try {
|
| 684 |
+
const userObject = JSON.parse(event.data.payload);
|
| 685 |
+
if (userObject && userObject.isLogin && userObject.accessible_pages) {
|
| 686 |
+
if (userObject.accessible_pages.includes(PREMIUM_PAGE_ID) || userObject.accessible_pages.includes(parseInt(PREMIUM_PAGE_ID))) {
|
| 687 |
+
isUserPremium = true;
|
| 688 |
+
}
|
| 689 |
+
}
|
| 690 |
+
} catch (e) { console.error("Error parsing user data from parent:", e); }
|
| 691 |
+
}
|
| 692 |
+
currentUserStatus.isPremium = isUserPremium;
|
| 693 |
+
currentUserStatus.hasBeenChecked = true;
|
| 694 |
+
if (!dom.settingsModal.classList.contains('hidden')) {
|
| 695 |
+
modalUI.updateSettingsUI(isUserPremium);
|
| 696 |
+
}
|
| 697 |
+
}
|
| 698 |
+
});
|
| 699 |
+
|
| 700 |
+
parent.postMessage({ type: 'REQUEST_USER_DATA_SIMPLE_CHECK' }, '*');
|
| 701 |
+
});
|
| 702 |
+
|
| 703 |
+
window.handleSuggestionClick = chatUI.handleSuggestionClick;
|
static/js/state.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// static/js/state.js
|
| 2 |
+
|
| 3 |
+
export let chatSessions = [];
|
| 4 |
+
export let activeChatId = null;
|
| 5 |
+
export let attachedFile = null;
|
| 6 |
+
export let isGenerating = false;
|
| 7 |
+
export let globalAbortController = null;
|
| 8 |
+
export let currentUploadXHR = null; // این متغیر دیگر برای آپلود استفاده نمیشود اما برای لغو پردازش فایل نگه داشته میشود
|
| 9 |
+
export let activeToolPrefix = null;
|
| 10 |
+
export let activeTool = null;
|
| 11 |
+
|
| 12 |
+
// *** START: ADDED FOR MESSAGE LIMIT ***
|
| 13 |
+
const DAILY_MESSAGE_LIMIT = 10;
|
| 14 |
+
const MESSAGE_LIMIT_KEY = 'alphaChatDailyLimit';
|
| 15 |
+
|
| 16 |
+
/**
|
| 17 |
+
* Gets the current date as a formatted string (YYYY-MM-DD).
|
| 18 |
+
* @returns {string} The formatted date string.
|
| 19 |
+
*/
|
| 20 |
+
function getTodayDateString() {
|
| 21 |
+
const today = new Date();
|
| 22 |
+
const year = today.getFullYear();
|
| 23 |
+
const month = String(today.getMonth() + 1).padStart(2, '0');
|
| 24 |
+
const day = String(today.getDate()).padStart(2, '0');
|
| 25 |
+
return `${year}-${month}-${day}`;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
/**
|
| 29 |
+
* Retrieves usage data from localStorage.
|
| 30 |
+
* @returns {{count: number, date: string}} The usage data object.
|
| 31 |
+
*/
|
| 32 |
+
function getUsageData() {
|
| 33 |
+
try {
|
| 34 |
+
const saved = localStorage.getItem(MESSAGE_LIMIT_KEY);
|
| 35 |
+
return saved ? JSON.parse(saved) : { count: 0, date: getTodayDateString() };
|
| 36 |
+
} catch (e) {
|
| 37 |
+
console.error("Failed to parse usage data from localStorage:", e);
|
| 38 |
+
return { count: 0, date: getTodayDateString() };
|
| 39 |
+
}
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
/**
|
| 43 |
+
* Saves usage data to localStorage.
|
| 44 |
+
* @param {{count: number, date: string}} data The usage data object to save.
|
| 45 |
+
*/
|
| 46 |
+
function saveUsageData(data) {
|
| 47 |
+
try {
|
| 48 |
+
localStorage.setItem(MESSAGE_LIMIT_KEY, JSON.stringify(data));
|
| 49 |
+
} catch (e) {
|
| 50 |
+
console.error("Failed to save usage data to localStorage:", e);
|
| 51 |
+
}
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
/**
|
| 55 |
+
* Checks if the daily message limit has been reached for a free user.
|
| 56 |
+
* @param {boolean} isPremium - Whether the current user is premium.
|
| 57 |
+
* @returns {boolean} - True if the limit is reached, otherwise false.
|
| 58 |
+
*/
|
| 59 |
+
export function isMessageLimitReached(isPremium) {
|
| 60 |
+
if (isPremium) {
|
| 61 |
+
return false; // Premium users have no limit.
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
let usageData = getUsageData();
|
| 65 |
+
const today = getTodayDateString();
|
| 66 |
+
|
| 67 |
+
if (usageData.date !== today) {
|
| 68 |
+
// It's a new day, reset the counter.
|
| 69 |
+
usageData = { count: 0, date: today };
|
| 70 |
+
saveUsageData(usageData);
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
return usageData.count >= DAILY_MESSAGE_LIMIT;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
/**
|
| 77 |
+
* Increments the message count for a free user for the current day.
|
| 78 |
+
* @param {boolean} isPremium - Whether the current user is premium.
|
| 79 |
+
*/
|
| 80 |
+
export function incrementMessageCount(isPremium) {
|
| 81 |
+
if (isPremium) {
|
| 82 |
+
return; // Don't track for premium users.
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
let usageData = getUsageData();
|
| 86 |
+
const today = getTodayDateString();
|
| 87 |
+
|
| 88 |
+
if (usageData.date !== today) {
|
| 89 |
+
// If the day changed between the check and the increment, reset first.
|
| 90 |
+
usageData = { count: 1, date: today };
|
| 91 |
+
} else {
|
| 92 |
+
usageData.count += 1;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
saveUsageData(usageData);
|
| 96 |
+
}
|
| 97 |
+
// *** END: ADDED FOR MESSAGE LIMIT ***
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
export function setActiveTool(toolName) {
|
| 101 |
+
activeTool = toolName;
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
export function getActiveTool() {
|
| 105 |
+
return activeTool;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
export function setActiveToolPrefix(prefix) {
|
| 109 |
+
activeToolPrefix = prefix;
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
export function getActiveToolPrefix() {
|
| 113 |
+
return activeToolPrefix;
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
export function setChatSessions(newSessions) {
|
| 117 |
+
chatSessions = newSessions;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
export function setActiveChatId(id) {
|
| 121 |
+
activeChatId = id;
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
export function setAttachedFile(file) {
|
| 125 |
+
attachedFile = file;
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
export function setGenerating(status) {
|
| 129 |
+
isGenerating = status;
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
export function setGlobalAbortController(controller) {
|
| 133 |
+
globalAbortController = controller;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
export function setCurrentUploadXHR(xhr) {
|
| 137 |
+
currentUploadXHR = xhr;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
export function getActiveChat() {
|
| 141 |
+
return chatSessions.find(s => s.id === activeChatId);
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
export function saveSessions() {
|
| 145 |
+
try {
|
| 146 |
+
const sessionsToSave = JSON.parse(JSON.stringify(chatSessions));
|
| 147 |
+
|
| 148 |
+
sessionsToSave.forEach(session => {
|
| 149 |
+
session.messages.forEach(message => {
|
| 150 |
+
if (message.parts) {
|
| 151 |
+
message.parts.forEach(part => {
|
| 152 |
+
if (part.base64Data) {
|
| 153 |
+
delete part.base64Data;
|
| 154 |
+
}
|
| 155 |
+
if (part.blobUrl) {
|
| 156 |
+
delete part.blobUrl;
|
| 157 |
+
}
|
| 158 |
+
});
|
| 159 |
+
}
|
| 160 |
+
});
|
| 161 |
+
});
|
| 162 |
+
|
| 163 |
+
localStorage.setItem('alphaChatSessions', JSON.stringify(sessionsToSave));
|
| 164 |
+
} catch (e) {
|
| 165 |
+
console.error("Failed to save sessions to localStorage:", e);
|
| 166 |
+
}
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
export function loadSessions() {
|
| 170 |
+
try {
|
| 171 |
+
const saved = localStorage.getItem('alphaChatSessions');
|
| 172 |
+
chatSessions = saved ? JSON.parse(saved) : [];
|
| 173 |
+
chatSessions.forEach(session => {
|
| 174 |
+
if (session.showThoughts === undefined) {
|
| 175 |
+
session.showThoughts = false;
|
| 176 |
+
}
|
| 177 |
+
// *** START: MODIFIED - افزودن فیلد جدید به پیامهای قدیمی برای سازگاری ***
|
| 178 |
+
session.messages.forEach(message => {
|
| 179 |
+
if (message.role === 'assistant' && message.wasGeneratedWithThoughts === undefined) {
|
| 180 |
+
message.wasGeneratedWithThoughts = false;
|
| 181 |
+
}
|
| 182 |
+
});
|
| 183 |
+
// *** END: MODIFIED ***
|
| 184 |
+
});
|
| 185 |
+
} catch (e) {
|
| 186 |
+
console.error("Failed to load sessions from localStorage:", e);
|
| 187 |
+
chatSessions = [];
|
| 188 |
+
}
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
export function findLastIndex(array, predicate) {
|
| 192 |
+
for (let i = array.length - 1; i >= 0; i--) {
|
| 193 |
+
if (predicate(array[i])) { return i; }
|
| 194 |
+
}
|
| 195 |
+
return -1;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
export function deleteMessage(chatId, messageIndex) {
|
| 199 |
+
const chat = chatSessions.find(s => s.id === chatId);
|
| 200 |
+
if (chat && chat.messages[messageIndex]) {
|
| 201 |
+
chat.messages.splice(messageIndex, 1);
|
| 202 |
+
saveSessions();
|
| 203 |
+
}
|
| 204 |
+
}
|