Mark-Lasfar commited on
Commit
1b2adb2
·
1 Parent(s): 2d34eda

update full

Browse files
api/__pycache__/database.cpython-311.pyc CHANGED
Binary files a/api/__pycache__/database.cpython-311.pyc and b/api/__pycache__/database.cpython-311.pyc differ
 
api/auth.py CHANGED
@@ -16,8 +16,9 @@ from typing import Optional, Dict
16
  import os
17
  import logging
18
  import secrets
 
19
 
20
- from api.database import User, OAuthAccount, CustomSQLAlchemyUserDatabase, get_user_db # استيراد من database.py
21
  from api.models import UserRead, UserCreate, UserUpdate
22
 
23
  # إعداد اللوقينج
@@ -151,7 +152,7 @@ google_oauth_router = get_oauth_router(
151
  get_user_manager,
152
  state_secret=SECRET,
153
  associate_by_email=True,
154
- redirect_url=GOOGLE_REDIRECT_URL,
155
  )
156
 
157
  github_oauth_client._access_token_url = "https://github.com/login/oauth/access_token"
@@ -162,7 +163,7 @@ github_oauth_router = get_oauth_router(
162
  get_user_manager,
163
  state_secret=SECRET,
164
  associate_by_email=True,
165
- redirect_url=GITHUB_REDIRECT_URL,
166
  )
167
 
168
  fastapi_users = FastAPIUsers[User, int](
@@ -180,4 +181,4 @@ def get_auth_router(app: FastAPI):
180
  app.include_router(fastapi_users.get_register_router(UserRead, UserCreate), prefix="/auth", tags=["auth"])
181
  app.include_router(fastapi_users.get_reset_password_router(), prefix="/auth", tags=["auth"])
182
  app.include_router(fastapi_users.get_verify_router(UserRead), prefix="/auth", tags=["auth"])
183
- app.include_router(fastapi_users.get_users_router(UserRead, UserUpdate), prefix="/users", tags=["users"])
 
16
  import os
17
  import logging
18
  import secrets
19
+ from fastapi.responses import RedirectResponse
20
 
21
+ from api.database import User, OAuthAccount, CustomSQLAlchemyUserDatabase, get_user_db
22
  from api.models import UserRead, UserCreate, UserUpdate
23
 
24
  # إعداد اللوقينج
 
152
  get_user_manager,
153
  state_secret=SECRET,
154
  associate_by_email=True,
155
+ redirect_url="https://mgzon-mgzon-app.hf.space/chat", # تعديل الـ redirect_url ليحوّل مباشرة إلى /chat
156
  )
157
 
158
  github_oauth_client._access_token_url = "https://github.com/login/oauth/access_token"
 
163
  get_user_manager,
164
  state_secret=SECRET,
165
  associate_by_email=True,
166
+ redirect_url="https://mgzon-mgzon-app.hf.space/chat", # تعديل الـ redirect_url ليحوّل مباشرة إلى /chat
167
  )
168
 
