AnesKAM commited on
Commit
5b19d71
·
verified ·
1 Parent(s): 3bf537a

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +389 -524
index.html CHANGED
@@ -2,16 +2,13 @@
2
  <html lang="en" dir="ltr">
3
  <head>
4
  <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
6
- <title>Genisi AI · Hidden Sidebar</title>
7
  <link href="https://fonts.googleapis.com/css2?family=Cairo:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"/>
8
  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
9
- <script src="https://cdn.jsdelivr.net/npm/dompurify@3.1.6/dist/purify.min.js"></script>
10
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/katex.min.css"/>
11
  <script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/katex.min.js"></script>
12
  <script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/contrib/auto-render.min.js"></script>
13
- <!-- Lucide Icons -->
14
- <script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
15
  <style>
16
  :root {
17
  --bg: #0d0f14; --surface: #161920; --surface2: #1e2230; --surface3: #252a3a;
@@ -19,7 +16,7 @@
19
  --accent: #4f8ef7; --accent2: #7c5cf7; --accent-soft: rgba(79,142,247,0.12);
20
  --text: #e8eaf2; --text2: #9aa3be; --text3: #5c6480;
21
  --user-bubble: #1a2340; --bot-bubble: #161920;
22
- --danger: #f75f5f;
23
  --radius: 28px; --radius-sm: 18px; --radius-pill: 50px;
24
  --sidebar-w: 280px; --tr: 0.3s cubic-bezier(.4,0,.2,1);
25
  }
@@ -30,104 +27,113 @@
30
  --user-bubble: #dde6ff; --bot-bubble: #fff; --accent-soft: rgba(79,142,247,0.1);
31
  }
32
  *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
33
- html,body{height:100%;font-family:'Cairo',sans-serif;background:var(--bg);color:var(--text);scroll-behavior:smooth;}
34
  ::-webkit-scrollbar{width:6px}::-webkit-scrollbar-track{background:transparent}
35
  ::-webkit-scrollbar-thumb{background:var(--border2);border-radius:var(--radius-pill)}
36
 
37
- /* Lucide icon base style */
38
- .icon { width:20px; height:20px; stroke:currentColor; fill:none; stroke-width:2; stroke-linecap:round; stroke-linejoin:round; pointer-events:none; flex-shrink:0; }
39
- .icon-sm { width:16px; height:16px; }
40
- .icon-lg { width:22px; height:22px; }
41
-
42
  .app{display:flex;height:100vh;overflow:hidden}
43
 
44
  /* SIDEBAR */
45
  .sidebar{width:var(--sidebar-w);min-width:var(--sidebar-w);background:var(--surface);
46
  border-right:1px solid var(--border);display:flex;flex-direction:column;
47
  transition:width var(--tr),min-width var(--tr);overflow:hidden;
48
- border-top-right-radius:var(--radius);border-bottom-right-radius:var(--radius);}
49
- [dir="rtl"] .sidebar{border-right:none;border-left:1px solid var(--border);border-radius:0 var(--radius) var(--radius) 0;}
50
  .sidebar.collapsed{width:0;min-width:0;border:none}
51
  .sidebar-header{padding:24px 20px 16px;display:flex;align-items:center;gap:12px;}
52
  .s-logo{display:flex;align-items:center;gap:12px;flex:1}
53
- .s-logo img{width:36px;height:36px;border-radius:12px;object-fit:cover;box-shadow:0 4px 12px var(--accent-soft);}
54
  .s-logo-name{font-size:1.2rem;font-weight:700;background:linear-gradient(135deg,var(--accent),var(--accent2));-webkit-background-clip:text;-webkit-text-fill-color:transparent}
55
-
56
- .close-sidebar-btn{background:none;border:none;color:var(--text2);cursor:pointer;padding:8px;border-radius:var(--radius-pill);transition:all 0.2s;display:flex;align-items:center;justify-content:center;width:36px;height:36px;}
57
- .close-sidebar-btn:hover{background:var(--surface2);color:var(--danger);transform:scale(1.1);}
58
-
59
  .btn-new{margin:12px 16px;padding:12px 16px;background:linear-gradient(135deg,var(--accent),var(--accent2));
60
  color:#fff;border:none;border-radius:var(--radius-pill);cursor:pointer;font-family:'Cairo',sans-serif;
61
- font-size:.95rem;font-weight:600;display:flex;align-items:center;gap:8px;justify-content:center;
62
- transition:all var(--tr);box-shadow:0 4px 15px var(--accent-soft);}
63
  .btn-new:hover{opacity:.9;transform:translateY(-2px)}
64
  .s-label{padding:10px 20px;font-size:.75rem;font-weight:700;color:var(--text3);letter-spacing:.08em;text-transform:uppercase}
65
  .chats-list{flex:1;overflow-y:auto;padding:4px 12px;display:flex;flex-direction:column;gap:6px}
