Jux154 commited on
Commit
ea94e0a
·
verified ·
1 Parent(s): 0519f1d

Manual changes saved

Browse files
Files changed (1) hide show
  1. index.html +818 -18
index.html CHANGED
@@ -1,19 +1,819 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
+ <!DOCTYPE html>
2
+ <html lang="fr">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Jux-Ai - Assistant IA Personnel</title>
7
+ <meta name="description" content="Interface de chat moderne avec l'assistant Jux, propulsé par l'IA">
8
+ <meta name="author" content="JuxAi Team">
9
+
10
+ <!-- Styles intégrés -->
11
+ <style>
12
+ /* (Ton CSS d'origine inchangé) */
13
+ :root {
14
+ --background: 218 23% 97%;
15
+ --foreground: 222 47% 11%;
16
+ --card: 0 0% 100%;
17
+ --card-foreground: 222 47% 11%;
18
+ --primary: 217 91% 60%;
19
+ --primary-foreground: 0 0% 100%;
20
+ --primary-glow: 217 91% 70%;
21
+ --secondary: 214 32% 91%;
22
+ --secondary-foreground: 222 47% 11%;
23
+ --muted: 210 40% 96%;
24
+ --muted-foreground: 215 16% 47%;
25
+ --accent: 142 76% 36%;
26
+ --accent-foreground: 0 0% 100%;
27
+ --destructive: 0 84% 60%;
28
+ --destructive-foreground: 210 40% 98%;
29
+ --border: 214 32% 91%;
30
+ --input: 214 32% 91%;
31
+ --ring: 217 91% 60%;
32
+ --chat-user: 217 91% 60%;
33
+ --chat-user-foreground: 0 0% 100%;
34
+ --chat-assistant: 0 0% 100%;
35
+ --chat-assistant-foreground: 222 47% 11%;
36
+ --radius: 0.5rem;
37
+ --gradient-primary: linear-gradient(135deg, hsl(var(--primary)), hsl(var(--primary-glow)));
38
+ --gradient-subtle: linear-gradient(180deg, hsl(var(--background)), hsl(var(--muted)));
39
+ --gradient-card: linear-gradient(145deg, hsl(var(--card)) 0%, hsl(var(--muted)) 100%);
40
+ --shadow-elegant: 0 10px 25px -5px hsl(var(--primary) / 0.1);
41
+ --shadow-chat: 0 2px 8px -1px hsl(222 47% 11% / 0.1);
42
+ }
43
+ * { margin: 0; padding: 0; box-sizing: border-box; }
44
+ body {
45
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
46
+ background: hsl(var(--background));
47
+ color: hsl(var(--foreground));
48
+ height: 100vh;
49
+ overflow: hidden;
50
+ }
51
+ /* Utility classes */
52
+ .flex { display: flex; }
53
+ .flex-col { flex-direction: column; }
54
+ .flex-1 { flex: 1; }
55
+ .items-center { align-items: center; }
56
+ .justify-between { justify-content: space-between; }
57
+ .justify-center { justify-content: center; }
58
+ .justify-end { justify-content: flex-end; }
59
+ .justify-start { justify-content: flex-start; }
60
+ .space-x-2 > * + * { margin-left: 0.5rem; }
61
+ .space-x-3 > * + * { margin-left: 0.75rem; }
62
+ .gap-3 { gap: 0.75rem; }
63
+ .h-full { height: 100%; }
64
+ .h-screen { height: 100vh; }
65
+ .min-h-96 { min-height: 24rem; }
66
+ .max-w-xs { max-width: 20rem; }
67
+ .max-w-sm { max-width: 24rem; }
68
+ .max-w-md { max-width: 28rem; }
69
+ .w-full { width: 100%; }
70
+ .p-4 { padding: 1rem; }
71
+ .p-3 { padding: 0.75rem; }
72
+ .px-4 { padding-left: 1rem; padding-right: 1rem; }
73
+ .py-2 { padding-top: 0.5rem; padding-bottom: 0.5rem; }
74
+ .pb-20 { padding-bottom: 5rem; }
75
+ .mb-2 { margin-bottom: 0.5rem; }
76
+ .mb-4 { margin-bottom: 1rem; }
77
+ .mb-6 { margin-bottom: 1.5rem; }
78
+ .mt-1 { margin-top: 0.25rem; }
79
+ .mt-2 { margin-top: 0.5rem; }
80
+ .mr-2 { margin-right: 0.5rem; }
81
+ .ml-auto { margin-left: auto; }
82
+ .mx-auto { margin-left: auto; margin-right: auto; }
83
+ .overflow-hidden { overflow: hidden; }
84
+ .overflow-y-auto { overflow-y: auto; }
85
+ .text-xs { font-size: 0.75rem; }
86
+ .text-sm { font-size: 0.875rem; }
87
+ .text-lg { font-size: 1.125rem; }
88
+ .text-xl { font-size: 1.25rem; }
89
+ .text-2xl { font-size: 1.5rem; }
90
+ .font-bold { font-weight: bold; }
91
+ .font-mono { font-family: monospace; }
92
+ .text-center { text-align: center; }
93
+ .text-right { text-align: right; }
94
+ .text-left { text-align: left; }
95
+ .leading-relaxed { line-height: 1.2; }
96
+ .whitespace-pre-wrap { white-space: pre-wrap; }
97
+ .border { border-width: 1px; }
98
+ .border-b { border-bottom-width: 1px; }
99
+ .border-t { border-top-width: 1px; }
100
+ .border-2 { border-width: 2px; }
101
+ .rounded { border-radius: 0.25rem; }
102
+ .rounded-lg { border-radius: var(--radius); }
103
+ .rounded-full { border-radius: 9999px; }
104
+ .sticky { position: sticky; }
105
+ .relative { position: relative; }
106
+ .absolute { position: absolute; }
107
+ .top-0 { top: 0; }
108
+ .bottom-0 { bottom: 0; }
109
+ .-bottom-1 { bottom: -0.25rem; }
110
+ .-right-1 { right: -0.25rem; }
111
+ .left-0 { left: 0; }
112
+ .z-10 { z-index: 10; }
113
+ .cursor-pointer { cursor: pointer; }
114
+ .select-none { user-select: none; }
115
+ .resize-none { resize: none; }
116
+ .hidden { display: none; }
117
+ .order-first { order: -1; }
118
+ /* Color utilities */
119
+ .bg-background { background-color: hsl(var(--background)); }
120
+ .bg-card { background-color: hsl(var(--card)); }
121
+ .bg-primary { background-color: hsl(var(--primary)); }
122
+ .bg-secondary { background-color: hsl(var(--secondary)); }
123
+ .bg-accent { background-color: hsl(var(--accent)); }
124
+ .bg-destructive { background-color: hsl(var(--destructive)); }
125
+ .bg-muted { background-color: hsl(var(--muted)); }
126
+ .bg-white { background-color: white; }
127
+ .bg-transparent { background-color: transparent; }
128
+ .text-foreground { color: hsl(var(--foreground)); }
129
+ .text-primary { color: hsl(var(--primary)); }
130
+ .text-white { color: white; }
131
+ .text-muted-foreground { color: hsl(var(--muted-foreground)); }
132
+ .text-destructive { color: hsl(var(--destructive)); }
133
+ .border-border { border-color: hsl(var(--border)); }
134
+ .border-destructive { border-color: hsl(var(--destructive)); }
135
+ .border-primary { border-color: hsl(var(--primary) / 0.2); }
136
+ .border-muted { border-color: hsl(var(--muted)); }
137
+ /* Background gradients */
138
+ .bg-gradient-primary { background: var(--gradient-primary); }
139
+ .bg-gradient-subtle { background: var(--gradient-subtle); }
140
+ .bg-gradient-card { background: var(--gradient-card); }
141
+ /* Chat specific colors */
142
+ .bg-chat-user { background-color: hsl(var(--chat-user)); }
143
+ .text-chat-user-foreground { color: hsl(var(--chat-user-foreground)); }
144
+ .bg-chat-assistant { background-color: hsl(var(--chat-assistant)); }
145
+ .text-chat-assistant-foreground { color: hsl(var(--chat-assistant-foreground)); }
146
+ /* Shadows */
147
+ .shadow-elegant { box-shadow: var(--shadow-elegant); }
148
+ .shadow-chat { box-shadow: var(--shadow-chat); }
149
+ /* Component specific styles */
150
+ .card {
151
+ background-color: hsl(var(--card));
152
+ color: hsl(var(--card-foreground));
153
+ border: 1px solid hsl(var(--border));
154
+ border-radius: var(--radius);
155
+ }
156
+ .message-card { padding: 0.05rem 0.1rem; border-radius: 0.15rem; line-height: 1.2; }
157
+ .button { display: inline-flex; align-items: center; justify-content: center; border-radius: var(--radius); font-size: 0.875rem; font-weight: 500; transition: all 0.2s; cursor: pointer; border: none; padding: 0.5rem 1rem; min-height: 2.5rem; }
158
+ .button-primary { background: var(--gradient-primary); color: hsl(var(--primary-foreground)); }
159
+ .button-ghost { background: transparent; color: hsl(var(--foreground)); }
160
+ .button-ghost:hover { background-color: hsl(var(--muted)); }
161
+ .button:disabled { opacity: 0.5; cursor: not-allowed; }
162
+ .badge { display: inline-flex; align-items: center; border-radius: calc(var(--radius) - 2px); padding: 0.25rem 0.5rem; font-size: 0.75rem; font-weight: 600; background-color: hsl(var(--secondary)); color: hsl(var(--secondary-foreground)); }
163
+ .avatar { display: flex; align-items: center; justify-content: center; border-radius: 50%; width: 2rem; height: 2rem; }
164
+ .textarea { width: 100%; border: none; background: transparent; resize: none; outline: none; font-family: inherit; font-size: 0.875rem; line-height: 1.5; padding: 0.5rem; }
165
+ .textarea:focus { outline: none; }
166
+ .chat-scroll { scrollbar-width: thin; scrollbar-color: hsl(var(--border)) transparent; }
167
+ .chat-scroll::-webkit-scrollbar { width: 4px; }
168
+ .chat-scroll::-webkit-scrollbar-track { background: transparent; }
169
+ .chat-scroll::-webkit-scrollbar-thumb { background: hsl(var(--border)); border-radius: 2px; }
170
+ .chat-scroll::-webkit-scrollbar-thumb:hover { background: hsl(var(--muted-foreground)); }
171
+ @keyframes typing-dots { 0%, 60%, 100% { transform: translateY(0); } 30% { transform: translateY(-10px); } }
172
+ .typing-dot { animation: typing-dots 1.5s infinite; width: 0.5rem; height: 0.5rem; background-color: hsl(var(--primary)); border-radius: 50%; }
173
+ .typing-dot:nth-child(2) { animation-delay: 0.2s; }
174
+ .typing-dot:nth-child(3) { animation-delay: 0.4s; }
175
+ @keyframes message-appear { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
176
+ .message-appear { animation: message-appear 0.4s ease-out; }
177
+ @keyframes spin { to { transform: rotate(360deg); } }
178
+ .animate-spin { animation: spin 1s linear infinite; }
179
+ .icon { width: 1rem; height: 1rem; display: inline-block; vertical-align: middle; }
180
+ .icon-lg { width: 1.25rem; height: 1.25rem; }
181
+ @media (min-width: 640px) { .sm\\:flex { display: flex; } .sm\\:hidden { display: none; } .sm\\:inline { display: inline; } .sm\\:block { display: block; } }
182
+ .message-container { display: inline-block; width: fit-content; }
183
+ .user-message { margin-left: auto; }
184
+ .assistant-message { margin-right: auto; }
185
+ .alert { padding: 1rem; border-radius: var(--radius); border: 1px solid hsl(var(--border)); background-color: hsl(var(--card)); }
186
+ .alert-destructive { border-color: hsl(var(--destructive)); color: hsl(var(--destructive)); }
187
+ .backdrop-blur { backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); }
188
+ .separator-vertical { width: 1px; height: 1.5rem; background-color: hsl(var(--border)); }
189
+ .input-container { position: relative; }
190
+ .button-sm { padding: 0.375rem 0.75rem; min-height: 2rem; font-size: 0.75rem; }
191
+ .button-icon { width: 2.5rem; height: 2.5rem; padding: 0; }
192
+ .message-bubble { word-wrap: break-word; overflow-wrap: break-word; }
193
+ /* Small gallery for generation previews */
194
+ #generationPreview { display:flex; gap:8px; margin-top:8px; flex-wrap:wrap; }
195
+ #generationPreview img { width:120px; height:auto; border-radius:8px; border:1px solid hsl(var(--border)); cursor:pointer; }
196
+ .gen-status { font-size:0.85rem; color: hsl(var(--muted-foreground)); margin-top:6px; }
197
+ </style>
198
+ </head>
199
+ <body>
200
+ <div class="flex flex-col h-screen bg-gradient-subtle">
201
+ <!-- En-tête -->
202
+ <header class="sticky top-0 z-10 border-b bg-gradient-card backdrop-blur">
203
+ <div class="flex items-center justify-between p-4">
204
+ <!-- Logo et titre -->
205
+ <div class="flex items-center space-x-3">
206
+ <div class="relative">
207
+ <div class="avatar bg-gradient-primary text-white shadow-elegant" style="width: 2.5rem; height: 2.5rem;">
208
+ <span class="text-lg font-bold">J</span>
209
+ </div>
210
+ <div class="absolute -bottom-1 -right-1 rounded-full bg-accent flex items-center justify-center" style="width: 1rem; height: 1rem;">
211
+ <div class="rounded-full bg-white" style="width: 0.5rem; height: 0.5rem;"></div>
212
+ </div>
213
+ </div>
214
+ <div>
215
+ <h1 class="text-xl font-bold text-foreground">Jux-Ai</h1>
216
+ <p class="text-sm text-muted-foreground">Assistant IA personnel</p>
217
+ </div>
218
+ </div>
219
+
220
+ <!-- Informations sur le modèle et boutons -->
221
+ <div class="flex items-center space-x-3">
222
+ <div class="hidden sm:flex items-center space-x-2">
223
+ <span class="text-sm text-muted-foreground">Modèle :</span>
224
+ <span class="badge font-mono text-xs">google/gemma-3n-e4b</span>
225
+ </div>
226
+
227
+ <div class="separator-vertical hidden sm:block"></div>
228
+
229
+ <!-- Boutons d'action -->
230
+ <div class="flex items-center space-x-2">
231
+ <button class="button button-ghost button-sm hidden sm:flex" onclick="checkModels()">
232
+ <svg class="icon mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
233
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
234
+ </svg>
235
+ Vérifier modèles
236
+ </button>
237
+
238
+ <button class="button button-ghost button-sm hidden sm:flex" onclick="exportConversation()">
239
+ <svg class="icon mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
240
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
241
+ </svg>
242
+ Exporter
243
+ </button>
244
+
245
+ <button class="button button-ghost button-sm text-destructive" onclick="newConversation()">
246
+ <svg class="icon mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
247
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
248
+ </svg>
249
+ <span class="hidden sm:inline">Nouvelle conversation</span>
250
+ </button>
251
+ </div>
252
+ </div>
253
+ </div>
254
+
255
+ <!-- Version mobile : modèle affiché en bas -->
256
+ <div class="sm:hidden px-4 pb-3">
257
+ <div class="flex items-center justify-center space-x-2">
258
+ <span class="text-xs text-muted-foreground">Modèle :</span>
259
+ <span class="badge font-mono text-xs">google/gemma-3n-e4b</span>
260
+ </div>
261
+ </div>
262
+ </header>
263
+
264
+ <!-- Indicateur de connexion (masqué par défaut) -->
265
+ <div id="connectionAlert" class="alert alert-destructive mx-4 mt-4 hidden">
266
+ <div class="flex items-center justify-between">
267
+ <div class="flex items-center space-x-2">
268
+ <svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
269
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 5.636l-3.536 3.536m0 5.656l3.536 3.536M9.172 9.172L5.636 5.636m3.536 9.192L5.636 18.364M12 2.168l5.657 5.657a8 8 0 010 11.314L12 24.832l-5.657-5.657a8 8 0 010-11.314L12 2.168z"/>
270
+ </svg>
271
+ <span>Connexion impossible à l'API (http://192.168.0.11:1234). Vérifiez que le serveur est démarré et que CORS est configuré.</span>
272
+ </div>
273
+ <button class="button button-ghost button-sm" onclick="checkConnection()">
274
+ Réessayer
275
+ </button>
276
+ </div>
277
+ </div>
278
+
279
+ <!-- Zone des messages -->
280
+ <div class="flex-1 overflow-hidden">
281
+ <div class="h-full overflow-y-auto chat-scroll">
282
+ <div class="p-4 pb-20" id="messagesContainer">
283
+ <!-- Message de bienvenue -->
284
+ <div id="welcomeMessage" class="flex items-center justify-center h-full min-h-96">
285
+ <div class="text-center max-w-md">
286
+ <div class="avatar bg-gradient-primary text-white mx-auto mb-4" style="width: 4rem; height: 4rem;">
287
+ <span class="text-2xl font-bold">J</span>
288
+ </div>
289
+ <h2 class="text-2xl font-bold mb-2">Bonjour ! Je suis Jux</h2>
290
+ <p class="text-muted-foreground mb-6">
291
+ Votre assistant IA personnel. Comment puis-je vous aider aujourd'hui ?
292
+ </p>
293
+ <div class="flex items-center justify-center space-x-2 text-sm text-muted-foreground">
294
+ <svg class="icon text-accent" fill="none" stroke="currentColor" viewBox="0 0 24 24">
295
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.857 15.355-5.857 21.213 0"/>
296
+ </svg>
297
+ <span>Connecté et prêt</span>
298
+ </div>
299
+ </div>
300
+ </div>
301
+
302
+ <!-- Les messages s'afficheront ici -->
303
+ <div id="messagesList"></div>
304
+
305
+ <!-- Indicateur de frappe -->
306
+ <div id="typingIndicator" class="message-appear mb-4 hidden">
307
+ <div class="flex gap-3 justify-start">
308
+ <div class="avatar border-2 border-primary bg-gradient-primary text-white">
309
+ <svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
310
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
311
+ </svg>
312
+ </div>
313
+
314
+ <div class="message-container">
315
+ <div class="card p-4 shadow-chat bg-chat-assistant text-chat-assistant-foreground">
316
+ <div class="flex items-center space-x-1">
317
+ <div class="typing-dot"></div>
318
+ <div class="typing-dot"></div>
319
+ <div class="typing-dot"></div>
320
+ </div>
321
+ </div>
322
+
323
+ <div class="mt-1 text-xs text-muted-foreground text-left">
324
+ Jux réfléchit...
325
+ </div>
326
+ </div>
327
+ </div>
328
+ </div>
329
+
330
+ <div id="messagesEnd"></div>
331
+ </div>
332
+ </div>
333
+ </div>
334
+
335
+ <!-- Zone de saisie -->
336
+ <div class="sticky bottom-0 border-t bg-gradient-subtle backdrop-blur">
337
+ <div class="p-4">
338
+ <div class="card p-4 shadow-elegant">
339
+ <div class="flex gap-3 items-end">
340
+ <div class="flex-1 relative input-container">
341
+ <textarea
342
+ id="messageInput"
343
+ class="textarea"
344
+ placeholder="Tapez votre message ici... (Entrée pour envoyer, Shift+Entrée pour une nouvelle ligne)"
345
+ rows="1"
346
+ style="min-height: 44px; max-height: 200px;"
347
+ ></textarea>
348
+
349
+ <!-- Indicateur de caractères -->
350
+ <div id="charCounter" class="absolute -bottom-6 left-0 text-xs text-muted-foreground hidden"></div>
351
+ </div>
352
+
353
+ <!-- Génération d'image controls -->
354
+ <div class="flex items-end gap-2">
355
+ <select id="genPreset" class="button button-ghost button-sm" title="Choisir preset génération">
356
+ <option value="fast">fast (preview)</option>
357
+ <option value="hd">hd (final)</option>
358
+ </select>
359
+ <button id="generateImageBtn" class="button button-ghost button-sm" onclick="generateImage()" title="Générer une image à partir du prompt">
360
+ Générer Image
361
+ </button>
362
+ </div>
363
+
364
+ <button
365
+ id="sendButton"
366
+ class="button button-primary button-icon"
367
+ onclick="sendMessage()"
368
+ >
369
+ <svg id="sendIcon" class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
370
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M12 5l7 7-7 7"/>
371
+ </svg>
372
+ <svg id="loadingIcon" class="icon animate-spin hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
373
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
374
+ </svg>
375
+ </button>
376
+ </div>
377
+
378
+ <!-- Generation preview + conseils -->
379
+ <div id="generationPanel" class="mt-3">
380
+ <div id="generationStatus" class="gen-status"></div>
381
+ <div id="generationPreview" aria-live="polite"></div>
382
+ </div>
383
+
384
+ <!-- Conseils d'utilisation -->
385
+ <div class="mt-2 text-xs text-muted-foreground text-center">
386
+ <span class="hidden sm:inline">
387
+ Entrée pour envoyer • Shift+Entrée pour une nouvelle ligne
388
+ </span>
389
+ <span class="sm:hidden">
390
+ Appuyez sur le bouton pour envoyer
391
+ </span>
392
+ </div>
393
+ </div>
394
+ </div>
395
+ </div>
396
+ </div>
397
+
398
+ <script>
399
+ // État de l'application
400
+ let messages = [];
401
+ let isLoading = false;
402
+ let isConnected = null;
403
+ let isGenerating = false;
404
+ const API_BASE_URL = 'http://192.168.0.11:1234'; // pour le chat (LM Studio)
405
+ const GENERATION_API_BASE = 'http://192.168.0.11:8001'; // <-- si ton backend de génération est à une autre adresse, change ici
406
+ const DEFAULT_MODEL = 'google/gemma-3n-e4b';
407
+
408
+ // Éléments DOM
409
+ const messageInput = document.getElementById('messageInput');
410
+ const sendButton = document.getElementById('sendButton');
411
+ const messagesContainer = document.getElementById('messagesContainer');
412
+ const messagesList = document.getElementById('messagesList');
413
+ const welcomeMessage = document.getElementById('welcomeMessage');
414
+ const typingIndicator = document.getElementById('typingIndicator');
415
+ const charCounter = document.getElementById('charCounter');
416
+ const sendIcon = document.getElementById('sendIcon');
417
+ const loadingIcon = document.getElementById('loadingIcon');
418
+ const connectionAlert = document.getElementById('connectionAlert');
419
+
420
+ // Génération DOM
421
+ const genPresetSelect = document.getElementById('genPreset');
422
+ const generationPreview = document.getElementById('generationPreview');
423
+ const generationStatus = document.getElementById('generationStatus');
424
+
425
+ // Polling handle
426
+ let genPollInterval = null;
427
+ let shownImages = new Set();
428
+
429
+ // Initialisation
430
+ document.addEventListener('DOMContentLoaded', function() {
431
+ // Add test message
432
+ addMessage('assistant', "Hey there! 👋 \n\nHow can I help you today? Let me know what's on your mind. 😊 \n\nI can:\n\n* **Answer questions:** About pretty much anything!\n* **Generate creative content:** Like poems, code, scripts, musical pieces, email, letters, etc. \n* **Help you brainstorm ideas.**\n* **Just chat!** \n\nJust tell me what you'd like to do.\n\n\n\n ");
433
+ messageInput.focus();
434
+ messageInput.addEventListener('input', function() {
435
+ adjustTextareaHeight();
436
+ updateCharCounter();
437
+ });
438
+ messageInput.addEventListener('keydown', function(e) {
439
+ if (e.key === 'Enter' && !e.shiftKey) {
440
+ e.preventDefault();
441
+ sendMessage();
442
+ }
443
+ });
444
+ checkConnection();
445
+
446
+ function adjustTextareaHeight() {
447
+ messageInput.style.height = 'auto';
448
+ messageInput.style.height = Math.min(messageInput.scrollHeight, 200) + 'px';
449
+ }
450
+
451
+ function updateCharCounter() {
452
+ const length = messageInput.value.length;
453
+ if (length > 1000) {
454
+ charCounter.textContent = `${length}/2000 caractères`;
455
+ charCounter.classList.remove('hidden');
456
+ } else {
457
+ charCounter.classList.add('hidden');
458
+ }
459
+ }
460
+
461
+ async function checkConnection() {
462
+ try {
463
+ const response = await fetch(`${API_BASE_URL}/v1/models`, { method: 'GET' });
464
+ isConnected = response.ok;
465
+ updateConnectionStatus();
466
+ } catch (error) {
467
+ console.error('Erreur de connexion:', error);
468
+ isConnected = false;
469
+ updateConnectionStatus();
470
+ }
471
+ }
472
+
473
+ function updateConnectionStatus() {
474
+ if (isConnected === false) {
475
+ connectionAlert.classList.remove('hidden');
476
+ messageInput.disabled = true;
477
+ messageInput.placeholder = "Connexion requise pour envoyer des messages...";
478
+ } else {
479
+ connectionAlert.classList.add('hidden');
480
+ messageInput.disabled = false;
481
+ messageInput.placeholder = "Tapez votre message ici... (Entrée pour envoyer, Shift+Entrée pour une nouvelle ligne)";
482
+ }
483
+ }
484
+
485
+ // Envoie un message (chat)
486
+ async function sendMessage() {
487
+ const content = messageInput.value.trim();
488
+ if (!content || isLoading || isConnected === false || isGenerating) return;
489
+ addMessage('user', content);
490
+ messageInput.value = '';
491
+ messageInput.style.height = 'auto';
492
+ charCounter.classList.add('hidden');
493
+ if (messages.length === 1) welcomeMessage.classList.add('hidden');
494
+ setLoading(true);
495
+
496
+ const controller = new AbortController();
497
+ const timeoutId = setTimeout(() => controller.abort(), 30000);
498
+
499
+ try {
500
+ const response = await fetch(`${API_BASE_URL}/v1/chat/completions`, {
501
+ method: 'POST',
502
+ headers: {'Content-Type': 'application/json'},
503
+ body: JSON.stringify({
504
+ model: DEFAULT_MODEL,
505
+ messages: messages.map(msg => ({ role: msg.role, content: msg.content })),
506
+ max_tokens: 1000,
507
+ temperature: 0.7
508
+ }),
509
+ signal: controller.signal
510
+ });
511
+ if (!response.ok) throw new Error(`Erreur HTTP: ${response.status}`);
512
+ const data = await response.json();
513
+ let assistantResponse = '';
514
+ if (data.choices && data.choices[0]) {
515
+ if (data.choices[0].message && data.choices[0].message.content) {
516
+ assistantResponse = data.choices[0].message.content;
517
+ } else if (data.choices[0].text) {
518
+ assistantResponse = data.choices[0].text;
519
+ }
520
+ } else if (data.output) assistantResponse = data.output;
521
+ if (!assistantResponse) throw new Error('Réponse vide de l\'API');
522
+ addMessage('assistant', assistantResponse);
523
+ isConnected = true;
524
+ } catch (error) {
525
+ console.error('Erreur lors de l\'envoi du message:', error);
526
+ isConnected = false;
527
+ if (error.name === 'AbortError') {
528
+ addMessage('assistant', 'Désolé, la requête a expiré. Le serveur ne répond pas.');
529
+ showToast('Timeout', 'La requête a expiré après 30 secondes', 'error');
530
+ } else {
531
+ addMessage('assistant', `Désolé, je n'ai pas pu traiter votre message. Erreur : ${error.message}`);
532
+ showToast('Erreur d\'envoi', error.message, 'error');
533
+ }
534
+ } finally {
535
+ clearTimeout(timeoutId);
536
+ setLoading(false);
537
+ }
538
+ }
539
+
540
+ function addMessage(role, content) {
541
+ const message = { role, content, timestamp: new Date() };
542
+ messages.push(message);
543
+ const messageElement = createMessageElement(message);
544
+ messagesList.appendChild(messageElement);
545
+ setTimeout(() => {
546
+ document.getElementById('messagesEnd').scrollIntoView({ behavior: 'smooth' });
547
+ }, 100);
548
+ }
549
+
550
+ function createMessageElement(message) {
551
+ const isUser = message.role === 'user';
552
+ const messageDiv = document.createElement('div');
553
+ messageDiv.className = 'message-appear mb-4';
554
+ messageDiv.innerHTML = `
555
+ <div class="flex gap-1 ${isUser ? 'justify-end' : 'justify-start'}">
556
+ ${!isUser ? `
557
+ <div class="avatar border-2 border-primary bg-gradient-primary text-white">
558
+ <svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
559
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
560
+ </svg>
561
+ </div>
562
+ ` : ''}
563
+
564
+ <div class="message-container ${isUser ? 'user-message order-first' : 'assistant-message'}">
565
+ <div class="card message-card shadow-chat message-bubble ${
566
+ isUser ? 'bg-chat-user text-chat-user-foreground' : 'bg-chat-assistant text-chat-assistant-foreground'
567
+ }">
568
+ <div class="text-sm leading-relaxed whitespace-pre-wrap">
569
+ ${escapeHtml(message.content)}
570
+ </div>
571
+ </div>
572
+
573
+ <div class="mt-0.5 text-xs text-muted-foreground ${isUser ? 'text-right' : 'text-left'}">
574
+ ${isUser ? 'Vous' : 'Jux'}
575
+ </div>
576
+ </div>
577
+
578
+ ${isUser ? `
579
+ <div class="avatar border-2 border-muted bg-secondary">
580
+ <svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
581
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
582
+ </svg>
583
+ </div>
584
+ ` : ''}
585
+ </div>
586
+ `;
587
+ return messageDiv;
588
+ }
589
+
590
+ // Propre échappement HTML basique pour éviter injection
591
+ function escapeHtml(unsafe) {
592
+ return unsafe
593
+ .replace(/&/g, "&amp;")
594
+ .replace(/</g, "&lt;")
595
+ .replace(/>/g, "&gt;")
596
+ .replace(/"/g, "&quot;")
597
+ .replace(/'/g, "&#039;");
598
+ }
599
+
600
+ function setLoading(loading) {
601
+ isLoading = loading;
602
+ if (loading) {
603
+ typingIndicator.classList.remove('hidden');
604
+ sendIcon.classList.add('hidden');
605
+ loadingIcon.classList.remove('hidden');
606
+ sendButton.disabled = true;
607
+ } else {
608
+ typingIndicator.classList.add('hidden');
609
+ sendIcon.classList.remove('hidden');
610
+ loadingIcon.classList.add('hidden');
611
+ sendButton.disabled = false;
612
+ }
613
+ }
614
+
615
+ async function checkModels() {
616
+ try {
617
+ const response = await fetch(`${API_BASE_URL}/v1/models`);
618
+ const data = await response.json();
619
+ if (data.data && Array.isArray(data.data)) {
620
+ const modelCount = data.data.length;
621
+ showToast('Modèles récupérés', `${modelCount} modèle(s) disponible(s)`, 'success');
622
+ isConnected = true;
623
+ updateConnectionStatus();
624
+ } else {
625
+ throw new Error('Format de réponse invalide');
626
+ }
627
+ } catch (error) {
628
+ console.error('Erreur lors de la récupération des modèles:', error);
629
+ showToast('Erreur', error.message, 'error');
630
+ isConnected = false;
631
+ updateConnectionStatus();
632
+ }
633
+ }
634
+
635
+ function newConversation() {
636
+ messages = [];
637
+ messagesList.innerHTML = '';
638
+ welcomeMessage.classList.remove('hidden');
639
+ showToast('Nouvelle conversation', 'Conversation réinitialisée', 'success');
640
+ }
641
+
642
+ function exportConversation() {
643
+ if (messages.length === 0) {
644
+ showToast('Aucune conversation', 'Aucune conversation à exporter', 'error');
645
+ return;
646
+ }
647
+ const exportData = {
648
+ title: `Conversation Jux-Ai - ${new Date().toLocaleDateString('fr-FR')}`,
649
+ model: DEFAULT_MODEL,
650
+ timestamp: new Date().toISOString(),
651
+ messages: messages
652
+ };
653
+ const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
654
+ const url = URL.createObjectURL(blob);
655
+ const a = document.createElement('a');
656
+ a.href = url;
657
+ a.download = `jux-conversation-${Date.now()}.json`;
658
+ document.body.appendChild(a);
659
+ a.click();
660
+ document.body.removeChild(a);
661
+ URL.revokeObjectURL(url);
662
+ showToast('Conversation exportée', 'Le fichier a été téléchargé', 'success');
663
+ }
664
+
665
+ // Toasts
666
+ function showToast(title, message, type = 'info') {
667
+ const toast = document.createElement('div');
668
+ toast.className = `fixed top-4 right-4 z-50 card p-4 shadow-elegant max-w-sm ${
669
+ type === 'error' ? 'border-destructive text-destructive' :
670
+ type === 'success' ? 'border-accent text-accent' : 'border-primary text-primary'
671
+ }`;
672
+ toast.innerHTML = `<div class="font-bold text-sm">${title}</div><div class="text-xs mt-1 opacity-90">${escapeHtml(message)}</div>`;
673
+ document.body.appendChild(toast);
674
+ toast.style.transform = 'translateX(100%)';
675
+ toast.style.transition = 'transform 0.3s ease-out';
676
+ setTimeout(() => { toast.style.transform = 'translateX(0)'; }, 10);
677
+ setTimeout(() => {
678
+ toast.style.transform = 'translateX(100%)';
679
+ setTimeout(() => { document.body.removeChild(toast); }, 300);
680
+ }, 4000);
681
+ }
682
+
683
+ // -----------------------
684
+ // Fonctionnalité de génération d'images
685
+ // -----------------------
686
+
687
+ // Lance la génération en appelant le backend /generate
688
+ async function generateImage() {
689
+ const preset = genPresetSelect.value || 'fast';
690
+ // Utilise le contenu du textarea comme prompt si présent, sinon prompt par défaut
691
+ const promptText = messageInput.value.trim() || 'un paysage de forêt ultra réaliste';
692
+
693
+ // Add user message
694
+ addMessage('user', `Générer image: ${promptText}`);
695
+ if (messages.length === 1) welcomeMessage.classList.add('hidden');
696
+
697
+ // Set generating state
698
+ isGenerating = true;
699
+ messageInput.disabled = true;
700
+ sendButton.disabled = true;
701
+ document.getElementById('generateImageBtn').disabled = true;
702
+
703
+ // Reset preview UI
704
+ generationPreview.innerHTML = '';
705
+ generationStatus.textContent = 'Envoi de la requête de génération...';
706
+
707
+ try {
708
+ const res = await fetch(`${GENERATION_API_BASE}/generate`, {
709
+ method: 'POST',
710
+ headers: { 'Content-Type': 'application/json' },
711
+ body: JSON.stringify({ prompt: promptText, preset })
712
+ });
713
+
714
+ if (!res.ok) {
715
+ const txt = await res.text();
716
+ throw new Error(`Erreur génération: ${res.status} ${txt}`);
717
+ }
718
+
719
+ const data = await res.json();
720
+ const runCode = data.run_code;
721
+ generationStatus.textContent = `Génération démarrée — run: ${runCode}`;
722
+ showToast('Génération démarrée', `run: ${runCode}`, 'success');
723
+
724
+ // Démarrer polling pour ces images
725
+ startPollingGeneration(runCode);
726
+ } catch (err) {
727
+ console.error('Erreur generateImage:', err);
728
+ generationStatus.textContent = `Erreur: ${err.message}`;
729
+ showToast('Erreur génération', err.message, 'error');
730
+ document.getElementById('generateImageBtn').disabled = false;
731
+ }
732
+ }
733
+
734
+ function startPollingGeneration(runCode) {
735
+ // arrête l'ancien polling
736
+ if (genPollInterval) {
737
+ clearInterval(genPollInterval);
738
+ genPollInterval = null;
739
+ }
740
+
741
+ // poll toutes les secondes
742
+ genPollInterval = setInterval(async () => {
743
+ try {
744
+ const metaRes = await fetch(`${GENERATION_API_BASE}/runs/${runCode}/metadata`);
745
+ if (!metaRes.ok) {
746
+ // si pas trouvé, on attend et on continue
747
+ generationStatus.textContent = `Run introuvable (code: ${runCode})`;
748
+ return;
749
+ }
750
+ const meta = await metaRes.json();
751
+ generationStatus.textContent = `Statut: ${meta.status} — mis à jour: ${meta.last_update || '—'}`;
752
+
753
+ // récupérer la liste d'images
754
+ const imgsRes = await fetch(`${GENERATION_API_BASE}/runs/${runCode}/images`);
755
+ if (imgsRes.ok) {
756
+ const imgsJson = await imgsRes.json();
757
+ renderGenerationImages(runCode, imgsJson.images || []);
758
+ }
759
+
760
+ // stop si terminé ou erreur
761
+ if (meta.status === 'done' || meta.status === 'error') {
762
+ clearInterval(genPollInterval);
763
+ genPollInterval = null;
764
+ isGenerating = false;
765
+ messageInput.disabled = false;
766
+ sendButton.disabled = false;
767
+ document.getElementById('generateImageBtn').disabled = false;
768
+ if (meta.status === 'done') showToast('Génération terminée', `run: ${runCode}`, 'success');
769
+ if (meta.status === 'error') showToast('Erreur génération', meta.error || 'Erreur inconnue', 'error');
770
+ }
771
+ } catch (e) {
772
+ console.error('Erreur polling generation:', e);
773
+ generationStatus.textContent = `Erreur polling: ${e.message}`;
774
+ clearInterval(genPollInterval);
775
+ genPollInterval = null;
776
+ document.getElementById('generateImageBtn').disabled = false;
777
+ }
778
+ }, 1000);
779
+ }
780
+
781
+ // Affiche les miniatures et permet d'ouvrir l'image en grand (nouvel onglet)
782
+ function renderGenerationImages(runCode, images) {
783
+ generationPreview.innerHTML = '';
784
+ // images: [{file, mtime}, ...] — tri par mtime
785
+ images.sort((a,b) => (a.mtime || 0) - (b.mtime || 0));
786
+ for (const it of images) {
787
+ const imgSrc = `${GENERATION_API_BASE}/runs/${runCode}/files/${it.file}`;
788
+ const img = document.createElement('img');
789
+ img.src = imgSrc;
790
+ img.alt = it.file;
791
+ img.title = it.file;
792
+ img.onclick = () => window.open(img.src, '_blank');
793
+ generationPreview.appendChild(img);
794
+
795
+ // Add to chat if new
796
+ if (!shownImages.has(it.file)) {
797
+ shownImages.add(it.file);
798
+ addMessage('assistant', `<img src="${imgSrc}" alt="${it.file}" style="max-width:300px;">`);
799
+ }
800
+ }
801
+ }
802
+
803
+ // -----------------------
804
+ // Fin génération d'images
805
+ // -----------------------
806
+
807
+ // Gestion du focus automatique
808
+ document.addEventListener('click', function(e) {
809
+ if (!e.target.closest('#messageInput') && !e.target.closest('button')) {
810
+ setTimeout(() => {
811
+ if (!isLoading && isConnected !== false) {
812
+ messageInput.focus();
813
+ }
814
+ }, 100);
815
+ }
816
+ });
817
+ </script>
818
+ </body>
819
  </html>