169
  fastapi_users = FastAPIUsers[User, int](
 
181
  app.include_router(fastapi_users.get_register_router(UserRead, UserCreate), prefix="/auth", tags=["auth"])
182
  app.include_router(fastapi_users.get_reset_password_router(), prefix="/auth", tags=["auth"])
183
  app.include_router(fastapi_users.get_verify_router(UserRead), prefix="/auth", tags=["auth"])
184
+ app.include_router(fastapi_users.get_users_router(UserRead, UserUpdate), prefix="/users", tags=["users"])
api/database.py CHANGED
@@ -1,146 +1,94 @@
1
- # api/database.py
2
  # SPDX-FileCopyrightText: Hadad <hadad@linuxmail.org>
3
  # SPDX-License-License: Apache-2.0
4
 
5
  import os
6
  import logging
7
- from datetime import datetime
8
- from typing import AsyncGenerator, Optional, Dict, Any
9
-
10
- from sqlalchemy import Column, String, Integer, ForeignKey, DateTime, Boolean, Text, select
11
- from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
12
- from sqlalchemy.ext.declarative import declarative_base
13
- from sqlalchemy.orm import relationship
14
- from fastapi import Depends
15
- from fastapi_users.db import SQLAlchemyBaseUserTable, SQLAlchemyUserDatabase
16
- import aiosqlite
17
 
18
  # إعداد اللوج
 
19
  logger = logging.getLogger(__name__)
20
 
21
- # استخدم القيمة مباشرة إذا لم يكن هناك متغير بيئة
22
- SQLALCHEMY_DATABASE_URL = os.environ.get(
23
- "SQLALCHEMY_DATABASE_URL"
24
- ) or "sqlite+aiosqlite:///./data/mgzon_users.db"
25
-
26
- # تأكد أن الدرايفر async
27
- if "aiosqlite" not in SQLALCHEMY_DATABASE_URL:
28
- raise ValueError("Database URL must use 'sqlite+aiosqlite' for async support")
29
-
30
- # إنشاء محرك async
31
- async_engine = create_async_engine(
32
- SQLALCHEMY_DATABASE_URL,
33
- echo=True,
34
- connect_args={"check_same_thread": False}
35
- )
36
-
37
- # إعداد الجلسة async
38
- AsyncSessionLocal = async_sessionmaker(
39
- async_engine,
40
- expire_on_commit=False,
41
- class_=AsyncSession
42
- )
43
-
44
- # القاعدة الأساسية للنماذج
45
- Base = declarative_base()
46
-
47
- # النماذج (Models)
48
- class OAuthAccount(Base):
49
- __tablename__ = "oauth_account"
50
- id = Column(Integer, primary_key=True, index=True)
51
- user_id = Column(Integer, ForeignKey("user.id"), nullable=False)
52
- oauth_name = Column(String, nullable=False)
53
- access_token = Column(String, nullable=False)
54
- expires_at = Column(Integer, nullable=True)
55
- refresh_token = Column(String, nullable=True)
56
- account_id = Column(String, index=True, nullable=False)
57
- account_email = Column(String, nullable=False)
58
-
59
- user = relationship("User", back_populates="oauth_accounts", lazy="selectin")
60
-
61
- class User(SQLAlchemyBaseUserTable[int], Base):
62
- __tablename__ = "user"
63
- id = Column(Integer, primary_key=True, index=True)
64
- email = Column(String, unique=True, index=True, nullable=False)
65
- hashed_password = Column(String, nullable=False)
66
- is_active = Column(Boolean, default=True)
67
- is_superuser = Column(Boolean, default=False)
68
- is_verified = Column(Boolean, default=False)
69
- display_name = Column(String, nullable=True)
70
- preferred_model = Column(String, nullable=True)
71
- job_title = Column(String, nullable=True)
72
- education = Column(String, nullable=True)
73
- interests = Column(String, nullable=True)
74
- additional_info = Column(Text, nullable=True)
75
- conversation_style = Column(String, nullable=True)
76
-
77
- oauth_accounts = relationship("OAuthAccount", back_populates="user", cascade="all, delete-orphan")
78
- conversations = relationship("Conversation", back_populates="user", cascade="all, delete-orphan")
79
-
80
- class Conversation(Base):
81
- __tablename__ = "conversation"
82
- id = Column(Integer, primary_key=True, index=True)
83
- conversation_id = Column(String, unique=True, index=True, nullable=False)
84
- user_id = Column(Integer, ForeignKey("user.id"), nullable=False)
85
- title = Column(String, nullable=False)
86
- created_at = Column(DateTime, default=datetime.utcnow)
87
- updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
88
-
89
- user = relationship("User", back_populates="conversations")
90
- messages = relationship("Message", back_populates="conversation", cascade="all, delete-orphan")
91
-
92
- class Message(Base):
93
- __tablename__ = "message"
94
- id = Column(Integer, primary_key=True, index=True)
95
- conversation_id = Column(Integer, ForeignKey("conversation.id"), nullable=False)
96
- role = Column(String, nullable=False)
97
- content = Column(Text, nullable=False)
98
- created_at = Column(DateTime, default=datetime.utcnow)
99
-
100
- conversation = relationship("Conversation", back_populates="messages")
101
 
102
- # قاعدة بيانات المستخدم المخصصة (نقلناها من user_db.py)
103
- class CustomSQLAlchemyUserDatabase(SQLAlchemyUserDatabase[User, int]):
104
- """
105
- قاعدة بيانات مخصَّصة لمكتبة fastapi-users.
106
- تضيف طريقة parse_id التي تُحوِّل الـ ID من str → int.
107
- """
108
- def parse_id(self, value: Any) -> int:
109
- logger.debug(f"Parsing user id: {value} (type={type(value)})")
110
- return int(value) if isinstance(value, str) else value
111
-
112
- async def get_by_email(self, email: str) -> Optional[User]:
113
- logger.info(f"Looking for user with email: {email}")
114
- stmt = select(self.user_table).where(self.user_table.email == email)
115
- result = await self.session.execute(stmt)
116
- return result.scalar_one_or_none()
117
 
118
- async def create(self, create_dict: Dict[str, Any]) -> User:
119
- logger.info(f"Creating new user: {create_dict.get('email')}")
120
- user = self.user_table(**create_dict)
121
- self.session.add(user)
122
- await self.session.commit()
123
- await self.session.refresh(user)
124
- return user
 
125
 
126
- # دالة لجلب الجلسة async
127
- async def get_db() -> AsyncGenerator[AsyncSession, None]:
128
  async with AsyncSessionLocal() as session:
129
  try:
130
- yield session
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  finally:
132
  await session.close()
133
 
134
- # دالة لجلب قاعدة بيانات المستخدمين لـ fastapi-users
135
- async def get_user_db(session: AsyncSession = Depends(get_db)) -> AsyncGenerator[CustomSQLAlchemyUserDatabase, None]:
136
- yield CustomSQLAlchemyUserDatabase(session, User, OAuthAccount)
137
 
138
- # دالة لإنشاء الجداول
139
- async def init_db():
140
  try:
141
- async with async_engine.begin() as conn:
142
- await conn.run_sync(Base.metadata.create_all)
143
- logger.info("Database tables created successfully")
144
  except Exception as e:
145
- logger.error(f"Error creating database tables: {e}")
146
  raise
 
1
+ # init_db.py
2
  # SPDX-FileCopyrightText: Hadad <hadad@linuxmail.org>
3
  # SPDX-License-License: Apache-2.0
4
 
5
  import os
6
  import logging
7
+ import asyncio
8
+ from sqlalchemy.ext.asyncio import AsyncSession
9
+ from sqlalchemy import select, delete
10
+ from api.database import async_engine, Base, User, OAuthAccount, Conversation, Message, AsyncSessionLocal
11
+ from passlib.context import CryptContext
 
 
 
 
 
12
 
13
  # إعداد اللوج
14
+ logging.basicConfig(level=logging.INFO)
15
  logger = logging.getLogger(__name__)
16
 
17
+ # إعداد تشفير كلمة المرور
18
+ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
+ async def init_db():
21
+ logger.info("Starting database initialization...")
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
+ # إنشاء الجداول
24
+ try:
25
+ async with async_engine.begin() as conn:
26
+ await conn.run_sync(Base.metadata.create_all)
27
+ logger.info("Database tables created successfully.")
28
+ except Exception as e:
29
+ logger.error(f"Error creating database tables: {e}")
30
+ raise
31
 
32
+ # تنظيف البيانات غير المتسقة
 
33
  async with AsyncSessionLocal() as session:
34
  try:
35
+ # حذف سجلات oauth_accounts اللي مش مرتبطة بمستخدم موجود
36
+ stmt = delete(OAuthAccount).where(
37
+ OAuthAccount.user_id.notin_(select(User.id))
38
+ )
39
+ result = await session.execute(stmt)
40
+ deleted_count = result.rowcount
41
+ await session.commit()
42
+ logger.info(f"Deleted {deleted_count} orphaned OAuth accounts.")
43
+
44
+ # التأكد من إن كل المستخدمين ليهم is_active=True
45
+ users = (await session.execute(select(User))).scalars().all()
46
+ for user in users:
47
+ if not user.is_active:
48
+ user.is_active = True
49
+ logger.info(f"Updated user {user.email} to is_active=True")
50
+ await session.commit()
51
+
52
+ # اختبار إنشاء مستخدم ومحادثة (اختياري)
53
+ test_user = (await session.execute(
54
+ select(User).filter_by(email="test@example.com")
55
+ )).scalar_one_or_none()
56
+ if not test_user:
57
+ test_user = User(
58
+ email="test@example.com",
59
+ hashed_password=pwd_context.hash("testpassword123"),
60
+ is_active=True,
61
+ display_name="Test User"
62
+ )
63
+ session.add(test_user)
64
+ await session.commit()
65
+ logger.info("Test user created successfully.")
66
+
67
+ test_conversation = (await session.execute(
68
+ select(Conversation).filter_by(user_id=test_user.id)
69
+ )).scalar_one_or_none()
70
+ if not test_conversation:
71
+ test_conversation = Conversation(
72
+ conversation_id="test-conversation-1",
73
+ user_id=test_user.id,
74
+ title="Test Conversation"
75
+ )
76
+ session.add(test_conversation)
77
+ await session.commit()
78
+ logger.info("Test conversation created successfully.")
79
+
80
+ except Exception as e:
81
+ await session.rollback()
82
+ logger.error(f"Error during initialization: {e}")
83
+ raise
84
  finally:
85
  await session.close()
86
 
87
+ logger.info("Database initialization completed.")
 
 
88
 
89
+ if __name__ == "__main__":
 
90
  try:
91
+ asyncio.run(init_db())
 
 
92
  except Exception as e:
93
+ logger.error(f"Failed to initialize database: {e}")
94
  raise
create_dummy_user.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # create_dummy_user.py
2
+
3
+ import asyncio
4
+ from api.database import get_db, User
5
+ from passlib.hash import bcrypt
6
+
7
+ async def create_dummy_user():
8
+ async for session in get_db():
9
+ existing = await session.execute(
10
+ User.__table__.select().where(User.email == "admin@example.com")
11
+ )
12
+ if existing.scalar():
13
+ print("⚠️ User already exists.")
14
+ return
15
+
16
+ user = User(
17
+ email="admin@example.com",
18
+ hashed_password=bcrypt.hash("00000000"),
19
+ is_active=True,
20
+ is_superuser=True,
21
+ is_verified=True,
22
+ display_name="Admin"
23
+ )
24
+ session.add(user)
25
+ await session.commit()
26
+ await session.refresh(user)
27
+ print(f"✅ User created: {user.email}")
28
+
29
+ if __name__ == "__main__":
30
+ asyncio.run(create_dummy_user())
data/mgzon_users.db CHANGED
Binary files a/data/mgzon_users.db and b/data/mgzon_users.db differ
 
requirements.txt CHANGED
@@ -1,5 +1,13 @@
1
- fastapi==0.115.2
2
- packaging>=23.0
 
 
 
 
 
 
 
 
3
  uvicorn==0.30.6
4
  openai==1.51.2
5
  httpx==0.27.0
@@ -22,17 +30,9 @@ webrtcvad==2.0.10
22
  Pillow==10.4.0
23
  urllib3==2.2.3
24
  itsdangerous==2.2.0
25
- protobuf==3.19.6 # عدلنا من 4.25.5 عشان يتوافق مع descript-audiotools
26
- fastapi-users[sqlalchemy,oauth2]==14.0.0
27
- sqlalchemy==2.0.35
28
- python-jose[cryptography]==3.3.0
29
- passlib[bcrypt]==1.7.4
30
- httpx-oauth==0.16.1
31
- python-multipart==0.0.17
32
  aiofiles==24.1.0
33
  motor==3.7.0
34
- aiosqlite
35
-
36
  redis==5.0.0
37
  markdown2==2.5.0
38
  pymongo==4.10.1
@@ -44,4 +44,4 @@ wsproto>=1.2.0
44
  descript-audiotools>=0.7.2
45
  scipy>=1.15.0
46
  librosa>=0.10.0
47
- matplotlib>=3.10.0
 
1
+ fastapi==0.95.2
2
+ fastapi-users[sqlalchemy,oauth2]==10.4.2
3
+ pydantic==1.10.13
4
+ email-validator==1.3.1
5
+ aiosqlite==0.21.0
6
+ sqlalchemy==2.0.43
7
+ python-jose[cryptography]==3.3.0
8
+ passlib[bcrypt]==1.7.4
9
+ httpx-oauth==0.16.1
10
+ python-multipart==0.0.6
11
  uvicorn==0.30.6
12
  openai==1.51.2
13
  httpx==0.27.0
 
30
  Pillow==10.4.0
31
  urllib3==2.2.3
32
  itsdangerous==2.2.0
33
+ protobuf==3.19.6
 
 
 
 
 
 
34
  aiofiles==24.1.0
35
  motor==3.7.0
 
 
36
  redis==5.0.0
37
  markdown2==2.5.0
38
  pymongo==4.10.1
 
44
  descript-audiotools>=0.7.2
45
  scipy>=1.15.0
46
  librosa>=0.10.0
47
+ matplotlib>=3.10.0
static/images/settings-icon.svg ADDED
static/images/splash-screen-750x1334.png ADDED
static/js/sw.js CHANGED
@@ -2,43 +2,102 @@
2
  // SPDX-FileCopyrightText: Hadad <hadad@linuxmail.org>
3
  // SPDX-License-Identifier: Apache-2.0
4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  self.addEventListener('install', (event) => {
6
  event.waitUntil(
7
- caches.open('ChatMG-v1').then((cache) => {
8
- return cache.addAll([
9
- '/',
10
- '/chat',
11
- '/static/css/style.css',
12
- '/static/css/chat/style.css',
13
- '/static/css/sidebar.css',
14
- '/static/js/chat.js',
15
- '/static/images/mg.svg',
16
- '/static/images/icons/mg-48.png',
17
- '/static/images/icons/mg-72.png',
18
- '/static/images/icons/mg-96.png',
19
- '/static/images/icons/mg-128.png',
20
- '/static/images/icons/mg-192.png',
21
- '/static/images/icons/mg-256.png',
22
- '/static/images/icons/mg-384.png',
23
- '/static/images/icons/mg-512.png'
24
- ]);
25
  })
26
  );
27
  });
 
 
28
  self.addEventListener('activate', (event) => {
29
  event.waitUntil(
30
  caches.keys().then((cacheNames) => {
31
  return Promise.all(
32
- cacheNames.filter((name) => name !== 'ChatMG-v1').map((name) => caches.delete(name))
33
  );
34
  })
35
  );
36
  });
37
 
 
38
  self.addEventListener('fetch', (event) => {
39
- event.respondWith(
40
- caches.match(event.request).then((response) => {
41
- return response || fetch(event.request);
42
- })
43
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  // SPDX-FileCopyrightText: Hadad <hadad@linuxmail.org>
3
  // SPDX-License-Identifier: Apache-2.0
4
 
5
+ const CACHE_NAME = 'ChatMG-v2'; // غيّر الإصدار عشان يجبر الـ cache يتحدّث
6
+ const urlsToCache = [
7
+ '/',
8
+ '/chat',
9
+ '/static/css/style.css',
10
+ '/static/css/chat/style.css',
11
+ '/static/css/sidebar.css',
12
+ '/static/js/chat.js',
13
+ '/static/images/mg.svg',
14
+ '/static/images/icons/mg-48.png',
15
+ '/static/images/icons/mg-72.png',
16
+ '/static/images/icons/mg-96.png',
17
+ '/static/images/icons/mg-128.png',
18
+ '/static/images/icons/mg-192.png',
19
+ '/static/images/icons/mg-256.png',
20
+ '/static/images/icons/mg-384.png',
21
+ '/static/images/icons/mg-512.png',
22
+ '/static/images/splash-screen.png',
23
+ '/static/images/splash-screen-750x1334.png',
24
+ '/static/images/settings-icon.svg',
25
+ '/static/images/swipe-hint.svg'
26
+ ];
27
+
28
+ // Install event
29
  self.addEventListener('install', (event) => {
30
  event.waitUntil(
31
+ caches.open(CACHE_NAME).then((cache) => {
32
+ console.log('Opened cache for install');
33
+ return cache.addAll(urlsToCache);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  })
35
  );
36
  });
37
+
38
+ // Activate event
39
  self.addEventListener('activate', (event) => {
40
  event.waitUntil(
41
  caches.keys().then((cacheNames) => {
42
  return Promise.all(
43
+ cacheNames.filter((name) => name !== CACHE_NAME).map((name) => caches.delete(name))
44
  );
45
  })
46
  );
47
  });
48
 
49
+ // Fetch event with dynamic caching and stale-while-revalidate
50
  self.addEventListener('fetch', (event) => {
51
+ const { url } = event.request;
52
+ const isAPI = url.includes('/api/');
53
+ const isStatic = url.includes('/static/');
54
+
55
+ if (isStatic || isAPI) {
56
+ event.respondWith(
57
+ caches.match(event.request).then((response) => {
58
+ if (response) {
59
+ // Return cached version immediately (stale)
60
+ event.waitUntil(updateCache(event.request));
61
+ return response;
62
+ }
63
+ return fetch(event.request).then((fetchResponse) => {
64
+ // Cache the new response
65
+ if (fetchResponse.ok) {
66
+ return caches.open(CACHE_NAME).then((cache) => {
67
+ cache.put(event.request, fetchResponse.clone());
68
+ return fetchResponse;
69
+ });
70
+ }
71
+ return fetchResponse;
72
+ });
73
+ }).catch(() => {
74
+ // Offline fallback for API
75
+ if (isAPI) {
76
+ return new Response(JSON.stringify({ error: 'Offline mode - Please check your connection' }), {
77
+ status: 503,
78
+ headers: { 'Content-Type': 'application/json' }
79
+ });
80
+ }
81
+ return caches.match(event.request);
82
+ })
83
+ );
84
+ } else {
85
+ // For other requests, use network-first
86
+ event.respondWith(
87
+ fetch(event.request).catch(() => caches.match(event.request))
88
+ );
89
+ }
90
  });
91
+
92
+ // Function to update cache in background
93
+ async function updateCache(request) {
94
+ try {
95
+ const response = await fetch(request);
96
+ if (response.ok) {
97
+ const cache = await caches.open(CACHE_NAME);
98
+ await cache.put(request, response.clone());
99
+ }
100
+ } catch (error) {
101
+ console.log('Cache update failed:', error);
102
+ }
103
+ }
static/manifest.json CHANGED
@@ -7,51 +7,105 @@
7
  "background_color": "#1a202c",
8
  "theme_color": "#2d3748",
9
  "scope": "/",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  "icons": [
11
  {
12
  "src": "/static/images/mg.svg",
13
  "sizes": "192x192",
14
- "type": "image/svg+xml"
 
15
  },
16
  {
17
  "src": "/static/images/icons/mg-48.png",
18
  "sizes": "48x48",
19
- "type": "image/png"
 
20
  },
21
  {
22
  "src": "/static/images/icons/mg-72.png",
23
  "sizes": "72x72",
24
- "type": "image/png"
 
25
  },
26
  {
27
  "src": "/static/images/icons/mg-96.png",
28
  "sizes": "96x96",
29
- "type": "image/png"
 
30
  },
31
  {
32
  "src": "/static/images/icons/mg-128.png",
33
  "sizes": "128x128",
34
- "type": "image/png"
 
35
  },
36
  {
37
  "src": "/static/images/icons/mg-192.png",
38
  "sizes": "192x192",
39
- "type": "image/png"
 
40
  },
41
  {
42
  "src": "/static/images/icons/mg-256.png",
43
  "sizes": "256x256",
44
- "type": "image/png"
 
45
  },
46
  {
47
  "src": "/static/images/icons/mg-384.png",
48
  "sizes": "384x384",
49
- "type": "image/png"
 
50
  },
51
  {
52
  "src": "/static/images/icons/mg-512.png",
53
  "sizes": "512x512",
54
- "type": "image/png"
 
55
  }
56
  ]
57
- }
 
7
  "background_color": "#1a202c",
8
  "theme_color": "#2d3748",
9
  "scope": "/",
10
+ "lang": "en",
11
+ "dir": "ltr",
12
+ "categories": ["productivity", "utilities", "business"],
13
+ "shortcuts": [
14
+ {
15
+ "name": "New Chat",
16
+ "short_name": "Chat",
17
+ "description": "Start a new conversation",
18
+ "url": "/chat",
19
+ "icons": [
20
+ {
21
+ "src": "/static/images/mg.svg",
22
+ "sizes": "96x96"
23
+ }
24
+ ]
25
+ },
26
+ {
27
+ "name": "Settings",
28
+ "short_name": "Settings",
29
+ "description": "Open settings",
30
+ "url": "/chat?settings=open",
31
+ "icons": [
32
+ {
33
+ "src": "/static/images/settings-icon.svg",
34
+ "sizes": "96x96"
35
+ }
36
+ ]
37
+ }
38
+ ],
39
+ "splash_screens": [
40
+ {
41
+ "src": "/static/images/splash-screen.png",
42
+ "sizes": "1080x1920",
43
+ "type": "image/png",
44
+ "background_color": "#1a202c",
45
+ "position": "center"
46
+ },
47
+ {
48
+ "src": "/static/images/splash-screen-750x1334.png",
49
+ "sizes": "750x1334",
50
+ "type": "image/png",
51
+ "background_color": "#1a202c",
52
+ "position": "center"
53
+ }
54
+ ],
55
  "icons": [
56
  {
57
  "src": "/static/images/mg.svg",
58
  "sizes": "192x192",
59
+ "type": "image/svg+xml",
60
+ "purpose": "any maskable"
61
  },
62
  {
63
  "src": "/static/images/icons/mg-48.png",
64
  "sizes": "48x48",
65
+ "type": "image/png",
66
+ "purpose": "any maskable"
67
  },
68
  {
69
  "src": "/static/images/icons/mg-72.png",
70
  "sizes": "72x72",
71
+ "type": "image/png",
72
+ "purpose": "any maskable"
73
  },
74
  {
75
  "src": "/static/images/icons/mg-96.png",
76
  "sizes": "96x96",
77
+ "type": "image/png",
78
+ "purpose": "any maskable"
79
  },
80
  {
81
  "src": "/static/images/icons/mg-128.png",
82
  "sizes": "128x128",
83
+ "type": "image/png",
84
+ "purpose": "any maskable"
85
  },
86
  {
87
  "src": "/static/images/icons/mg-192.png",
88
  "sizes": "192x192",
89
+ "type": "image/png",
90
+ "purpose": "any maskable"
91
  },
92
  {
93
  "src": "/static/images/icons/mg-256.png",
94
  "sizes": "256x256",
95
+ "type": "image/png",
96
+ "purpose": "any maskable"
97
  },
98
  {
99
  "src": "/static/images/icons/mg-384.png",
100
  "sizes": "384x384",
101
+ "type": "image/png",
102
+ "purpose": "any maskable"
103
  },
104
  {
105
  "src": "/static/images/icons/mg-512.png",
106
  "sizes": "512x512",
107
+ "type": "image/png",
108
+ "purpose": "any maskable"
109
  }
110
  ]
111
+ }
templates/chat.html CHANGED
@@ -95,6 +95,11 @@
95
  <link rel="stylesheet" href="/static/css/style.css" />
96
  <link rel="stylesheet" href="/static/css/webkit.css" />
97
  <link rel="stylesheet" href="/static/css/sidebar.css" />
 
 
 
 
 
98
  </head>
99
 
100
  <body class="min-h-screen flex flex-col bg-gradient-to-br from-gray-900 via-teal-900 to-emerald-900">
@@ -400,10 +405,16 @@
400
  <div class="text-center text-xs text-gray-400 py-2">
401
  © 2025 Mark Al-Asfar & MGZon AI. All rights reserved.
402
  </div>
 
403
  <button id="installAppBtn" style="display: none;"
404
  class="fixed bottom-4 right-4 bg-blue-600 text-white px-4 py-2 rounded shadow-lg z-50">
405
  📲 Install MG Chat
406
  </button>
 
 
 
 
 
407
  </div>
408
 
409
  <!-- Scripts -->
@@ -441,7 +452,6 @@
441
  const installBtn = document.getElementById('installAppBtn');
442
  if (installBtn) {
443
  installBtn.style.display = 'block';
444
-
445
  installBtn.addEventListener('click', () => {
446
  deferredPrompt.prompt();
447
  deferredPrompt.userChoice.then(choice => {
@@ -455,8 +465,100 @@
455
  });
456
  }
457
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
458
  </script>
459
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
460
  </body>
461
 
462
  </html>
 
95
  <link rel="stylesheet" href="/static/css/style.css" />
96
  <link rel="stylesheet" href="/static/css/webkit.css" />
97
  <link rel="stylesheet" href="/static/css/sidebar.css" />
98
+ <!-- Preload Resources -->
99
+ <link rel="preload" href="/static/js/chat.js" as="script">
100
+ <link rel="preload" href="/static/css/chat/style.css" as="style">
101
+ <link rel="preload" href="/static/images/mg.svg" as="image">
102
+ <link rel="preload" href="/static/images/splash-screen.png" as="image">
103
  </head>
104
 
105
  <body class="min-h-screen flex flex-col bg-gradient-to-br from-gray-900 via-teal-900 to-emerald-900">
 
405
  <div class="text-center text-xs text-gray-400 py-2">
406
  © 2025 Mark Al-Asfar & MGZon AI. All rights reserved.
407
  </div>
408
+ <!-- Install Button -->
409
  <button id="installAppBtn" style="display: none;"
410
  class="fixed bottom-4 right-4 bg-blue-600 text-white px-4 py-2 rounded shadow-lg z-50">
411
  📲 Install MG Chat
412
  </button>
413
+ <!-- PWA Install Instructions -->
414
+ <div id="pwa-instructions" class="fixed bottom-4 left-4 right-4 bg-blue-600 text-white p-4 rounded-lg z-50 hidden">
415
+ <p><strong>Add to Home Screen:</strong> Tap the share button in your browser, then "Add to Home Screen" for a native-like experience!</p>
416
+ <button onclick="document.getElementById('pwa-instructions').classList.add('hidden')" class="ml-2 text-sm">Close</button>
417
+ </div>
418
  </div>
419
 
420
  <!-- Scripts -->
 
452
  const installBtn = document.getElementById('installAppBtn');
453
  if (installBtn) {
454
  installBtn.style.display = 'block';
 
455
  installBtn.addEventListener('click', () => {
456
  deferredPrompt.prompt();
457
  deferredPrompt.userChoice.then(choice => {
 
465
  });
466
  }
467
  });
468
+
469
+ // Show PWA instructions on first visit
470
+ if (localStorage.getItem('pwa-instructions-shown') !== 'true') {
471
+ document.getElementById('pwa-instructions').classList.remove('hidden');
472
+ localStorage.setItem('pwa-instructions-shown', 'true');
473
+ }
474
+
475
+ // Enhance touch gestures for sidebar
476
+ const sidebar = document.getElementById('sidebar');
477
+ const hammer = new Hammer(sidebar);
478
+ hammer.on('swipeleft', () => {
479
+ sidebar.classList.remove('open');
480
+ });
481
+ hammer.on('swiperight', () => {
482
+ sidebar.classList.add('open');
483
+ });
484
  </script>
485
 
486
+ <style>
487
+ /* Mobile Native Feel */
488
+ @media (max-width: 768px) {
489
+ body {
490
+ padding: 0;
491
+ margin: 0;
492
+ overflow: hidden; /* Prevent scrolling outside app */
493
+ }
494
+ .min-h-screen {
495
+ height: 100vh;
496
+ max-height: 100vh;
497
+ }
498
+ .flex-1 {
499
+ flex: 1;
500
+ overflow-y: auto;
501
+ }
502
+ #chatBox {
503
+ padding: 10px;
504
+ font-size: 16px; /* Prevent zoom on iOS */
505
+ }
506
+ textarea, input, select, button {
507
+ font-size: 16px; /* Prevent zoom on iOS */
508
+ touch-action: manipulation; /* Improve touch responsiveness */
509
+ }
510
+ .chat-header {
511
+ position: sticky;
512
+ top: 0;
513
+ z-index: 10;
514
+ background: rgba(31, 41, 55, 0.9); /* Match bg-gray-800 */
515
+ backdrop-filter: blur(10px);
516
+ }
517
+ /* Hide scrollbar for native look */
518
+ #chatBox::-webkit-scrollbar, #conversationList::-webkit-scrollbar {
519
+ display: none;
520
+ }
521
+ #chatBox, #conversationList {
522
+ -ms-overflow-style: none;
523
+ scrollbar-width: none;
524
+ }
525
+ #sidebar {
526
+ width: 80%; /* Slightly narrower for better UX */
527
+ }
528
+ #swipeHint {
529
+ animation: pulse 2s infinite;
530
+ }
531
+ }
532
+
533
+ /* Prevent zoom on iOS */
534
+ input, textarea, select, button {
535
+ font-size: 16px;
536
+ touch-action: manipulation;
537
+ }
538
+
539
+ /* Full-screen for PWA */
540
+ @media (display-mode: standalone) {
541
+ body {
542
+ margin: 0;
543
+ padding: 0;
544
+ overscroll-behavior: none; /* Prevent pull-to-refresh */
545
+ }
546
+ .chat-header {
547
+ padding-top: env(safe-area-inset-top); /* Respect iOS notch */
548
+ }
549
+ #footerForm {
550
+ padding-bottom: env(safe-area-inset-bottom); /* Respect iOS bottom bar */
551
+ }
552
+ }
553
+
554
+ /* Smooth transitions for sidebar */
555
+ #sidebar {
556
+ transition: transform 0.3s ease-in-out;
557
+ }
558
+ #sidebar.open {
559
+ transform: translateX(0);
560
+ }
561
+ </style>
562
  </body>
563
 
564
  </html>