66
  .chat-item{padding:10px 16px;border-radius:var(--radius-sm);cursor:pointer;font-size:.9rem;color:var(--text2);
67
- display:flex;align-items:center;gap:10px;transition:all var(--tr);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
68
- .chat-item:hover{background:var(--surface2);color:var(--text);transform:translateX(4px);}
69
- [dir="rtl"] .chat-item:hover{transform:translateX(-4px);}
70
  .chat-item.active{background:var(--accent-soft);color:var(--accent);font-weight:600;}
71
  .chat-item .ct{flex:1;overflow:hidden;text-overflow:ellipsis}
72
- .del-btn{opacity:0;background:none;border:none;color:var(--danger);cursor:pointer;padding:4px;border-radius:50%;transition:all var(--tr);display:flex;align-items:center;justify-content:center;}
 
73
  .chat-item:hover .del-btn{opacity:1}
74
- .del-btn:hover{background:rgba(247,95,95,.15);transform:scale(1.1);}
75
- .s-footer{padding:16px;display:flex;align-items:center;gap:12px;justify-content:center;}
76
- .btn-icon{width:42px;height:42px;background:var(--surface2);border:none;border-radius:var(--radius-pill);
77
- color:var(--text2);cursor:pointer;display:flex;align-items:center;justify-content:center;
78
- transition:all var(--tr)}
79
- .btn-icon:hover{background:var(--surface3);color:var(--text);transform:rotate(10deg);}
80
 
81
  /* MAIN */
82
- .main{flex:1;display:flex;flex-direction:column;overflow:hidden;min-width:0;background:var(--bg);}
83
- .topbar{height:70px;padding:0 24px;display:flex;align-items:center;gap:16px;flex-shrink:0}
84
  .topbar-title{flex:1;font-size:1.05rem;font-weight:600;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
85
 
86
  /* CHAT AREA */
87
- .chat-area{flex:1;overflow-y:auto;padding:20px 0 40px;display:flex;flex-direction:column;scroll-behavior:smooth;}
88
  .welcome{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;
89
- gap:20px;padding:40px 24px;text-align:center;animation:fadeSlideUp 0.6s ease;}
90
  .w-logo{width:85px;height:85px;border-radius:24px;object-fit:cover;box-shadow:0 12px 40px var(--accent-soft)}
91
- .w-title{font-size:2.4rem;font-weight:700;background:linear-gradient(135deg,var(--accent),var(--accent2));-webkit-background-clip:text;-webkit-text-fill-color:transparent}
 
92
  .w-sub{color:var(--text2);font-size:1.05rem}
93
  .chips{display:flex;flex-wrap:wrap;justify-content:center;gap:10px;margin-top:12px;max-width:600px}
94
  .chip{padding:10px 20px;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-pill);
95
- font-size:.9rem;color:var(--text);cursor:pointer;transition:all var(--tr);box-shadow:0 4px 12px rgba(0,0,0,0.05);}
96
- .chip:hover{background:var(--accent-soft);border-color:var(--accent);color:var(--accent);transform:translateY(-2px);}
97
 
98
- @keyframes popIn{from{opacity:0;transform:translateY(15px) scale(0.98);filter:blur(4px);}to{opacity:1;transform:translateY(0) scale(1);filter:blur(0);}}
99
- @keyframes fadeSlideUp{from{opacity:0;transform:translateY(20px);}to{opacity:1;transform:translateY(0);}}
100
-
101
- .msg-row{display:flex;padding:8px 30px;gap:16px;animation:popIn 0.4s cubic-bezier(0.16,1,0.3,1) forwards;}
102
  .msg-row.user{flex-direction:row-reverse}
103
  .msg-av{width:38px;height:38px;border-radius:14px;flex-shrink:0;display:flex;align-items:center;
104
- justify-content:center;font-size:1.1rem;font-weight:700;margin-top:2px;overflow:hidden;box-shadow:0 4px 10px rgba(0,0,0,0.1);}
 
105
  .msg-row.bot .msg-av{background:linear-gradient(135deg,var(--accent),var(--accent2))}
106
  .msg-row.bot .msg-av img{width:100%;height:100%;object-fit:cover}
107
  .msg-row.user .msg-av{background:var(--surface3);color:var(--text)}
108
-
109
- .bubble{max-width:min(700px,80vw);padding:16px 22px;border-radius:var(--radius);font-size:.98rem;line-height:1.75;word-break:break-word;box-shadow:0 4px 15px rgba(0,0,0,0.03);}
 
110
  .msg-row.bot .bubble{background:transparent;border:none;padding:8px 4px;box-shadow:none;border-top-left-radius:var(--radius);}
111
- .msg-row.user .bubble{background:var(--user-bubble);border:none;border-top-right-radius:6px}
112
- [dir="rtl"] .msg-row.user .bubble{border-top-right-radius:var(--radius);border-top-left-radius:6px;}
113
-
114
- .bubble img{max-width:100%;height:auto;border-radius:16px;margin:12px 0;box-shadow:0 8px 25px rgba(0,0,0,0.15);cursor:zoom-in;transition:transform 0.2s;border:1px solid var(--border);}
115
- .bubble img:active{transform:scale(1.02);}
116
-
117
- .gemini-cursor{display:inline-block;width:14px;height:14px;margin-inline-start:6px;background:linear-gradient(135deg,var(--accent),var(--accent2));mask-image:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 0l2.5 9.5L24 12l-9.5 2.5L12 24l-2.5-9.5L0 12l9.5-2.5z"/></svg>');-webkit-mask-image:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 0l2.5 9.5L24 12l-9.5 2.5L12 24l-2.5-9.5L0 12l9.5-2.5z"/></svg>');mask-size:cover;-webkit-mask-size:cover;animation:spinPulse 1s infinite linear;transform-origin:center;vertical-align:middle;}
118
- @keyframes spinPulse{0%{transform:scale(0.8) rotate(0deg);opacity:0.5;}50%{transform:scale(1.2) rotate(90deg);opacity:1;filter:drop-shadow(0 0 5px var(--accent));}100%{transform:scale(0.8) rotate(180deg);opacity:0.5;}}
119
-
120
- .skeleton-img{width:100%;max-width:400px;height:350px;margin:15px auto;border-radius:18px;background:linear-gradient(90deg,var(--surface2) 25%,var(--surface3) 50%,var(--surface2) 75%);background-size:200% 100%;animation:skeletonLoading 1.5s infinite linear;box-shadow:0 8px 25px rgba(0,0,0,0.1);display:flex;align-items:center;justify-content:center;color:var(--text3);font-size:2rem;}
121
- @keyframes skeletonLoading{0%{background-position:200% 0;}100%{background-position:-200% 0;}}
 
 
 
 
 
 
 
 
 
 
 
 
122
 
 
123
  .bubble p{margin-bottom:12px}.bubble p:last-child{margin-bottom:0}
124
  .bubble ul,.bubble ol{padding-inline-start:24px;margin:12px 0}
125
- .bubble li{margin-bottom:6px;}
126
  .bubble .table-wrapper{width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;margin:16px 0;border-radius:var(--radius-sm);border:1px solid var(--border);}
127
  .bubble table{width:100%;border-collapse:collapse;min-width:400px;}
128
  .bubble th,.bubble td{border:1px solid var(--border);padding:10px 14px;text-align:start;white-space:nowrap;}
129
  .bubble th{background:var(--surface3);font-weight:600;}
130
 
 
131
  .code-container{position:relative;margin:16px 0;background:var(--surface3);border-radius:var(--radius-sm);border:1px solid var(--border);overflow:hidden;max-width:100%;}
132
  .code-header{display:flex;justify-content:space-between;align-items:center;background:#12141a;padding:8px 16px;font-size:0.8rem;color:var(--text2);font-family:'JetBrains Mono',monospace;}
133
  .copy-btn{background:var(--surface2);border:none;color:var(--text);cursor:pointer;display:flex;align-items:center;gap:6px;font-size:0.8rem;padding:4px 10px;border-radius:var(--radius-pill);transition:all 0.2s;}
@@ -136,164 +142,126 @@
136
  .code-container pre code{white-space:pre;display:block;}
137
  .bubble code:not(pre code){font-family:'JetBrains Mono',monospace;font-size:.85rem;background:var(--surface3);padding:3px 6px;border-radius:6px;color:var(--accent);}
138
 
 
139
  .katex-display{overflow-x:auto;overflow-y:hidden;padding:8px 0;-webkit-overflow-scrolling:touch;}
140
  .katex{font-size:1.05em;}
141
 
142
- .file-chip{display:inline-flex;align-items:center;gap:6px;background:var(--surface2);border:1px solid var(--border);border-radius:var(--radius-pill);padding:4px 12px;font-size:0.85rem;margin:0 4px 8px 0;color:var(--text);}
143
- .file-chip button{background:none;border:none;color:var(--danger);cursor:pointer;font-size:1rem;padding:0;margin-left:4px;transition:transform 0.2s;}
144
- .file-chip button:hover{transform:scale(1.2);}
145
- .file-prev{display:flex;align-items:center;gap:8px;background:var(--surface3);padding:8px 12px;border-radius:8px;margin-bottom:8px;font-size:0.85rem;}
146
- .mem-badge{font-size:.75rem;color:var(--text3);padding:4px 30px 10px;display:flex;align-items:center;gap:6px;font-weight:600;}
 
 
147
 
148
  /* INPUT */
149
- .input-area{padding:0 24px 30px;display:flex;flex-direction:column;align-items:center;flex-shrink:0;}
150
- .input-wrapper{width:100%;max-width:850px;position:relative;}
151
- .file-chips-container{width:100%;max-width:850px;display:flex;flex-wrap:wrap;margin-bottom:8px;}
152
- .input-box{display:flex;align-items:flex-end;gap:10px;background:var(--surface);border:1px solid var(--border);border-radius:32px;padding:10px 14px;transition:all var(--tr);box-shadow:0 8px 30px rgba(0,0,0,0.15);}
153
- .input-box:focus-within{border-color:var(--accent);box-shadow:0 8px 30px var(--accent-soft),0 0 0 3px var(--accent-soft);}
154
-
155
- .btn-attach{background:none;border:none;color:var(--text3);cursor:pointer;padding:8px;border-radius:50%;transition:all var(--tr);display:flex;align-items:center;justify-content:center;width:38px;height:38px;}
156
- .btn-attach:hover{color:var(--accent);background:var(--surface2);}
157
-
158
- .input-box textarea{flex:1;background:none;border:none;outline:none;color:var(--text);font-family:'Cairo',sans-serif;font-size:.98rem;resize:none;max-height:160px;line-height:1.6;padding:8px 0;}
 
 
 
159
  .input-box textarea::placeholder{color:var(--text3)}
160
-
161
- /* Action buttons in input */
162
- .btn-send,.btn-stop,.btn-voice{width:38px;height:38px;border:none;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all var(--tr);flex-shrink:0;margin-bottom:2px;}
163
- .btn-send{background:linear-gradient(135deg,var(--accent),var(--accent2));color:#fff;}
164
- .btn-send:hover{opacity:.9;transform:scale(1.08) rotate(-10deg);}
165
- [dir="rtl"] .btn-send:hover{transform:scale(1.08) rotate(10deg);}
166
  .btn-send:disabled{opacity:.4;cursor:not-allowed;transform:none;}
167
- .btn-stop{background:var(--surface3);color:var(--text);border:1px solid var(--border);}
168
- .btn-stop:hover{background:var(--danger);color:#fff;transform:scale(1.05);}
169
- .btn-voice{background:var(--surface2);color:var(--text2);}
170
- .btn-voice:hover{background:var(--surface3);color:var(--accent);transform:scale(1.05);}
171
-
172
- /* Recording state */
173
- .btn-voice.recording{background:var(--danger);color:#fff;animation:pulseRecord 1.5s infinite;}
174
- @keyframes pulseRecord{0%{box-shadow:0 0 0 0 rgba(247,95,95,0.4);}70%{box-shadow:0 0 0 12px rgba(247,95,95,0);}100%{box-shadow:0 0 0 0 rgba(247,95,95,0);}}
175
-
176
- /* STT status banner */
177
- .stt-status{display:none;align-items:center;gap:10px;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-pill);padding:8px 18px;font-size:.85rem;color:var(--text2);width:100%;max-width:850px;margin-bottom:8px;box-shadow:0 4px 15px rgba(0,0,0,0.1);}
178
- .stt-status.visible{display:flex;}
179
- .stt-dot{width:10px;height:10px;border-radius:50%;background:var(--danger);animation:pulseRecord 1.5s infinite;flex-shrink:0;}
180
- .stt-text{flex:1;font-style:italic;}
181
- /* TTS playing indicator */
182
- .tts-badge{display:inline-flex;align-items:center;gap:6px;background:linear-gradient(135deg,rgba(79,142,247,0.15),rgba(124,92,247,0.15));border:1px solid var(--border);border-radius:var(--radius-pill);padding:5px 14px;font-size:.8rem;color:var(--accent);margin-top:8px;cursor:pointer;}
183
- .tts-badge:hover{background:var(--accent-soft);}
184
- .tts-wave{display:flex;align-items:center;gap:2px;height:14px;}
185
- .tts-wave span{display:block;width:3px;border-radius:2px;background:var(--accent);animation:wave 1s ease-in-out infinite;}
186
- .tts-wave span:nth-child(1){height:5px;animation-delay:0s;}
187
- .tts-wave span:nth-child(2){height:10px;animation-delay:0.15s;}
188
- .tts-wave span:nth-child(3){height:14px;animation-delay:0.3s;}
189
- .tts-wave span:nth-child(4){height:10px;animation-delay:0.45s;}
190
- .tts-wave span:nth-child(5){height:5px;animation-delay:0.6s;}
191
- @keyframes wave{0%,100%{transform:scaleY(0.5);}50%{transform:scaleY(1);}}
192
 
193
  /* MODAL */
194
- .overlay{position:fixed;inset:0;z-index:999;background:rgba(0,0,0,.6);display:flex;align-items:center;justify-content:center;backdrop-filter:blur(8px);opacity:0;pointer-events:none;transition:opacity var(--tr)}
 
 
195
  .overlay.open{opacity:1;pointer-events:all}
196
- .modal{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:32px;width:min(440px,90vw);box-shadow:0 30px 80px rgba(0,0,0,.5);transform:translateY(20px) scale(0.95);transition:all var(--tr)}
 
 
197
  .overlay.open .modal{transform:translateY(0) scale(1)}
198
  .m-header{display:flex;align-items:center;gap:12px;margin-bottom:24px}
199
  .m-title{font-size:1.3rem;font-weight:700;flex:1}
200
- .m-close{background:var(--surface2);border:none;color:var(--text);cursor:pointer;width:36px;height:36px;border-radius:50%;display:flex;align-items:center;justify-content:center;transition:background var(--tr);}
201
- .m-close:hover{background:var(--danger);color:#fff;}
202
  .s-row{display:flex;align-items:center;justify-content:space-between;padding:16px 0;border-bottom:1px solid var(--border)}
203
  .s-lbl{font-size:.95rem;font-weight:600;color:var(--text)}
204
  .s-desc{font-size:.8rem;color:var(--text3);margin-top:4px}
205
  .toggle{position:relative;width:48px;height:26px}
206
  .toggle input{opacity:0;width:0;height:0}
207
  .tslider{position:absolute;inset:0;background:var(--surface3);border-radius:var(--radius-pill);cursor:pointer;transition:all var(--tr)}
208
- .tslider::before{content:'';position:absolute;width:20px;height:20px;border-radius:50%;background:#fff;top:3px;left:4px;transition:all var(--tr);box-shadow:0 2px 5px rgba(0,0,0,0.2);}
 
209
  .toggle input:checked+.tslider{background:var(--accent)}
210
  .toggle input:checked+.tslider::before{transform:translateX(20px)}
211
- select.s-input{width:auto;padding:8px 16px;background:var(--surface2);border:1px solid var(--border);border-radius:var(--radius-pill);color:var(--text);cursor:pointer;font-family:'Cairo',sans-serif;font-weight:600;}
212
- .btn-save{width:100%;margin-top:24px;padding:14px;background:linear-gradient(135deg,var(--accent),var(--accent2));color:#fff;border:none;border-radius:var(--radius-pill);cursor:pointer;font-family:'Cairo',sans-serif;font-size:1rem;font-weight:700;transition:opacity var(--tr);}
 
 
213
  .btn-save:hover{opacity:.9}
214
- .btn-danger{padding:8px 18px;background:rgba(247,95,95,.12);border:1px solid var(--danger);color:var(--danger);border-radius:var(--radius-pill);cursor:pointer;font-size:.9rem;font-weight:600;font-family:'Cairo',sans-serif;transition:all var(--tr);}
215
- .btn-danger:hover{background:var(--danger);color:#fff;}
 
216
 
 
217
  @media (max-width: 640px) {
218
- .msg-row{padding:6px 12px;gap:10px;}
219
- .bubble{max-width:92vw;padding:12px 14px;font-size:.93rem;}
220
- .msg-row.bot .bubble{padding:6px 2px;}
221
- .topbar{height:56px;padding:0 14px;}
222
- .input-area{padding:0 10px 18px;}
223
- .code-container pre{font-size:0.78rem;padding:12px;}
224
- .bubble th,.bubble td{font-size:0.85rem;padding:8px 10px;}
225
- .w-title{font-size:1.8rem;}
226
- .chips{gap:8px;}
227
- .chip{font-size:.82rem;padding:8px 14px;}
228
- .sidebar{position:fixed;top:0;bottom:0;left:0;z-index:100;height:100vh;box-shadow:4px 0 20px rgba(0,0,0,0.2);transition:transform 0.3s ease,width 0.3s ease;}
229
- [dir="rtl"] .sidebar{left:auto;right:0;}
230
- .sidebar.collapsed{transform:translateX(-100%);width:var(--sidebar-w) !important;min-width:var(--sidebar-w) !important;}
231
- [dir="rtl"] .sidebar.collapsed{transform:translateX(100%);}
232
- .sidebar:not(.collapsed){transform:translateX(0);}
233
  }
234
  </style>
235
  </head>
236
  <body>
237
  <div class="app">
238
- <aside class="sidebar collapsed" id="sidebar">
 
239
  <div class="sidebar-header">
240
  <div class="s-logo">
241
  <img src="https://copilot.microsoft.com/th/id/BCO.f29916dd-b0c1-4089-87cd-43d099a7d1a6.png" alt="Genisi"/>
242
  <span class="s-logo-name">Genisi AI</span>
243
  </div>
244
- <button class="close-sidebar-btn" onclick="toggleSidebar()" title="إخفاء القائمة">
245
- <i data-lucide="x" class="icon"></i>
246
- </button>
247
  </div>
248
- <button class="btn-new" onclick="newChat()" id="i18n-new-chat">
249
- <i data-lucide="sparkles" class="icon-sm"></i>
250
- <span>New Chat</span>
251
- </button>
252
  <div class="s-label" id="i18n-chats-label">CHATS</div>
253
  <div class="chats-list" id="chats-list"></div>
254
  <div class="s-footer">
255
- <button class="btn-icon" onclick="openSettings()" title="Settings">
256
- <i data-lucide="settings" class="icon"></i>
257
- </button>
258
- <button class="btn-icon" id="theme-btn" onclick="toggleTheme()" title="Toggle theme">
259
- <i data-lucide="moon" class="icon" id="theme-icon"></i>
260
- </button>
261
  </div>
262
  </aside>
263
 
264
  <main class="main">
265
  <div class="topbar">
266
- <button class="btn-icon" id="menu-toggle-btn" onclick="toggleSidebar()">
267
- <i data-lucide="menu" class="icon" id="menu-icon"></i>
268
- </button>
269
  <div class="topbar-title" id="topbar-title">Genisi AI</div>
270
  </div>
 
271
  <div class="chat-area" id="chat-area"></div>
 
272
  <div class="input-area">
273
- <div class="stt-status" id="stt-status">
274
- <div class="stt-dot"></div>
275
- <span class="stt-text" id="stt-text">Listening...</span>
276
- <button class="btn-icon" style="width:28px;height:28px;" onclick="stopSpeechInput()" title="Stop">
277
- <i data-lucide="square" class="icon-sm"></i>
278
- </button>
279
- </div>
280
  <div class="file-chips-container" id="file-chips"></div>
281
  <div class="input-wrapper">
282
  <div class="input-box">
283
  <input type="file" id="file-input" multiple hidden onchange="handleFiles(this.files)" />
284
- <button class="btn-attach" onclick="document.getElementById('file-input').click()" title="Attach file">
285
- <i data-lucide="paperclip" class="icon"></i>
286
- </button>
287
  <textarea id="msg-input" rows="1" onkeydown="handleKey(event)" oninput="autoResize(this)" placeholder="Type a message..."></textarea>
288
- <button class="btn-voice" id="voice-btn" onclick="toggleSpeechInput()" title="Voice input">
289
- <i data-lucide="mic" class="icon" id="mic-icon"></i>
290
- </button>
291
- <button class="btn-send" id="send-btn" onclick="sendMessage()" title="Send">
292
- <i data-lucide="send" class="icon"></i>
293
- </button>
294
- <button class="btn-stop" id="stop-btn" onclick="stopGeneration()" style="display:none;" title="Stop generation">
295
- <i data-lucide="square" class="icon"></i>
296
- </button>
297
  </div>
298
  </div>
299
  </div>
@@ -305,9 +273,7 @@
305
  <div class="modal">
306
  <div class="m-header">
307
  <div class="m-title" id="i18n-settings-title">Settings</div>
308
- <button class="m-close" onclick="closeSettings()">
309
- <i data-lucide="x" class="icon-sm"></i>
310
- </button>
311
  </div>
312
  <div class="s-row">
313
  <div class="s-lbl" id="i18n-lang-lbl">Language</div>
@@ -325,36 +291,24 @@
325
  <span class="tslider"></span>
326
  </label>
327
  </div>
328
- <div class="s-row">
329
- <div>
330
- <div class="s-lbl" id="i18n-tts-lbl">Auto-speak Responses</div>
331
- <div class="s-desc" id="i18n-tts-desc">Read bot responses aloud automatically</div>
332
- </div>
333
- <label class="toggle">
334
- <input type="checkbox" id="tts-toggle" onchange="saveTtsSetting()"/>
335
- <span class="tslider"></span>
336
- </label>
337
- </div>
338
  <div class="s-row">
339
  <div>
340
  <div class="s-lbl" id="i18n-del-lbl">Delete All Chats</div>
341
  <div class="s-desc" id="i18n-del-desc">This action cannot be undone</div>
342
  </div>
343
- <button class="btn-danger" onclick="clearAllChats()" id="i18n-del-btn">
344
- <i data-lucide="trash-2" class="icon-sm" style="display:inline;vertical-align:middle;margin-right:4px;"></i>Delete
345
- </button>
346
  </div>
347
- <button class="btn-save" onclick="closeSettings()" id="i18n-save-btn">Close</button>
348
  </div>
349
  </div>
350
 
351
  <script>
352
- // ════════ i18n ════════
353
  const i18n = {
354
- en: { newChat:"✦ New Chat", chatsLabel:"CHATS", settings:"Settings", lang:"Language", dark:"Dark Mode", ttsLabel:"Auto-speak Responses", ttsDesc:"Read bot responses aloud automatically", del:"Delete All Chats", delDesc:"This action cannot be undone", delBtn:"Delete", saveBtn:"Close", placeholder:"Type your message... (Enter to send)", welcomeTitle:"Welcome to Genisi", welcomeSub:"Your smart assistant by AnesNT — Batna 🇩🇿", c1:"What is AI?", c2:"Write Python code", c3:"Summarize a topic", c4:"Design an image in space 🚀", errConnect:"Connection Error", memBadge:"Memory: {c}/{t} messages", stopMsg:"[Generation stopped by user]", vListening:"Listening... speak now", vProcessing:"Processing your voice...", vPlay:"Speaking response...", vEnd:"Done speaking", vNotSupported:"Voice input not supported in this browser" },
355
- ar: { newChat:"✦ محادثة جديدة", chatsLabel:"المحادثات", settings:"الإعدادات", lang:"اللغة", dark:"الوضع الليلي", ttsLabel:"قراءة الردود صوتياً", ttsDesc:"اقرأ ردود الذكاء الاصطناعي بصوت تلقائي", del:"حذف جميع المحادثات", delDesc:"لا يمكن التراجع عن هذا الإجراء", delBtn:"حذف", saveBtn:"إغلاق", placeholder:"اكتب رسالتك... (Enter للإرسال)", welcomeTitle:"مرحبًا في Genisi", welcomeSub:"أنا Genisi، مساعدك الذكي من AnesNT — باتنة 🇩🇿", c1:"ما هو الذكاء الاصطناعي؟", c2:"اكتب كود بايثون", c3:"لخص لي موضوعاً", c4:"صمم صورة بالفضاء 🚀", errConnect:"خطأ في الاتصال", memBadge:"الذاكرة: {c}/{t} رسائل", stopMsg:"[تم إيقاف التوليد]", vListening:"استمع... تحدث الآن", vProcessing:"جاري معالجة صوتك...", vPlay:"يقرأ الرد الآن...", vEnd:"انتهت القراءة الصوتية", vNotSupported:"التعرف على الكلام غير مدعوم في هذا المتصفح" },
356
- fr: { newChat:"✦ Nouveau", chatsLabel:"DISCUSSIONS", settings:"Paramètres", lang:"Langue", dark:"Mode Sombre", ttsLabel:"Lire les réponses", ttsDesc:"Lire les réponses du bot à voix haute", del:"Supprimer Tout", delDesc:"Action irréversible", delBtn:"Supprimer", saveBtn:"Fermer", placeholder:"Écrivez votre message...", welcomeTitle:"Bienvenue sur Genisi", welcomeSub:"Votre assistant par AnesNT — Batna 🇩🇿", c1:"Qu'est-ce que l'IA?", c2:"Code Python", c3:"Résumer un sujet", c4:"Créer une image 🚀", errConnect:"Erreur de connexion", memBadge:"Mémoire: {c}/{t} msgs", stopMsg:"[Génération arrêtée]", vListening:"Écoute... parlez maintenant", vProcessing:"Traitement de votre voix...", vPlay:"Lecture en cours...", vEnd:"Fin de la lecture", vNotSupported:"Reconnaissance vocale non supportée" },
357
- es: { newChat:"✦ Nuevo Chat", chatsLabel:"CHATS", settings:"Ajustes", lang:"Idioma", dark:"Modo Oscuro", ttsLabel:"Leer respuestas", ttsDesc:"Leer respuestas del bot en voz alta", del:"Borrar Todo", delDesc:"Acción irreversible", delBtn:"Borrar", saveBtn:"Cerrar", placeholder:"Escribe tu mensaje...", welcomeTitle:"Bienvenido a Genisi", welcomeSub:"Tu asistente por AnesNT — Batna 🇩🇿", c1:"¿Qué es la IA?", c2:"Código Python", c3:"Resumir", c4:"Diseñar imagen 🚀", errConnect:"Error de conexión", memBadge:"Memoria: {c}/{t} msgs", stopMsg:"[Generación detenida]", vListening:"Escuchando... habla ahora", vProcessing:"Procesando tu voz...", vPlay:"Leyendo respuesta...", vEnd:"Fin de la lectura", vNotSupported:"Reconocimiento de voz no soportado" }
358
  };
359
 
360
  let currentLang = localStorage.getItem('genisi_lang') || 'en';
@@ -363,441 +317,352 @@ function applyI18n() {
363
  const t = i18n[currentLang];
364
  document.documentElement.dir = currentLang === 'ar' ? 'rtl' : 'ltr';
365
  document.documentElement.lang = currentLang;
366
- const newChatBtn = document.getElementById('i18n-new-chat');
367
- newChatBtn.querySelector('span').textContent = t.newChat;
368
  document.getElementById('i18n-chats-label').textContent = t.chatsLabel;
369
  document.getElementById('i18n-settings-title').textContent = t.settings;
370
  document.getElementById('i18n-lang-lbl').textContent = t.lang;
371
  document.getElementById('i18n-dark-lbl').textContent = t.dark;
372
- document.getElementById('i18n-tts-lbl').textContent = t.ttsLabel;
373
- document.getElementById('i18n-tts-desc').textContent = t.ttsDesc;
374
  document.getElementById('i18n-del-lbl').textContent = t.del;
375
  document.getElementById('i18n-del-desc').textContent = t.delDesc;
 
376
  document.getElementById('i18n-save-btn').textContent = t.saveBtn;
377
  document.getElementById('msg-input').placeholder = t.placeholder;
 
378
  if(!activeChatId) renderWelcome();
379
  renderChatList();
380
  }
381
- function changeLanguage(val){ currentLang=val; localStorage.setItem('genisi_lang',val); applyI18n(); }
 
 
 
382
 
383
- // ════════ STATE ════════
384
- const genId = () => Date.now().toString(36)+Math.random().toString(36).substring(2,6);
385
  let userId = localStorage.getItem('genisi_user_id');
386
- if(!userId){ userId='user_'+genId(); localStorage.setItem('genisi_user_id',userId); }
 
 
 
387
 
388
- let chats = JSON.parse(localStorage.getItem('genisi_chats')||'[]');
389
  let activeChatId = null;
390
  let isGenerating = false;
391
- let pendingFiles = [];
392
- let currentAbortController = null;
393
- let autoTTS = localStorage.getItem('genisi_tts') !== 'false';
394
- let currentUtterance = null;
395
-
396
- // ════════ SPEECH RECOGNITION (STT) ════════
397
- let recognition = null;
398
- let isListening = false;
399
-
400
- function initSpeechRecognition() {
401
- const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
402
- if (!SpeechRecognition) return null;
403
- const rec = new SpeechRecognition();
404
- rec.continuous = false;
405
- rec.interimResults = true;
406
- // Set language based on current UI language
407
- const langMap = { en: 'en-US', ar: 'ar-DZ', fr: 'fr-FR', es: 'es-ES' };
408
- rec.lang = langMap[currentLang] || 'en-US';
409
-
410
- rec.onstart = () => {
411
- isListening = true;
412
- document.getElementById('stt-status').classList.add('visible');
413
- document.getElementById('stt-text').textContent = i18n[currentLang].vListening;
414
- const voiceBtn = document.getElementById('voice-btn');
415
- voiceBtn.classList.add('recording');
416
- // swap mic icon to mic-off
417
- lucide.createIcons({ icons: { 'mic-off': lucide.icons['mic-off'] }, attrs: { class: 'icon' } });
418
- const micIcon = document.getElementById('mic-icon');
419
- micIcon.setAttribute('data-lucide', 'mic-off');
420
- lucide.createIcons({ nameAttr: 'data-lucide', attrs: { class: 'icon' } });
421
- };
422
-
423
- rec.onresult = (event) => {
424
- let interim = '';
425
- let final = '';
426
- for (let i = event.resultIndex; i < event.results.length; i++) {
427
- if (event.results[i].isFinal) final += event.results[i][0].transcript;
428
- else interim += event.results[i][0].transcript;
429
- }
430
- const input = document.getElementById('msg-input');
431
- input.value = (final || interim);
432
- autoResize(input);
433
- if (final) document.getElementById('stt-text').textContent = final;
434
- else document.getElementById('stt-text').textContent = interim || i18n[currentLang].vListening;
435
- };
436
-
437
- rec.onend = () => {
438
- isListening = false;
439
- document.getElementById('stt-status').classList.remove('visible');
440
- const voiceBtn = document.getElementById('voice-btn');
441
- voiceBtn.classList.remove('recording');
442
- const micIcon = document.getElementById('mic-icon');
443
- micIcon.setAttribute('data-lucide', 'mic');
444
- lucide.createIcons({ nameAttr: 'data-lucide', attrs: { class: 'icon' } });
445
- // Auto-send if there's text
446
- const input = document.getElementById('msg-input');
447
- if (input.value.trim()) sendMessage();
448
- };
449
-
450
- rec.onerror = (event) => {
451
- console.error('STT error:', event.error);
452
- isListening = false;
453
- document.getElementById('stt-status').classList.remove('visible');
454
- const voiceBtn = document.getElementById('voice-btn');
455
- voiceBtn.classList.remove('recording');
456
- const micIcon = document.getElementById('mic-icon');
457
- micIcon.setAttribute('data-lucide', 'mic');
458
- lucide.createIcons({ nameAttr: 'data-lucide', attrs: { class: 'icon' } });
459
- };
460
-
461
- return rec;
462
- }
463
-
464
- function toggleSpeechInput() {
465
- const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
466
- if (!SpeechRecognition) {
467
- alert(i18n[currentLang].vNotSupported);
468
- return;
469
- }
470
- if (isListening) {
471
- stopSpeechInput();
472
- return;
473
- }
474
- recognition = initSpeechRecognition();
475
- if (recognition) recognition.start();
476
- }
477
-
478
- function stopSpeechInput() {
479
- if (recognition && isListening) recognition.stop();
480
- }
481
 
482
- // ════════ TTS (Text-to-Speech) ════════
483
- function saveTtsSetting() {
484
- autoTTS = document.getElementById('tts-toggle').checked;
485
- localStorage.setItem('genisi_tts', autoTTS);
486
- }
487
-
488
- function speakText(text) {
489
- if (!window.speechSynthesis) return;
490
- // Cancel any ongoing speech
491
- window.speechSynthesis.cancel();
492
- // Strip markdown symbols for cleaner speech
493
- const clean = text
494
- .replace(/```[\s\S]*?```/g, 'code block.')
495
- .replace(/`([^`]+)`/g, '$1')
496
- .replace(/\*\*([^*]+)\*\*/g, '$1')
497
- .replace(/\*([^*]+)\*/g, '$1')
498
- .replace(/#{1,6}\s/g, '')
499
- .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
500
- .replace(/!\[([^\]]+)\]\([^)]+\)/g, 'image.')
501
- .replace(/\n+/g, '. ')
502
- .trim();
503
-
504
- const utterance = new SpeechSynthesisUtterance(clean);
505
- const langMap = { en: 'en-US', ar: 'ar-SA', fr: 'fr-FR', es: 'es-ES' };
506
- utterance.lang = langMap[currentLang] || 'en-US';
507
- utterance.rate = 1.0;
508
- utterance.pitch = 1.0;
509
- currentUtterance = utterance;
510
- window.speechSynthesis.speak(utterance);
511
- return utterance;
512
- }
513
-
514
- function stopTTS() {
515
- if (window.speechSynthesis) window.speechSynthesis.cancel();
516
- }
517
-
518
- // ════════ MARKDOWN ════════
519
  const renderer = new marked.Renderer();
520
  renderer.code = function(token) {
521
  const codeText = typeof token === 'string' ? token : token.text || '';
522
  const lang = typeof token === 'string' ? arguments[1] : token.lang || 'text';
523
- const escapedCode = String(codeText).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');
524
- return `<div class="code-container"><div class="code-header"><span>${lang}</span><button class="copy-btn" onclick="copyCode(this,'${encodeURIComponent(codeText)}')"><i data-lucide="copy" class="icon-sm"></i> Copy</button></div><pre><code class="language-${lang}">${escapedCode}</code></pre></div>`;
 
 
 
 
 
 
 
525
  };
526
  renderer.table = function(header, body) {
527
- if(typeof header==='object'&&header!==null&&'header' in header){
528
- const token=header;
529
- const hdr=token.header.map(cell=>`<th>${cell.tokens.map(t=>t.raw||'').join('')}</th>`).join('');
530
- const rows=(token.rows||[]).map(row=>`<tr>${row.map(cell=>`<td>${cell.tokens.map(t=>t.raw||'').join('')}</td>`).join('')}</tr>`).join('');
531
  return `<div class="table-wrapper"><table><thead><tr>${hdr}</tr></thead><tbody>${rows}</tbody></table></div>`;
532
  }
533
  return `<div class="table-wrapper"><table><thead>${header}</thead><tbody>${body}</tbody></table></div>`;
534
  };
535
- renderer.html = function(html){ return html; };
536
- marked.setOptions({ renderer, breaks:true, gfm:true });
537
-
538
- function safeMarked(text){
539
- if(!text) return '';
540
- return DOMPurify.sanitize(marked.parse(text),{USE_PROFILES:{html:true}});
541
- }
542
- function renderKaTeX(el){
543
- if(window.renderMathInElement) renderMathInElement(el,{delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false},{left:'\\(',right:'\\)',display:false},{left:'\\[',right:'\\]',display:true}],throwOnError:false});
 
 
 
 
 
 
544
  }
545
- function copyCode(btn, encodedCode){
546
- navigator.clipboard.writeText(decodeURIComponent(encodedCode)).then(()=>{
547
- const orig = btn.innerHTML;
548
- btn.innerHTML = '<i data-lucide="check" class="icon-sm" style="color:var(--accent)"></i> Copied!';
549
- lucide.createIcons({ nameAttr:'data-lucide', attrs:{class:'icon-sm'} });
550
- setTimeout(()=>{btn.innerHTML=orig; lucide.createIcons({nameAttr:'data-lucide',attrs:{class:'icon-sm'}});},2000);
551
  });
552
  }
553
- function removeGeminiCursor(){ document.querySelectorAll('.gemini-cursor').forEach(el=>el.remove()); }
554
 
555
- // ════════ UTILS ════════
556
- const saveChats = () => localStorage.setItem('genisi_chats',JSON.stringify(chats));
557
- const getChat = (id) => chats.find(c=>c.id===(id??activeChatId));
558
  function esc(t){ return String(t).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
559
- function scrollToBottom(){ const area=document.getElementById('chat-area'); area.scrollTop=area.scrollHeight; }
560
-
561
- async function handleFiles(files){
562
- for(let f of files){
563
- const isText = f.type.startsWith('text/')||f.name.match(/\.(py|js|html|css|json|md|txt|csv|cpp|java)$/i);
564
- const reader = new FileReader();
565
- reader.onload = (e)=>{
566
- let data=e.target.result;
567
- if(!isText) data=data.split(',')[1];
568
- pendingFiles.push({name:f.name,type:f.type||'application/octet-stream',data,isText});
569
- renderFileChips();
570
- };
571
- if(isText) reader.readAsText(f); else reader.readAsDataURL(f);
572
- }
573
- document.getElementById('file-input').value='';
574
  }
575
- function renderFileChips(){
576
- const c=document.getElementById('file-chips'); c.innerHTML='';
577
- pendingFiles.forEach((f,idx)=>{
578
- const icon = f.isText ? 'file-text' : 'image';
579
- c.innerHTML += `<div class="file-chip"><i data-lucide="${icon}" class="icon-sm"></i>${f.name}<button onclick="removeFile(${idx})"><i data-lucide="x" class="icon-sm"></i></button></div>`;
580
- });
581
- lucide.createIcons({ nameAttr:'data-lucide', attrs:{class:'icon-sm'} });
 
 
 
 
 
 
 
 
582
  }
583
- function removeFile(idx){ pendingFiles.splice(idx,1); renderFileChips(); }
584
 
585
- // ════════ THEME ════════
 
 
 
 
 
 
 
 
 
 
586
  function applyTheme(t){
587
  document.documentElement.setAttribute('data-theme',t);
588
  localStorage.setItem('genisi_theme',t);
 
589
  document.getElementById('dark-toggle').checked = t==='dark';
590
- const themeIcon = document.getElementById('theme-icon');
591
- themeIcon.setAttribute('data-lucide', t==='dark'?'moon':'sun');
592
- lucide.createIcons({ nameAttr:'data-lucide', attrs:{class:'icon'} });
593
  }
594
  function toggleTheme(){ applyTheme(document.documentElement.getAttribute('data-theme')==='dark'?'light':'dark'); }
595
  function applyThemeToggle(){ applyTheme(document.getElementById('dark-toggle').checked?'dark':'light'); }
596
 
597
- // ════════ SIDEBAR ════════
598
- function updateMenuIcon(){
599
- const sidebar=document.getElementById('sidebar');
600
- const menuIcon=document.getElementById('menu-icon');
601
- menuIcon.setAttribute('data-lucide', sidebar.classList.contains('collapsed')?'menu':'x');
602
- lucide.createIcons({ nameAttr:'data-lucide', attrs:{class:'icon'} });
603
- }
604
- function toggleSidebar(){ document.getElementById('sidebar').classList.toggle('collapsed'); updateMenuIcon(); }
605
- function initSidebarState(){
606
- const sidebar=document.getElementById('sidebar');
607
- if(window.innerWidth<=640) sidebar.classList.add('collapsed');
608
- else sidebar.classList.remove('collapsed');
609
- updateMenuIcon();
610
- }
611
- window.addEventListener('resize',()=>{
612
- const sidebar=document.getElementById('sidebar');
613
- if(window.innerWidth>640) sidebar.classList.remove('collapsed');
614
- else sidebar.classList.add('collapsed');
615
- updateMenuIcon();
616
- });
617
-
618
- // ════════ SETTINGS ════════
619
- function openSettings(){ document.getElementById('settings-modal').classList.add('open'); document.getElementById('lang-select').value=currentLang; document.getElementById('tts-toggle').checked=autoTTS; }
620
  function closeSettings(){ document.getElementById('settings-modal').classList.remove('open'); }
621
 
622
- // ════════ CHAT LIST ════════
623
  function renderChatList(){
624
- const list=document.getElementById('chats-list'); list.innerHTML='';
 
625
  [...chats].reverse().forEach(chat=>{
626
  const div=document.createElement('div');
627
  div.className='chat-item'+(chat.id===activeChatId?' active':'');
628
- div.innerHTML=`<i data-lucide="message-circle" class="icon-sm"></i><span class="ct">${esc(chat.title||'Chat')}</span><button class="del-btn" onclick="deleteChat('${chat.id}',event)"><i data-lucide="x" class="icon-sm"></i></button>`;
629
- div.onclick = ()=>loadChat(chat.id);
 
630
  list.appendChild(div);
631
  });
632
- lucide.createIcons({ nameAttr:'data-lucide', attrs:{class:'icon-sm'} });
633
  }
634
 
635
  function renderWelcome(){
636
- const t=i18n[currentLang];
637
  document.getElementById('topbar-title').textContent='Genisi AI';
638
- document.getElementById('chat-area').innerHTML=`<div class="welcome" id="welcome"><img src="https://copilot.microsoft.com/th/id/BCO.f29916dd-b0c1-4089-87cd-43d099a7d1a6.png" class="w-logo" alt="Genisi"/><div class="w-title">${t.welcomeTitle}</div><div class="w-sub">${t.welcomeSub}</div><div class="chips"><div class="chip" onclick="quickSend('${t.c1}')">${t.c1}</div><div class="chip" onclick="quickSend('${t.c2}')">${t.c2}</div><div class="chip" onclick="quickSend('${t.c3}')">${t.c3}</div><div class="chip" onclick="quickSend('${t.c4}')">${t.c4}</div></div></div>`;
 
 
 
 
 
 
 
 
 
 
 
639
  }
640
 
641
  function newChat(){ activeChatId=null; pendingFiles=[]; renderFileChips(); document.getElementById('msg-input').value=''; renderWelcome(); renderChatList(); }
642
 
643
  function loadChat(id){
644
- activeChatId=id; const chat=getChat(id); if(!chat) return;
 
645
  document.getElementById('topbar-title').textContent=chat.title;
646
- const area=document.getElementById('chat-area'); area.innerHTML='';
647
- chat.history.forEach(entry=>{ appendBubble('user',entry.user,false,entry.uiFiles); appendBubble('bot',entry.bot,true); });
648
- if(chat.history.length) appendMemBadge(chat.history.length,chat.history.length);
649
- scrollToBottom(); renderChatList();
 
 
 
 
 
 
 
 
 
 
650
  }
651
- function deleteChat(id,e){ e.stopPropagation(); chats=chats.filter(c=>c.id!==id); saveChats(); if(activeChatId===id) newChat(); else renderChatList(); }
652
  function clearAllChats(){ chats=[]; saveChats(); newChat(); closeSettings(); }
653
- function stopGeneration(){ if(currentAbortController) currentAbortController.abort(); }
 
 
 
 
 
 
 
 
654
  function quickSend(text){ document.getElementById('msg-input').value=text; sendMessage(); }
655
  function handleKey(e){ if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();sendMessage();} }
656
  function autoResize(el){ el.style.height='auto'; el.style.height=Math.min(el.scrollHeight,160)+'px'; }
657
 
658
- // ════════ SEND MESSAGE ════════
659
  async function sendMessage(){
660
- if(isGenerating||isListening) return;
661
- const input=document.getElementById('msg-input'); let text=input.value.trim();
662
- if(!text&&pendingFiles.length===0) return;
663
- const welcome=document.getElementById('welcome'); if(welcome) welcome.remove();
 
 
 
 
 
664
  input.value=''; input.style.height='auto';
665
- stopTTS();
666
 
667
  if(!activeChatId){
668
- activeChatId=genId();
669
- const chatTitle=text?text.slice(0,35)+'...':(pendingFiles[0]?pendingFiles[0].name:'Chat');
670
- chats.push({id:activeChatId,title:chatTitle,history:[]});
671
  document.getElementById('topbar-title').textContent=chatTitle;
672
  }
673
- const chat=getChat();
674
- const uiFiles=pendingFiles.map(f=>f.name); const binaryFiles=[];
675
- pendingFiles.forEach(f=>{
676
- if(f.isText){ text+=`\n\n\`\`\`${f.name.split('.').pop()}\n// File: ${f.name}\n${f.data}\n\`\`\``; }
677
- else{ binaryFiles.push({name:f.name,mime_type:f.type,data:f.data}); }
 
 
 
678
  });
679
- appendBubble('user',text,false,uiFiles);
680
- pendingFiles=[]; renderFileChips();
681
 
682
- isGenerating=true;
683
- document.getElementById('send-btn').style.display='none';
684
- document.getElementById('stop-btn').style.display='flex';
685
- const botContentDiv=appendBubble('bot','',true);
 
 
 
686
 
687
- const imageTriggers=["ارسم","صمم","تخيل","صورة ل","draw","generate","imagine","create an image","تصميم"];
688
- const isImageRequest=imageTriggers.some(trigger=>text.toLowerCase().startsWith(trigger));
689
- currentAbortController=new AbortController();
690
- let fullBotResponse="";
 
 
 
 
691
 
692
  try {
693
- const response=await fetch('/chat',{
694
- method:'POST',headers:{'Content-Type':'application/json'},
695
- body:JSON.stringify({user_id:userId,message:text,history:chat.history,files:binaryFiles}),
696
- signal:currentAbortController.signal
 
697
  });
 
698
  if(!response.ok) throw new Error("Server Error");
699
- const reader=response.body.getReader();
700
- const decoder=new TextDecoder("utf-8");
701
- while(true){
702
- const{value,done}=await reader.read();
 
 
703
  if(done) break;
704
- fullBotResponse+=decoder.decode(value,{stream:true});
705
- let safeHtml=safeMarked(fullBotResponse);
706
- botContentDiv.innerHTML=safeHtml;
707
- if(isImageRequest&&!botContentDiv.querySelector('img')) botContentDiv.innerHTML+='<div class="skeleton-img">🎨 جاري التخيل...</div>';
708
- else botContentDiv.innerHTML+='<span class="gemini-cursor"></span>';
709
- renderKaTeX(botContentDiv); scrollToBottom();
 
 
 
 
 
 
 
 
 
 
710
  }
711
- botContentDiv.innerHTML=safeMarked(fullBotResponse);
712
- renderKaTeX(botContentDiv);
713
- // Add TTS play button
714
- appendTTSBadge(botContentDiv, fullBotResponse);
715
- // Auto TTS
716
- if(autoTTS) speakText(fullBotResponse);
717
- chat.history.push({user:text,bot:fullBotResponse,uiFiles});
718
- saveChats(); appendMemBadge(chat.history.length,chat.history.length);
719
- } catch(err){
720
- removeGeminiCursor();
721
- if(err.name==='AbortError'){
722
- botContentDiv.innerHTML=safeMarked(fullBotResponse)+`<br><br><span style="color:var(--text3);font-style:italic;">${i18n[currentLang].stopMsg}</span>`;
723
- renderKaTeX(botContentDiv);
724
- chat.history.push({user:text,bot:fullBotResponse+"\n\n*"+i18n[currentLang].stopMsg+"*",uiFiles});
725
- saveChats();
726
- } else { botContentDiv.innerHTML=`<span style="color:var(--danger)">⚠️ ${i18n[currentLang].errConnect}: ${err.message}</span>`; }
727
- } finally {
728
- removeGeminiCursor(); isGenerating=false;
729
- document.getElementById('send-btn').style.display='flex';
730
- document.getElementById('stop-btn').style.display='none';
731
- lucide.createIcons({ nameAttr:'data-lucide', attrs:{class:'icon'} });
732
- renderChatList(); scrollToBottom();
733
- }
734
- }
735
 
736
- // ════════ TTS BADGE ════════
737
- function appendTTSBadge(container, text) {
738
- if (!window.speechSynthesis) return;
739
- const badge = document.createElement('div');
740
- badge.className = 'tts-badge';
741
- badge.title = 'Click to play / stop';
742
- badge.innerHTML = `<div class="tts-wave"><span></span><span></span><span></span><span></span><span></span></div><span id="tts-label-${Date.now()}">Play</span>`;
743
- let playing = false;
744
- badge.onclick = () => {
745
- if (playing) {
746
- stopTTS(); playing = false;
747
- badge.querySelector('span:last-child').textContent = 'Play';
748
  } else {
749
- const utt = speakText(text); playing = true;
750
- badge.querySelector('span:last-child').textContent = i18n[currentLang].vPlay;
751
- if (utt) utt.onend = () => { playing=false; badge.querySelector('span:last-child').textContent='Play'; };
752
  }
753
- };
754
- container.appendChild(badge);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
755
  }
756
 
757
- // ════════ BUBBLES ════════
758
- function appendBubble(role,text,isMarkdown,filesList=[]){
759
  const area=document.getElementById('chat-area');
760
  const row=document.createElement('div'); row.className=`msg-row ${role}`;
761
  const av=document.createElement('div'); av.className='msg-av';
762
- if(role==='bot') av.innerHTML=`<img src="https://copilot.microsoft.com/th/id/BCO.f29916dd-b0c1-4089-87cd-43d099a7d1a6.png" alt="G"/>`;
763
- else av.innerHTML=`<i data-lucide="user" class="icon"></i>`;
 
 
764
  const bub=document.createElement('div'); bub.className='bubble';
765
- if(filesList&&filesList.length>0) filesList.forEach(name=>{ bub.innerHTML+=`<div class="file-prev"><i data-lucide="paperclip" class="icon-sm"></i>${esc(name)}</div>`; });
766
- const contentDiv=document.createElement('div');
767
- contentDiv.dir='auto'; contentDiv.style.unicodeBidi='plaintext';
768
- if(text){
769
- contentDiv.innerHTML=isMarkdown?safeMarked(text):esc(text).replace(/\n/g,'<br/>');
770
- if(isMarkdown) renderKaTeX(contentDiv);
771
  }
772
- bub.appendChild(contentDiv); row.appendChild(av); row.appendChild(bub); area.appendChild(row);
773
- lucide.createIcons({ nameAttr:'data-lucide', attrs:{class:'icon'} });
774
- lucide.createIcons({ nameAttr:'data-lucide', attrs:{class:'icon-sm'} });
775
- if(role==='bot'){
776
- const images=bub.querySelectorAll('img');
777
- if(images.length) Promise.all(Array.from(images).map(img=>img.complete?Promise.resolve():new Promise(r=>img.onload=r))).then(()=>scrollToBottom());
778
- else scrollToBottom();
779
- } else scrollToBottom();
780
- return contentDiv;
781
- }
782
 
783
- function appendMemBadge(count,total){
784
- const area=document.getElementById('chat-area'); const b=document.createElement('div'); b.className='mem-badge';
785
- b.innerHTML=`<i data-lucide="brain" class="icon-sm"></i>${i18n[currentLang].memBadge.replace('{c}',count).replace('{t}',total)}`;
786
- area.appendChild(b);
787
- lucide.createIcons({ nameAttr:'data-lucide', attrs:{class:'icon-sm'} });
 
 
 
 
 
 
 
788
  scrollToBottom();
 
 
 
 
 
 
 
 
 
789
  }
790
 
791
  // ════════ INIT ════════
792
- window.addEventListener('DOMContentLoaded', ()=>{
793
- applyTheme(localStorage.getItem('genisi_theme')||'dark');
794
- document.getElementById('lang-select').value=currentLang;
795
- document.getElementById('tts-toggle').checked=autoTTS;
796
- applyI18n();
797
- initSidebarState();
798
- lucide.createIcons({ nameAttr:'data-lucide', attrs:{class:'icon'} });
799
- lucide.createIcons({ nameAttr:'data-lucide', attrs:{class:'icon-sm'} });
800
- });
801
  </script>
802
  </body>
803
- </html>
 
2
  <html lang="en" dir="ltr">
3
  <head>
4
  <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Genisi AI</title>
7
  <link href="https://fonts.googleapis.com/css2?family=Cairo:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"/>
8
  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
 
9
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/katex.min.css"/>
10
  <script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/katex.min.js"></script>
11
  <script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/contrib/auto-render.min.js"></script>
 
 
12
  <style>
13
  :root {
14
  --bg: #0d0f14; --surface: #161920; --surface2: #1e2230; --surface3: #252a3a;
 
16
  --accent: #4f8ef7; --accent2: #7c5cf7; --accent-soft: rgba(79,142,247,0.12);
17
  --text: #e8eaf2; --text2: #9aa3be; --text3: #5c6480;
18
  --user-bubble: #1a2340; --bot-bubble: #161920;
19
+ --danger: #f75f5f;
20
  --radius: 28px; --radius-sm: 18px; --radius-pill: 50px;
21
  --sidebar-w: 280px; --tr: 0.3s cubic-bezier(.4,0,.2,1);
22
  }
 
27
  --user-bubble: #dde6ff; --bot-bubble: #fff; --accent-soft: rgba(79,142,247,0.1);
28
  }
29
  *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
30
+ html,body{height:100%;font-family:'Cairo',sans-serif;background:var(--bg);color:var(--text); scroll-behavior: smooth;}
31
  ::-webkit-scrollbar{width:6px}::-webkit-scrollbar-track{background:transparent}
32
  ::-webkit-scrollbar-thumb{background:var(--border2);border-radius:var(--radius-pill)}
33
 
 
 
 
 
 
34
  .app{display:flex;height:100vh;overflow:hidden}
35
 
36
  /* SIDEBAR */
37
  .sidebar{width:var(--sidebar-w);min-width:var(--sidebar-w);background:var(--surface);
38
  border-right:1px solid var(--border);display:flex;flex-direction:column;
39
  transition:width var(--tr),min-width var(--tr);overflow:hidden;
40
+ border-top-right-radius: var(--radius); border-bottom-right-radius: var(--radius);}
41
+ [dir="rtl"] .sidebar { border-right:none; border-left:1px solid var(--border); border-radius: 0 var(--radius) var(--radius) 0; }
42
  .sidebar.collapsed{width:0;min-width:0;border:none}
43
  .sidebar-header{padding:24px 20px 16px;display:flex;align-items:center;gap:12px;}
44
  .s-logo{display:flex;align-items:center;gap:12px;flex:1}
45
+ .s-logo img{width:36px;height:36px;border-radius:12px;object-fit:cover; box-shadow: 0 4px 12px var(--accent-soft);}
46
  .s-logo-name{font-size:1.2rem;font-weight:700;background:linear-gradient(135deg,var(--accent),var(--accent2));-webkit-background-clip:text;-webkit-text-fill-color:transparent}
 
 
 
 
47
  .btn-new{margin:12px 16px;padding:12px 16px;background:linear-gradient(135deg,var(--accent),var(--accent2));
48
  color:#fff;border:none;border-radius:var(--radius-pill);cursor:pointer;font-family:'Cairo',sans-serif;
49
+ font-size:.95rem;font-weight:600;display:flex;align-items:center;gap:8px; justify-content:center;
50
+ transition:all var(--tr); box-shadow: 0 4px 15px var(--accent-soft);}
51
  .btn-new:hover{opacity:.9;transform:translateY(-2px)}
52
  .s-label{padding:10px 20px;font-size:.75rem;font-weight:700;color:var(--text3);letter-spacing:.08em;text-transform:uppercase}
53
  .chats-list{flex:1;overflow-y:auto;padding:4px 12px;display:flex;flex-direction:column;gap:6px}
54
  .chat-item{padding:10px 16px;border-radius:var(--radius-sm);cursor:pointer;font-size:.9rem;color:var(--text2);
55
+ display:flex;align-items:center;gap:10px;transition:all var(--tr);
56
+ white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
57
+ .chat-item:hover{background:var(--surface2);color:var(--text); transform: translateX(4px);}[dir="rtl"] .chat-item:hover {transform: translateX(-4px);}
58
  .chat-item.active{background:var(--accent-soft);color:var(--accent);font-weight:600;}
59
  .chat-item .ct{flex:1;overflow:hidden;text-overflow:ellipsis}
60
+ .del-btn{opacity:0;background:none;border:none;color:var(--danger);cursor:pointer;font-size:.9rem;
61
+ padding:4px;border-radius:50%;transition:all var(--tr); display:flex; align-items:center; justify-content:center;}
62
  .chat-item:hover .del-btn{opacity:1}
63
+ .del-btn:hover{background:rgba(247,95,95,.15); transform:scale(1.1);}
64
+ .s-footer{padding:16px;display:flex;align-items:center;gap:12px; justify-content: center;}
65
+ .btn-icon{width:42px;height:42px;background:var(--surface2);border:none;
66
+ border-radius:var(--radius-pill);color:var(--text2);cursor:pointer;display:flex;
67
+ align-items:center;justify-content:center;font-size:1.1rem;transition:all var(--tr)}
68
+ .btn-icon:hover{background:var(--surface3);color:var(--text); transform:rotate(10deg);}
69
 
70
  /* MAIN */
71
+ .main{flex:1;display:flex;flex-direction:column;overflow:hidden;min-width:0; background: var(--bg);}
72
+ .topbar{height:70px;padding:0 24px;display:flex;align-items:center;gap:16px; flex-shrink:0}
73
  .topbar-title{flex:1;font-size:1.05rem;font-weight:600;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
74
 
75
  /* CHAT AREA */
76
+ .chat-area{flex:1;overflow-y:auto;padding:20px 0 40px;display:flex;flex-direction:column; scroll-behavior: smooth;}
77
  .welcome{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;
78
+ gap:20px;padding:40px 24px;text-align:center; animation: fadeSlideUp 0.6s ease;}
79
  .w-logo{width:85px;height:85px;border-radius:24px;object-fit:cover;box-shadow:0 12px 40px var(--accent-soft)}
80
+ .w-title{font-size:2.4rem;font-weight:700;background:linear-gradient(135deg,var(--accent),var(--accent2));
81
+ -webkit-background-clip:text;-webkit-text-fill-color:transparent}
82
  .w-sub{color:var(--text2);font-size:1.05rem}
83
  .chips{display:flex;flex-wrap:wrap;justify-content:center;gap:10px;margin-top:12px;max-width:600px}
84
  .chip{padding:10px 20px;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-pill);
85
+ font-size:.9rem;color:var(--text);cursor:pointer;transition:all var(--tr); box-shadow: 0 4px 12px rgba(0,0,0,0.05);}
86
+ .chip:hover{background:var(--accent-soft);border-color:var(--accent);color:var(--accent); transform:translateY(-2px);}
87
 
88
+ /* MESSAGES & ANIMATIONS */
89
+ .msg-row{display:flex;padding:8px 30px;gap:16px; animation: popIn 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards;}
90
+ @keyframes popIn { from { opacity: 0; transform: translateY(15px) scale(0.98); filter: blur(4px); } to { opacity: 1; transform: translateY(0) scale(1); filter: blur(0); } }
91
+ @keyframes fadeSlideUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
92
  .msg-row.user{flex-direction:row-reverse}
93
  .msg-av{width:38px;height:38px;border-radius:14px;flex-shrink:0;display:flex;align-items:center;
94
+ justify-content:center;font-size:1.1rem;font-weight:700;margin-top:2px;overflow:hidden;
95
+ box-shadow: 0 4px 10px rgba(0,0,0,0.1);}
96
  .msg-row.bot .msg-av{background:linear-gradient(135deg,var(--accent),var(--accent2))}
97
  .msg-row.bot .msg-av img{width:100%;height:100%;object-fit:cover}
98
  .msg-row.user .msg-av{background:var(--surface3);color:var(--text)}
99
+
100
+ .bubble{max-width:min(700px,80vw);padding:16px 22px;border-radius:var(--radius);
101
+ font-size:.98rem;line-height:1.75;word-break:break-word; box-shadow: 0 4px 15px rgba(0,0,0,0.03);}
102
  .msg-row.bot .bubble{background:transparent;border:none;padding:8px 4px;box-shadow:none;border-top-left-radius:var(--radius);}
103
+ [dir="rtl"] .msg-row.bot .bubble { border-top-left-radius:var(--radius); border-top-right-radius:var(--radius); }
104
+ .msg-row.user .bubble{background:var(--user-bubble);border:none; border-top-right-radius:6px}
105
+ [dir="rtl"] .msg-row.user .bubble { border-top-right-radius:var(--radius); border-top-left-radius:6px; }
106
+
107
+ /* GEMINI STREAMING EFFECT */
108
+ .gemini-cursor { display: inline-block; width: 14px; height: 14px; margin-inline-start: 6px; background: linear-gradient(135deg, var(--accent), var(--accent2)); mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 0l2.5 9.5L24 12l-9.5 2.5L12 24l-2.5-9.5L0 12l9.5-2.5z"/></svg>'); -webkit-mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 0l2.5 9.5L24 12l-9.5 2.5L12 24l-2.5-9.5L0 12l9.5-2.5z"/></svg>'); mask-size: cover; -webkit-mask-size: cover; animation: spinPulse 1s infinite linear; transform-origin: center; vertical-align: middle; }
109
+ @keyframes spinPulse { 0% { transform: scale(0.8) rotate(0deg); opacity: 0.5; } 50% { transform: scale(1.2) rotate(90deg); opacity: 1; filter: drop-shadow(0 0 5px var(--accent)); } 100% { transform: scale(0.8) rotate(180deg); opacity: 0.5; } }
110
+
111
+ /* IMAGE SKELETON LOADING (تأثير التدرج لتوليد الصور) */
112
+ .skeleton-img {
113
+ width: 100%; max-width: 400px; height: 350px;
114
+ margin: 15px auto; border-radius: 18px;
115
+ background: linear-gradient(90deg, var(--surface2) 25%, var(--surface3) 50%, var(--surface2) 75%);
116
+ background-size: 200% 100%;
117
+ animation: skeletonLoading 1.5s infinite linear;
118
+ box-shadow: 0 8px 25px rgba(0,0,0,0.1);
119
+ display: flex; align-items: center; justify-content: center;
120
+ color: var(--text3); font-size: 2rem;
121
+ }
122
+ @keyframes skeletonLoading {
123
+ 0% { background-position: 200% 0; }
124
+ 100% { background-position: -200% 0; }
125
+ }
126
 
127
+ /* MARKDOWN STYLES */
128
  .bubble p{margin-bottom:12px}.bubble p:last-child{margin-bottom:0}
129
  .bubble ul,.bubble ol{padding-inline-start:24px;margin:12px 0}
130
+ .bubble li {margin-bottom: 6px;}
131
  .bubble .table-wrapper{width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;margin:16px 0;border-radius:var(--radius-sm);border:1px solid var(--border);}
132
  .bubble table{width:100%;border-collapse:collapse;min-width:400px;}
133
  .bubble th,.bubble td{border:1px solid var(--border);padding:10px 14px;text-align:start;white-space:nowrap;}
134
  .bubble th{background:var(--surface3);font-weight:600;}
135
 
136
+ /* Code Blocks */
137
  .code-container{position:relative;margin:16px 0;background:var(--surface3);border-radius:var(--radius-sm);border:1px solid var(--border);overflow:hidden;max-width:100%;}
138
  .code-header{display:flex;justify-content:space-between;align-items:center;background:#12141a;padding:8px 16px;font-size:0.8rem;color:var(--text2);font-family:'JetBrains Mono',monospace;}
139
  .copy-btn{background:var(--surface2);border:none;color:var(--text);cursor:pointer;display:flex;align-items:center;gap:6px;font-size:0.8rem;padding:4px 10px;border-radius:var(--radius-pill);transition:all 0.2s;}
 
142
  .code-container pre code{white-space:pre;display:block;}
143
  .bubble code:not(pre code){font-family:'JetBrains Mono',monospace;font-size:.85rem;background:var(--surface3);padding:3px 6px;border-radius:6px;color:var(--accent);}
144
 
145
+ /* KaTeX Math Styles */
146
  .katex-display{overflow-x:auto;overflow-y:hidden;padding:8px 0;-webkit-overflow-scrolling:touch;}
147
  .katex{font-size:1.05em;}
148
 
149
+ /* Files */
150
+ .file-chip {display:inline-flex; align-items:center; gap:6px; background:var(--surface2); border:1px solid var(--border); border-radius:var(--radius-pill); padding:4px 12px; font-size:0.85rem; margin:0 4px 8px 0; color:var(--text);}
151
+ .file-chip button {background:none; border:none; color:var(--danger); cursor:pointer; font-size:1rem; padding:0; margin-left:4px; transition:transform 0.2s;}
152
+ .file-chip button:hover {transform:scale(1.2);}
153
+ .file-prev {display:flex; align-items:center; gap:8px; background:var(--surface3); padding:8px 12px; border-radius:8px; margin-bottom:8px; font-size:0.85rem;}
154
+
155
+ .mem-badge{font-size:.75rem;color:var(--text3);padding:4px 30px 10px;display:flex;align-items:center;gap:6px; font-weight: 600;}
156
 
157
  /* INPUT */
158
+ .input-area{padding:0 24px 30px; display:flex; flex-direction:column; align-items:center; flex-shrink:0;}
159
+ .input-wrapper{width:100%; max-width:850px; position:relative;}
160
+ .file-chips-container {width:100%; max-width:850px; display:flex; flex-wrap:wrap; margin-bottom:8px;}
161
+ .input-box{display:flex;align-items:flex-end;gap:12px;background:var(--surface);
162
+ border:1px solid var(--border);border-radius:32px;padding:12px 16px 12px 16px;
163
+ transition:all var(--tr); box-shadow: 0 8px 30px rgba(0,0,0,0.15);}
164
+ .input-box:focus-within{border-color:var(--accent); box-shadow: 0 8px 30px var(--accent-soft), 0 0 0 3px var(--accent-soft);}
165
+
166
+ .btn-attach{background:none; border:none; color:var(--text3); cursor:pointer; font-size:1.3rem; padding:6px; border-radius:50%; transition:all var(--tr); display:flex; align-items:center; justify-content:center;}
167
+ .btn-attach:hover{color:var(--accent); background:var(--surface2);}
168
+
169
+ .input-box textarea{flex:1;background:none;border:none;outline:none;color:var(--text);
170
+ font-family:'Cairo',sans-serif;font-size:.98rem;resize:none;max-height:160px;line-height:1.6; padding: 6px 0;}
171
  .input-box textarea::placeholder{color:var(--text3)}
172
+
173
+ .btn-send, .btn-stop {width:42px;height:42px; border:none;border-radius:50%;cursor:pointer;color:#fff;display:flex;align-items:center;justify-content:center;font-size:1.1rem;transition:all var(--tr); flex-shrink:0; margin-bottom: 2px;}
174
+ .btn-send {background:linear-gradient(135deg,var(--accent),var(--accent2));}
175
+ .btn-send:hover{opacity:.9;transform:scale(1.08) rotate(-10deg);}[dir="rtl"] .btn-send:hover{transform:scale(1.08) rotate(10deg);}
 
 
176
  .btn-send:disabled{opacity:.4;cursor:not-allowed;transform:none;}
177
+
178
+ /* Stop Button Styles */
179
+ .btn-stop {background:var(--surface3); color:var(--text); border:1px solid var(--border);}
180
+ .btn-stop:hover {background:var(--danger); color:#fff; transform:scale(1.05);}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181
 
182
  /* MODAL */
183
+ .overlay{position:fixed;inset:0;z-index:999;background:rgba(0,0,0,.6);display:flex;
184
+ align-items:center;justify-content:center;backdrop-filter:blur(8px);
185
+ opacity:0;pointer-events:none;transition:opacity var(--tr)}
186
  .overlay.open{opacity:1;pointer-events:all}
187
+ .modal{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:32px;
188
+ width:min(440px,90vw);box-shadow:0 30px 80px rgba(0,0,0,.5);
189
+ transform:translateY(20px) scale(0.95);transition:all var(--tr)}
190
  .overlay.open .modal{transform:translateY(0) scale(1)}
191
  .m-header{display:flex;align-items:center;gap:12px;margin-bottom:24px}
192
  .m-title{font-size:1.3rem;font-weight:700;flex:1}
193
+ .m-close{background:var(--surface2);border:none;color:var(--text);cursor:pointer;width:36px;height:36px; border-radius:50%; display:flex; align-items:center; justify-content:center; transition:background var(--tr);}
194
+ .m-close:hover{background:var(--danger); color:#fff;}
195
  .s-row{display:flex;align-items:center;justify-content:space-between;padding:16px 0;border-bottom:1px solid var(--border)}
196
  .s-lbl{font-size:.95rem;font-weight:600;color:var(--text)}
197
  .s-desc{font-size:.8rem;color:var(--text3);margin-top:4px}
198
  .toggle{position:relative;width:48px;height:26px}
199
  .toggle input{opacity:0;width:0;height:0}
200
  .tslider{position:absolute;inset:0;background:var(--surface3);border-radius:var(--radius-pill);cursor:pointer;transition:all var(--tr)}
201
+ .tslider::before{content:'';position:absolute;width:20px;height:20px;border-radius:50%;
202
+ background:#fff;top:3px;left:4px;transition:all var(--tr); box-shadow: 0 2px 5px rgba(0,0,0,0.2);}
203
  .toggle input:checked+.tslider{background:var(--accent)}
204
  .toggle input:checked+.tslider::before{transform:translateX(20px)}
205
+
206
+ select.s-input { width:auto; padding:8px 16px; background:var(--surface2); border:1px solid var(--border); border-radius:var(--radius-pill); color:var(--text); cursor:pointer; font-family:'Cairo',sans-serif; font-weight:600;}
207
+ .btn-save{width:100%;margin-top:24px;padding:14px;background:linear-gradient(135deg,var(--accent),var(--accent2));
208
+ color:#fff;border:none;border-radius:var(--radius-pill);cursor:pointer;font-family:'Cairo',sans-serif;font-size:1rem;font-weight:700; transition:opacity var(--tr);}
209
  .btn-save:hover{opacity:.9}
210
+ .btn-danger{padding:8px 18px;background:rgba(247,95,95,.12);border:1px solid var(--danger);
211
+ color:var(--danger);border-radius:var(--radius-pill);cursor:pointer;font-size:.9rem;font-weight:600;font-family:'Cairo',sans-serif; transition:all var(--tr);}
212
+ .btn-danger:hover{background:var(--danger); color:#fff;}
213
 
214
+ /* ══ RESPONSIVE MOBILE ══ */
215
  @media (max-width: 640px) {
216
+ .msg-row { padding: 6px 12px; gap: 10px; }
217
+ .bubble { max-width: 92vw; padding: 12px 14px; font-size: .93rem; }
218
+ .msg-row.bot .bubble { padding: 6px 2px; }
219
+ .topbar { height: 56px; padding: 0 14px; }
220
+ .input-area { padding: 0 10px 18px; }
221
+ .code-container pre { font-size: 0.78rem; padding: 12px; }
222
+ .bubble th, .bubble td { font-size: 0.85rem; padding: 8px 10px; }
223
+ .w-title { font-size: 1.8rem; }
224
+ .chips { gap: 8px; }
225
+ .chip { font-size: .82rem; padding: 8px 14px; }
 
 
 
 
 
226
  }
227
  </style>
228
  </head>
229
  <body>
230
  <div class="app">
231
+
232
+ <aside class="sidebar" id="sidebar">
233
  <div class="sidebar-header">
234
  <div class="s-logo">
235
  <img src="https://copilot.microsoft.com/th/id/BCO.f29916dd-b0c1-4089-87cd-43d099a7d1a6.png" alt="Genisi"/>
236
  <span class="s-logo-name">Genisi AI</span>
237
  </div>
 
 
 
238
  </div>
239
+ <button class="btn-new" onclick="newChat()" id="i18n-new-chat">✦ New Chat</button>
 
 
 
240
  <div class="s-label" id="i18n-chats-label">CHATS</div>
241
  <div class="chats-list" id="chats-list"></div>
242
  <div class="s-footer">
243
+ <button class="btn-icon" onclick="openSettings()">⚙</button>
244
+ <button class="btn-icon" id="theme-btn" onclick="toggleTheme()">🌙</button>
 
 
 
 
245
  </div>
246
  </aside>
247
 
248
  <main class="main">
249
  <div class="topbar">
250
+ <button class="btn-icon" onclick="toggleSidebar()">☰</button>
 
 
251
  <div class="topbar-title" id="topbar-title">Genisi AI</div>
252
  </div>
253
+
254
  <div class="chat-area" id="chat-area"></div>
255
+
256
  <div class="input-area">
 
 
 
 
 
 
 
257
  <div class="file-chips-container" id="file-chips"></div>
258
  <div class="input-wrapper">
259
  <div class="input-box">
260
  <input type="file" id="file-input" multiple hidden onchange="handleFiles(this.files)" />
261
+ <button class="btn-attach" onclick="document.getElementById('file-input').click()">📎</button>
 
 
262
  <textarea id="msg-input" rows="1" onkeydown="handleKey(event)" oninput="autoResize(this)" placeholder="Type a message..."></textarea>
263
+ <button class="btn-send" id="send-btn" onclick="sendMessage()">➤</button>
264
+ <button class="btn-stop" id="stop-btn" onclick="stopGeneration()" style="display:none;"></button>
 
 
 
 
 
 
 
265
  </div>
266
  </div>
267
  </div>
 
273
  <div class="modal">
274
  <div class="m-header">
275
  <div class="m-title" id="i18n-settings-title">Settings</div>
276
+ <button class="m-close" onclick="closeSettings()">✕</button>
 
 
277
  </div>
278
  <div class="s-row">
279
  <div class="s-lbl" id="i18n-lang-lbl">Language</div>
 
291
  <span class="tslider"></span>
292
  </label>
293
  </div>
 
 
 
 
 
 
 
 
 
 
294
  <div class="s-row">
295
  <div>
296
  <div class="s-lbl" id="i18n-del-lbl">Delete All Chats</div>
297
  <div class="s-desc" id="i18n-del-desc">This action cannot be undone</div>
298
  </div>
299
+ <button class="btn-danger" onclick="clearAllChats()" id="i18n-del-btn">🗑 Delete</button>
 
 
300
  </div>
301
+ <button class="btn-save" onclick="closeSettings()" id="i18n-save-btn">💾 Save</button>
302
  </div>
303
  </div>
304
 
305
  <script>
306
+ // ════════ i18n (DICTIONARY) ════════
307
  const i18n = {
308
+ en: { newChat: "✦ New Chat", chatsLabel: "CHATS", settings: "Settings", lang: "Language", dark: "Dark Mode", del: "Delete All Chats", delDesc: "This action cannot be undone", delBtn: "🗑 Delete", saveBtn: "💾 Close", placeholder: "Type your message... (Enter to send)", welcomeTitle: "Welcome to Genisi", welcomeSub: "Your smart assistant by AnesNT — Batna 🇩🇿", c1: "What is AI?", c2: "Write Python code", c3: "Summarize a topic", c4: "Design an image in space 🚀", errConnect: "Connection Error", memBadge: "Memory: {c}/{t} messages", stopMsg: "[Generation stopped by user]" },
309
+ ar: { newChat: "✦ محادثة جديدة", chatsLabel: "المحادثات", settings: "الإعدادات", lang: "اللغة", dark: "الوضع الليلي", del: "حذف جميع المحادثات", delDesc: "لا يمكن التراجع عن هذا الإجراء", delBtn: "🗑 حذف", saveBtn: "💾 إغلاق", placeholder: "اكتب رسالتك... (Enter للإرسال)", welcomeTitle: "مرحبًا في Genisi", welcomeSub: "مساعدك الذكي من AnesNT — ولاية باتنة 🇩🇿", c1: "ما هو الذكاء الاصطناعي؟", c2: "اكتب كود بايثون", c3: "لخص لي موضوعاً", c4: "صمم صورة بالفضاء 🚀", errConnect: "خطأ في الاتصال", memBadge: "الذاكرة: {c}/{t} رسائل", stopMsg: "[تم إيقاف التوليد]" },
310
+ fr: { newChat: "✦ Nouvelle Disc.", chatsLabel: "DISCUSSIONS", settings: "Paramètres", lang: "Langue", dark: "Mode Sombre", del: "Supprimer Tout", delDesc: "Action irréversible", delBtn: "🗑 Supprimer", saveBtn: "💾 Fermer", placeholder: "Écrivez votre message...", welcomeTitle: "Bienvenue sur Genisi", welcomeSub: "Votre assistant intelligent par AnesNT — Batna 🇩🇿", c1: "Qu'est-ce que l'IA?", c2: "Code Python", c3: "Résumer un sujet", c4: "Créer une image 🚀", errConnect: "Erreur de connexion", memBadge: "Mémoire: {c}/{t} msgs", stopMsg: "[Génération arrêtée]" },
311
+ es: { newChat: "✦ Nuevo Chat", chatsLabel: "CHATS", settings: "Ajustes", lang: "Idioma", dark: "Modo Oscuro", del: "Borrar Todo", delDesc: "Acción irreversible", delBtn: "🗑 Borrar", saveBtn: "💾 Cerrar", placeholder: "Escribe tu mensaje...", welcomeTitle: "Bienvenido a Genisi", welcomeSub: "Tu asistente por AnesNT — Batna 🇩🇿", c1: "¿Qué es la IA?", c2: "Código Python", c3: "Resumir", c4: "Diseñar imagen 🚀", errConnect: "Error de conexión", memBadge: "Memoria: {c}/{t} msgs", stopMsg: "[Generación detenida]" }
312
  };
313
 
314
  let currentLang = localStorage.getItem('genisi_lang') || 'en';
 
317
  const t = i18n[currentLang];
318
  document.documentElement.dir = currentLang === 'ar' ? 'rtl' : 'ltr';
319
  document.documentElement.lang = currentLang;
320
+
321
+ document.getElementById('i18n-new-chat').textContent = t.newChat;
322
  document.getElementById('i18n-chats-label').textContent = t.chatsLabel;
323
  document.getElementById('i18n-settings-title').textContent = t.settings;
324
  document.getElementById('i18n-lang-lbl').textContent = t.lang;
325
  document.getElementById('i18n-dark-lbl').textContent = t.dark;
 
 
326
  document.getElementById('i18n-del-lbl').textContent = t.del;
327
  document.getElementById('i18n-del-desc').textContent = t.delDesc;
328
+ document.getElementById('i18n-del-btn').textContent = t.delBtn;
329
  document.getElementById('i18n-save-btn').textContent = t.saveBtn;
330
  document.getElementById('msg-input').placeholder = t.placeholder;
331
+
332
  if(!activeChatId) renderWelcome();
333
  renderChatList();
334
  }
335
+ function changeLanguage(val) { currentLang = val; localStorage.setItem('genisi_lang', val); applyI18n(); }
336
+
337
+ // ════════ STATE & CONFIG ════════
338
+ const genId = () => Date.now().toString(36) + Math.random().toString(36).substring(2, 6);
339
 
 
 
340
  let userId = localStorage.getItem('genisi_user_id');
341
+ if (!userId) {
342
+ userId = 'user_' + genId();
343
+ localStorage.setItem('genisi_user_id', userId);
344
+ }
345
 
346
+ let chats = JSON.parse(localStorage.getItem('genisi_chats') || '[]');
347
  let activeChatId = null;
348
  let isGenerating = false;
349
+ let pendingFiles =[];
350
+ let currentAbortController = null; // متحكم إيقاف التوليد
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
351
 
352
+ // ════════ MARKDOWN PARSER ════════
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
353
  const renderer = new marked.Renderer();
354
  renderer.code = function(token) {
355
  const codeText = typeof token === 'string' ? token : token.text || '';
356
  const lang = typeof token === 'string' ? arguments[1] : token.lang || 'text';
357
+ const escapedCode = String(codeText).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
358
+ return `
359
+ <div class="code-container">
360
+ <div class="code-header">
361
+ <span>${lang}</span>
362
+ <button class="copy-btn" onclick="copyCode(this, '${encodeURIComponent(codeText)}')">📋 Copy</button>
363
+ </div>
364
+ <pre><code class="language-${lang}">${escapedCode}</code></pre>
365
+ </div>`;
366
  };
367
  renderer.table = function(header, body) {
368
+ if (typeof header === 'object' && header !== null && 'header' in header) {
369
+ const token = header;
370
+ const hdr = token.header.map(cell => `<th>${cell.tokens.map(t=>t.raw||'').join('')}</th>`).join('');
371
+ const rows = (token.rows || []).map(row => `<tr>${row.map(cell => `<td>${cell.tokens.map(t=>t.raw||'').join('')}</td>`).join('')}</tr>`).join('');
372
  return `<div class="table-wrapper"><table><thead><tr>${hdr}</tr></thead><tbody>${rows}</tbody></table></div>`;
373
  }
374
  return `<div class="table-wrapper"><table><thead>${header}</thead><tbody>${body}</tbody></table></div>`;
375
  };
376
+ renderer.html = function(html) { return html; };
377
+ marked.setOptions({ renderer: renderer, breaks: true, gfm: true });
378
+
379
+ function renderKaTeX(el) {
380
+ if (window.renderMathInElement) {
381
+ renderMathInElement(el, {
382
+ delimiters: [
383
+ {left: '$$', right: '$$', display: true},
384
+ {left: '$', right: '$', display: false},
385
+ {left: '\\(', right: '\\)', display: false},
386
+ {left: '\\[', right: '\\]', display: true}
387
+ ],
388
+ throwOnError: false
389
+ });
390
+ }
391
  }
392
+
393
+ function copyCode(btn, encodedCode) {
394
+ navigator.clipboard.writeText(decodeURIComponent(encodedCode)).then(() => {
395
+ const originalText = btn.innerHTML;
396
+ btn.innerHTML = "✅ Copied!";
397
+ setTimeout(() => btn.innerHTML = originalText, 2000);
398
  });
399
  }
 
400
 
401
+ // ════════ UTILS & FILES ════════
402
+ const saveChats = () => localStorage.setItem('genisi_chats', JSON.stringify(chats));
403
+ const getChat = (id) => chats.find(c => c.id === (id ?? activeChatId));
404
  function esc(t){ return String(t).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
405
+ function scrollToBottom() {
406
+ const area = document.getElementById('chat-area');
407
+ area.scrollTop = area.scrollHeight;
 
 
 
 
 
 
 
 
 
 
 
 
408
  }
409
+
410
+ async function handleFiles(files) {
411
+ for (let f of files) {
412
+ const isText = f.type.startsWith('text/') || f.name.match(/\.(py|js|html|css|json|md|txt|csv|cpp|java)$/i);
413
+ const reader = new FileReader();
414
+ reader.onload = (e) => {
415
+ let data = e.target.result;
416
+ if (!isText) data = data.split(',')[1];
417
+ pendingFiles.push({ name: f.name, type: f.type || 'application/octet-stream', data: data, isText: isText });
418
+ renderFileChips();
419
+ };
420
+ if (isText) reader.readAsText(f);
421
+ else reader.readAsDataURL(f);
422
+ }
423
+ document.getElementById('file-input').value = '';
424
  }
 
425
 
426
+ function renderFileChips() {
427
+ const container = document.getElementById('file-chips');
428
+ container.innerHTML = '';
429
+ pendingFiles.forEach((f, idx) => {
430
+ const icon = f.isText ? '📄' : '🖼️';
431
+ container.innerHTML += `<div class="file-chip">${icon} ${f.name} <button onclick="removeFile(${idx})">×</button></div>`;
432
+ });
433
+ }
434
+ function removeFile(idx) { pendingFiles.splice(idx, 1); renderFileChips(); }
435
+
436
+ // ════════ THEME & UI ════════
437
  function applyTheme(t){
438
  document.documentElement.setAttribute('data-theme',t);
439
  localStorage.setItem('genisi_theme',t);
440
+ document.getElementById('theme-btn').textContent = t==='dark'?'🌙':'☀️';
441
  document.getElementById('dark-toggle').checked = t==='dark';
 
 
 
442
  }
443
  function toggleTheme(){ applyTheme(document.documentElement.getAttribute('data-theme')==='dark'?'light':'dark'); }
444
  function applyThemeToggle(){ applyTheme(document.getElementById('dark-toggle').checked?'dark':'light'); }
445
 
446
+ function toggleSidebar(){ document.getElementById('sidebar').classList.toggle('collapsed'); }
447
+ function openSettings(){ document.getElementById('settings-modal').classList.add('open'); document.getElementById('lang-select').value=currentLang;}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
448
  function closeSettings(){ document.getElementById('settings-modal').classList.remove('open'); }
449
 
 
450
  function renderChatList(){
451
+ const list=document.getElementById('chats-list');
452
+ list.innerHTML='';
453
  [...chats].reverse().forEach(chat=>{
454
  const div=document.createElement('div');
455
  div.className='chat-item'+(chat.id===activeChatId?' active':'');
456
+ div.innerHTML=`<span>💬</span><span class="ct">${esc(chat.title||'Chat')}</span>
457
+ <button class="del-btn" onclick="deleteChat('${chat.id}',event)">✕</button>`;
458
+ div.onclick = () => loadChat(chat.id);
459
  list.appendChild(div);
460
  });
 
461
  }
462
 
463
  function renderWelcome(){
464
+ const t = i18n[currentLang];
465
  document.getElementById('topbar-title').textContent='Genisi AI';
466
+ document.getElementById('chat-area').innerHTML=`
467
+ <div class="welcome" id="welcome">
468
+ <img src="https://copilot.microsoft.com/th/id/BCO.f29916dd-b0c1-4089-87cd-43d099a7d1a6.png" class="w-logo" alt="Genisi"/>
469
+ <div class="w-title">${t.welcomeTitle}</div>
470
+ <div class="w-sub">${t.welcomeSub}</div>
471
+ <div class="chips">
472
+ <div class="chip" onclick="quickSend('${t.c1}')">${t.c1}</div>
473
+ <div class="chip" onclick="quickSend('${t.c2}')">${t.c2}</div>
474
+ <div class="chip" onclick="quickSend('${t.c3}')">${t.c3}</div>
475
+ <div class="chip" onclick="quickSend('${t.c4}')">${t.c4}</div>
476
+ </div>
477
+ </div>`;
478
  }
479
 
480
  function newChat(){ activeChatId=null; pendingFiles=[]; renderFileChips(); document.getElementById('msg-input').value=''; renderWelcome(); renderChatList(); }
481
 
482
  function loadChat(id){
483
+ activeChatId=id;
484
+ const chat=getChat(id); if(!chat) return;
485
  document.getElementById('topbar-title').textContent=chat.title;
486
+ const area=document.getElementById('chat-area');
487
+ area.innerHTML='';
488
+ chat.history.forEach((entry, idx)=>{
489
+ appendBubble('user', entry.user, false, entry.uiFiles);
490
+ appendBubble('bot', entry.bot, true);
491
+ appendMemBadge(idx+1, chat.history.length);
492
+ });
493
+ scrollToBottom();
494
+ renderChatList();
495
+ }
496
+
497
+ function deleteChat(id,e){
498
+ e.stopPropagation(); chats=chats.filter(c=>c.id!==id); saveChats();
499
+ if(activeChatId===id) newChat(); else renderChatList();
500
  }
 
501
  function clearAllChats(){ chats=[]; saveChats(); newChat(); closeSettings(); }
502
+
503
+ // ════════ STOP GENERATION ════════
504
+ function stopGeneration() {
505
+ if (currentAbortController) {
506
+ currentAbortController.abort();
507
+ }
508
+ }
509
+
510
+ // ════════ MESSAGING & STREAMING ════════
511
  function quickSend(text){ document.getElementById('msg-input').value=text; sendMessage(); }
512
  function handleKey(e){ if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();sendMessage();} }
513
  function autoResize(el){ el.style.height='auto'; el.style.height=Math.min(el.scrollHeight,160)+'px'; }
514
 
 
515
  async function sendMessage(){
516
+ if(isGenerating) return;
517
+ const input = document.getElementById('msg-input');
518
+ let text = input.value.trim();
519
+
520
+ if(!text && pendingFiles.length === 0) return;
521
+
522
+ const welcome = document.getElementById('welcome');
523
+ if(welcome) welcome.remove();
524
+
525
  input.value=''; input.style.height='auto';
 
526
 
527
  if(!activeChatId){
528
+ activeChatId = genId();
529
+ const chatTitle = text ? text.slice(0,35)+'...' : (pendingFiles[0] ? pendingFiles[0].name : 'Chat');
530
+ chats.push({id:activeChatId, title:chatTitle, history:[]});
531
  document.getElementById('topbar-title').textContent=chatTitle;
532
  }
533
+
534
+ const chat = getChat();
535
+ const uiFiles = pendingFiles.map(f => f.name);
536
+ const binaryFiles =[];
537
+
538
+ pendingFiles.forEach(f => {
539
+ if(f.isText) { text += `\n\n\`\`\`${f.name.split('.').pop()}\n// File: ${f.name}\n${f.data}\n\`\`\``; }
540
+ else { binaryFiles.push({ name: f.name, mime_type: f.type, data: f.data }); }
541
  });
 
 
542
 
543
+ appendBubble('user', text, false, uiFiles);
544
+ pendingFiles =[]; renderFileChips();
545
+
546
+ // تبديل زر الإرسال بزر الإيقاف
547
+ isGenerating = true;
548
+ document.getElementById('send-btn').style.display = 'none';
549
+ document.getElementById('stop-btn').style.display = 'flex';
550
 
551
+ const botContentDiv = appendBubble('bot', '', true);
552
+
553
+ // التحقق من أن الطلب لإنشاء صورة لتفعيل المربع المتدرج (Skeleton)
554
+ const imageTriggers =["ارسم", "صمم", "تخيل", "صورة ل", "draw", "generate", "imagine", "create an image", "تصميم"];
555
+ const isImageRequest = imageTriggers.some(trigger => text.toLowerCase().startsWith(trigger));
556
+
557
+ currentAbortController = new AbortController();
558
+ let fullBotResponse = "";
559
 
560
  try {
561
+ const response = await fetch('/chat', {
562
+ method: 'POST',
563
+ headers: {'Content-Type':'application/json'},
564
+ body: JSON.stringify({ user_id: userId, message: text, history: chat.history, files: binaryFiles }),
565
+ signal: currentAbortController.signal
566
  });
567
+
568
  if(!response.ok) throw new Error("Server Error");
569
+
570
+ const reader = response.body.getReader();
571
+ const decoder = new TextDecoder("utf-8");
572
+
573
+ while(true) {
574
+ const { value, done } = await reader.read();
575
  if(done) break;
576
+
577
+ const chunk = decoder.decode(value, {stream: true});
578
+ fullBotResponse += chunk;
579
+
580
+ // إذا كان الرد عبارة عن صورة HTML جاهزة، تظهر مباشرة
581
+ if (fullBotResponse.includes('<div style="text-align:center;')) {
582
+ botContentDiv.innerHTML = fullBotResponse;
583
+ } else {
584
+ // إذا كان الطلب لصورة لكننا لم نستلم الـ HTML النهائي بعد، نعرض Skeleton
585
+ let htmlToRender = marked.parse(fullBotResponse);
586
+ if (isImageRequest && !fullBotResponse.includes('<img')) {
587
+ htmlToRender += '<div class="skeleton-img">🎨 جاري التخيل والتصميم...</div>';
588
+ }
589
+ botContentDiv.innerHTML = htmlToRender + '<span class="gemini-cursor"></span>';
590
+ }
591
+ scrollToBottom();
592
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
593
 
594
+ if (fullBotResponse.includes('<div style="text-align:center;')) {
595
+ botContentDiv.innerHTML = fullBotResponse;
 
 
 
 
 
 
 
 
 
 
596
  } else {
597
+ botContentDiv.innerHTML = marked.parse(fullBotResponse);
598
+ renderKaTeX(botContentDiv);
 
599
  }
600
+
601
+ chat.history.push({ user: text, bot: fullBotResponse, uiFiles: uiFiles });
602
+ saveChats();
603
+ appendMemBadge(chat.history.length, chat.history.length);
604
+
605
+ } catch(err){
606
+ if (err.name === 'AbortError') {
607
+ botContentDiv.innerHTML = marked.parse(fullBotResponse) + `<br><br><span style="color:var(--text3); font-style:italic;">${i18n[currentLang].stopMsg}</span>`;
608
+ renderKaTeX(botContentDiv);
609
+ chat.history.push({ user: text, bot: fullBotResponse + "\n\n*" + i18n[currentLang].stopMsg + "*", uiFiles: uiFiles });
610
+ saveChats();
611
+ } else {
612
+ botContentDiv.innerHTML = `<span style="color:var(--danger)">⚠️ ${i18n[currentLang].errConnect}: ${err.message}</span>`;
613
+ }
614
+ } finally {
615
+ // إرجاع زر الإرسال عند الانتهاء أو الإلغاء
616
+ isGenerating = false;
617
+ document.getElementById('send-btn').style.display = 'flex';
618
+ document.getElementById('stop-btn').style.display = 'none';
619
+ renderChatList();
620
+ scrollToBottom();
621
+ }
622
  }
623
 
624
+ function appendBubble(role, text, isMarkdown, filesList =[]){
 
625
  const area=document.getElementById('chat-area');
626
  const row=document.createElement('div'); row.className=`msg-row ${role}`;
627
  const av=document.createElement('div'); av.className='msg-av';
628
+
629
+ if(role==='bot'){ av.innerHTML=`<img src="https://copilot.microsoft.com/th/id/BCO.f29916dd-b0c1-4089-87cd-43d099a7d1a6.png" alt="G"/>`; }
630
+ else { av.textContent='👤'; }
631
+
632
  const bub=document.createElement('div'); bub.className='bubble';
633
+
634
+ if(filesList && filesList.length > 0) {
635
+ filesList.forEach(name => { bub.innerHTML += `<div class="file-prev">📎 ${esc(name)}</div>`; });
 
 
 
636
  }
 
 
 
 
 
 
 
 
 
 
637
 
638
+ const contentDiv = document.createElement('div');
639
+ if(text) {
640
+ if (text.includes('<div style="text-align:center;')) { contentDiv.innerHTML = text; }
641
+ else {
642
+ contentDiv.innerHTML = isMarkdown ? marked.parse(text) : esc(text).replace(/\n/g, '<br/>');
643
+ if (isMarkdown) renderKaTeX(contentDiv);
644
+ }
645
+ }
646
+ bub.appendChild(contentDiv);
647
+
648
+ row.appendChild(av); row.appendChild(bub);
649
+ area.appendChild(row);
650
  scrollToBottom();
651
+
652
+ return contentDiv;
653
+ }
654
+
655
+ function appendMemBadge(count, total){
656
+ const area=document.getElementById('chat-area');
657
+ const b=document.createElement('div'); b.className='mem-badge';
658
+ b.innerHTML=`🧠 ${i18n[currentLang].memBadge.replace('{c}', count).replace('{t}', total)}`;
659
+ area.appendChild(b); scrollToBottom();
660
  }
661
 
662
  // ════════ INIT ════════
663
+ applyTheme(localStorage.getItem('genisi_theme')||'dark');
664
+ document.getElementById('lang-select').value = currentLang;
665
+ applyI18n();
 
 
 
 
 
 
666
  </script>
667
  </body>
668
+ </html>