Mattimax commited on
Commit
b9150bd
·
verified ·
1 Parent(s): 675b862

Upload 3 files

Browse files
Files changed (3) hide show
  1. app.py +164 -0
  2. requirements.txt +5 -0
  3. templates/index.html +649 -0
app.py ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import torch
3
+ import json
4
+ import os
5
+ from flask import Flask, render_template, request, Response, stream_with_context
6
+ from transformers import AutoTokenizer, AutoModelForCausalLM, TextIteratorStreamer, BitsAndBytesConfig
7
+ from threading import Thread
8
+ import random
9
+ import numpy as np
10
+
11
+ logging.basicConfig(level=logging.INFO)
12
+ app = Flask(__name__)
13
+
14
+ MODEL_NAME = "Mattimax/DATA-AI_Chat_3_0.5B"
15
+ # Controlla se c'è una GPU, altrimenti usa CPU
16
+ device = "cuda" if torch.cuda.is_available() else "cpu"
17
+
18
+ # Imposta seed per reproducibilità
19
+ SEED = 42
20
+ random.seed(SEED)
21
+ np.random.seed(SEED)
22
+ torch.manual_seed(SEED)
23
+ if device == "cuda":
24
+ torch.cuda.manual_seed_all(SEED)
25
+ torch.backends.cudnn.deterministic = True
26
+
27
+ # Configurazione per caricare il modello in modo efficiente
28
+ bnb_config = None
29
+ if device == "cuda":
30
+ bnb_config = BitsAndBytesConfig(
31
+ load_in_4bit=True,
32
+ bnb_4bit_compute_dtype=torch.float16
33
+ )
34
+
35
+ logging.info("Caricamento tokenizer e modello: %s (device=%s)", MODEL_NAME, device)
36
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
37
+
38
+ # Leggi il chat template dal tokenizer config
39
+ chat_template = None
40
+ try:
41
+ from transformers.models.auto.tokenization_auto import get_tokenizer_config
42
+ config_dict = get_tokenizer_config(MODEL_NAME)
43
+ chat_template = config_dict.get("chat_template")
44
+ logging.info("Chat template caricato: %s", chat_template[:100] if chat_template else "Non disponibile")
45
+ except Exception as e:
46
+ logging.warning("Impossibile caricare chat_template: %s", e)
47
+ # Fallback: template di default semplice
48
+ chat_template = "{% for message in messages %}{% if message['role'] == 'user' %}User: {{ message['content'] }}\n{% elif message['role'] == 'assistant' %}Assistant: {{ message['content'] }}\n{% else %}{{ message['role'] }}: {{ message['content'] }}\n{% endif %}{% endfor %}"
49
+
50
+ # assicurati che esista un pad_token
51
+ if tokenizer.pad_token_id is None:
52
+ tokenizer.pad_token_id = tokenizer.eos_token_id
53
+
54
+ if device == "cuda":
55
+ # usa device_map auto per posizionare i pesi sulla GPU in modo efficiente
56
+ model = AutoModelForCausalLM.from_pretrained(
57
+ MODEL_NAME,
58
+ quantization_config=bnb_config,
59
+ device_map="auto"
60
+ )
61
+ else:
62
+ # caricamento su CPU (può essere lento) - evita .to() per device_map compatibile
63
+ model = AutoModelForCausalLM.from_pretrained(MODEL_NAME)
64
+ model.to("cpu")
65
+
66
+ model.eval() # modo valutazione per stabilità
67
+
68
+ # System prompt per guidare il comportamento
69
+ SYSTEM_PROMPT = """Tu sei DAC, un assistente intelligente e amichevole. Rispondi in modo coerente, chiaro e utile.
70
+ Se non conosci la risposta, ammettilo con sincerità. Mantieni il tono professionale ma accessibile."""
71
+
72
+
73
+ @app.route('/')
74
+ def index():
75
+ return render_template('index.html')
76
+
77
+
78
+ @app.route('/chat', methods=['POST'])
79
+ def chat():
80
+ data = request.json or {}
81
+ user_input = data.get("message", "")
82
+ if not user_input:
83
+ return Response(json.dumps({"error": "empty message"}), status=400)
84
+
85
+ # Costruisci il prompt con system message e chat template
86
+ messages = [
87
+ {"role": "system", "content": SYSTEM_PROMPT},
88
+ {"role": "user", "content": user_input}
89
+ ]
90
+
91
+ # Applica il chat template se disponibile
92
+ if chat_template and hasattr(tokenizer, 'apply_chat_template'):
93
+ try:
94
+ prompt_text = tokenizer.apply_chat_template(
95
+ messages,
96
+ tokenize=False,
97
+ add_generation_prompt=True
98
+ )
99
+ except Exception as e:
100
+ logging.warning("Errore applicando chat_template: %s, fallback a prompt semplice", e)
101
+ prompt_text = f"System: {SYSTEM_PROMPT}\nUser: {user_input}\nAssistant:"
102
+ else:
103
+ # Fallback semplice
104
+ prompt_text = f"System: {SYSTEM_PROMPT}\nUser: {user_input}\nAssistant:"
105
+
106
+ logging.info("Prompt generato: %s", prompt_text[:200])
107
+
108
+ streamer = TextIteratorStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True)
109
+
110
+ inputs = tokenizer(prompt_text, return_tensors="pt")
111
+ # sposta gli input sulla GPU se disponibile
112
+ if device == "cuda":
113
+ inputs = {k: v.to("cuda") for k, v in inputs.items()}
114
+
115
+ # Parametri migliorati per stabilità e qualità
116
+ generation_kwargs = dict(
117
+ input_ids=inputs.get("input_ids"),
118
+ attention_mask=inputs.get("attention_mask"),
119
+ streamer=streamer,
120
+ max_new_tokens=2048,
121
+ temperature=0.5, # ridotto per più stabilità
122
+ do_sample=True,
123
+ top_p=0.80, # nucleus sampling per evitare token improbabili
124
+ top_k=40, # limita i candidati ai top-k
125
+ repetition_penalty=1.2, # penalizza ripetizioni
126
+ pad_token_id=tokenizer.pad_token_id,
127
+ eos_token_id=tokenizer.eos_token_id,
128
+ no_repeat_ngram_size=4, # evita n-grammi ripetuti
129
+ early_stopping=False,
130
+ )
131
+
132
+ def run_generate():
133
+ try:
134
+ with torch.no_grad():
135
+ model.generate(**generation_kwargs)
136
+ except Exception as e:
137
+ logging.exception("Errore durante la generazione:")
138
+
139
+ thread = Thread(target=run_generate)
140
+ thread.daemon = True
141
+ thread.start()
142
+
143
+ def generate():
144
+ try:
145
+ # yield streaming tokens in formato SSE
146
+ for new_text in streamer:
147
+ yield f"data: {json.dumps({'token': new_text})}\n\n"
148
+ except GeneratorExit:
149
+ logging.info("Client disconnected dalla stream")
150
+ except Exception:
151
+ logging.exception("Errore nello stream")
152
+
153
+ headers = {
154
+ 'Cache-Control': 'no-cache',
155
+ 'X-Accel-Buffering': 'no'
156
+ }
157
+
158
+ return Response(stream_with_context(generate()), mimetype='text/event-stream', headers=headers)
159
+
160
+
161
+ if __name__ == "__main__":
162
+ # HF Spaces richiede tassativamente la porta 7860
163
+ logging.info("Avvio app su 0.0.0.0:7860")
164
+ app.run(host='0.0.0.0', port=7860, threaded=True)
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ flask
2
+ torch
3
+ transformers
4
+ bitsandbytes
5
+ numpy
templates/index.html ADDED
@@ -0,0 +1,649 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="it">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>DAC M.INC. Interface</title>
7
+ <style>
8
+ :root {
9
+ --bg-dark: #000000;
10
+ --bg-sidebar: rgba(15, 15, 15, 0.95);
11
+ --bg-glass: rgba(255, 255, 255, 0.04);
12
+ /* Gradiente morbido e multicolore */
13
+ --primary-rainbow: linear-gradient(90deg, #4285f4, #9b72cb, #d96570, #f4b400, #4285f4);
14
+ --text-main: #f0f0f0;
15
+ --text-muted: #808080;
16
+ --border-glass: rgba(255, 255, 255, 0.08);
17
+ --shadow-premium: 0 20px 40px rgba(0,0,0,0.6);
18
+ --transition: all 0.5s cubic-bezier(0.19, 1, 0.22, 1);
19
+ }
20
+
21
+ * { margin: 0; padding: 0; box-sizing: border-box; }
22
+
23
+ body {
24
+ font-family: 'Segoe UI', Roboto, sans-serif;
25
+ background-color: var(--bg-dark);
26
+ color: var(--text-main);
27
+ height: 100vh;
28
+ display: flex;
29
+ overflow: hidden;
30
+ padding: 2rem;
31
+ }
32
+
33
+ /* --- HEADER --- */
34
+ .header {
35
+ position: fixed;
36
+ top: 0; left: 0; right: 0;
37
+ height: 60px;
38
+ padding: 0 20px;
39
+ display: flex;
40
+ align-items: center;
41
+ z-index: 1000;
42
+ justify-content: space-between;
43
+ }
44
+
45
+ .new-chat-btn {
46
+ width: 44px; height: 44px;
47
+ display: flex; justify-content: center; align-items: center;
48
+ cursor: pointer;
49
+ background: var(--bg-glass);
50
+ border-radius: 12px;
51
+ border: 1px solid var(--border-glass);
52
+ transition: var(--transition);
53
+ backdrop-filter: blur(5px);
54
+ font-size: 24px;
55
+ color: var(--text-main);
56
+ }
57
+ .new-chat-btn:hover { background: rgba(255,255,255,0.08); transform: scale(1.05); }
58
+
59
+ .menu-btn {
60
+ width: 44px; height: 44px;
61
+ display: flex; flex-direction: column;
62
+ justify-content: center; align-items: center;
63
+ cursor: pointer;
64
+ background: var(--bg-glass);
65
+ border-radius: 12px;
66
+ border: 1px solid var(--border-glass);
67
+ transition: var(--transition);
68
+ backdrop-filter: blur(5px);
69
+ }
70
+
71
+ .menu-btn-line {
72
+ width: 22px; height: 2px;
73
+ background-color: var(--text-main);
74
+ margin: 3px 0;
75
+ transition: var(--transition);
76
+ }
77
+
78
+ .menu-btn.active .menu-btn-line:nth-child(1) { transform: translateY(8px) rotate(45deg); }
79
+ .menu-btn.active .menu-btn-line:nth-child(2) { opacity: 0; }
80
+ .menu-btn.active .menu-btn-line:nth-child(3) { transform: translateY(-8px) rotate(-45deg); }
81
+
82
+ /* --- SIDEBAR --- */
83
+ .sidebar {
84
+ width: 300px;
85
+ background: var(--bg-sidebar);
86
+ backdrop-filter: blur(30px);
87
+ height: 100vh;
88
+ position: fixed;
89
+ left: -300px;
90
+ top: 0;
91
+ z-index: 900;
92
+ transition: var(--transition);
93
+ display: flex;
94
+ flex-direction: column;
95
+ padding: 80px 20px 20px;
96
+ border-right: 1px solid var(--border-glass);
97
+ }
98
+
99
+ .sidebar.open { transform: translateX(300px); }
100
+
101
+ .history-title {
102
+ font-size: 11px;
103
+ text-transform: uppercase;
104
+ color: var(--text-muted);
105
+ letter-spacing: 2px;
106
+ margin-bottom: 25px;
107
+ font-weight: 600;
108
+ }
109
+
110
+ .chat-list { flex: 1; overflow-y: auto; color: var(--text-muted); font-size: 14px; }
111
+
112
+ .profile-section {
113
+ padding: 12px 15px;
114
+ background: var(--bg-glass);
115
+ border-radius: 18px;
116
+ display: flex;
117
+ align-items: center;
118
+ gap: 15px;
119
+ cursor: pointer;
120
+ transition: var(--transition);
121
+ border: 1px solid var(--border-glass);
122
+ }
123
+
124
+ .avatar {
125
+ width: 38px; height: 38px;
126
+ border-radius: 50%;
127
+ background: #222;
128
+ color: #fff;
129
+ font-family: 'Georgia', serif;
130
+ font-size: 20px;
131
+ font-weight: bold;
132
+ display: flex; align-items: center; justify-content: center;
133
+ border: 1px solid rgba(255,255,255,0.1);
134
+ }
135
+
136
+ /* --- LOGO CENTRALE ANIMATO --- */
137
+ .main-content {
138
+ flex: 1;
139
+ display: flex;
140
+ flex-direction: column;
141
+ align-items: center;
142
+ justify-content: center;
143
+ position: relative;
144
+ width: 100%;
145
+ }
146
+
147
+ .logo-container {
148
+ position: relative;
149
+ text-align: center;
150
+ z-index: 10;
151
+ transition: all 1s ease;
152
+ }
153
+
154
+ .logo-container.minimized { top: 30px; transform: scale(0.4) translateY(0); opacity: 0.5; pointer-events: none; }
155
+
156
+ .logo-aura {
157
+ position: absolute;
158
+ top: 50%; left: 50%;
159
+ transform: translate(-50%, -50%);
160
+ width: 200px; height: 200px;
161
+ background: var(--primary-rainbow);
162
+ background-size: 300% 300%;
163
+ filter: blur(70px);
164
+ opacity: 0.25;
165
+ border-radius: 50%;
166
+ animation: flowColors 8s infinite linear; /* Calma */
167
+ }
168
+
169
+ .logo-text {
170
+ font-family: "Arial Black", "Arial Bold", Arial, sans-serif;
171
+ font-weight: 900;
172
+ font-size: 5rem;
173
+ letter-spacing: -3px;
174
+ /* Effetto gradiente nel testo */
175
+ background: var(--primary-rainbow);
176
+ background-size: 300% 300%;
177
+ -webkit-background-clip: text;
178
+ -webkit-text-fill-color: transparent;
179
+ background-clip: text;
180
+ position: relative;
181
+ animation: flowColors 8s infinite linear; /* Calma */
182
+ }
183
+
184
+ /* Stile per la nuova scritta M.INC. */
185
+ .sub-text {
186
+ color: var(--text-muted);
187
+ font-size: 13px; /* Mantiene la stessa grandezza */
188
+ letter-spacing: 5px;
189
+ margin-top: 5px;
190
+ font-weight: 700;
191
+ text-transform: uppercase;
192
+ }
193
+
194
+ @keyframes flowColors {
195
+ 0% { background-position: 0% 50%; }
196
+ 50% { background-position: 100% 50%; }
197
+ 100% { background-position: 0% 50%; }
198
+ }
199
+
200
+ /* CHAT WINDOW */
201
+ #chatWindow {
202
+ width: 100%;
203
+ max-width: 850px;
204
+ flex: 1;
205
+ overflow-y: auto;
206
+ padding: 100px 20px 180px;
207
+ display: flex;
208
+ flex-direction: column;
209
+ gap: 24px;
210
+ scrollbar-width: none;
211
+ }
212
+
213
+ .message { max-width: 85%; line-height: 1.6; font-size: 16px; }
214
+ .user-msg { align-self: flex-end; background: var(--bg-glass); padding: 12px 20px; border-radius: 20px 20px 4px 20px; border: 1px solid var(--border-glass); }
215
+ .bot-msg { align-self: flex-start; width: 100%; }
216
+
217
+ .message-actions {
218
+ display: flex;
219
+ gap: 8px;
220
+ margin-top: 8px;
221
+ font-size: 12px;
222
+ }
223
+ .message-actions button {
224
+ padding: 4px 10px;
225
+ background: rgba(255,255,255,0.1);
226
+ border: 1px solid rgba(255,255,255,0.2);
227
+ border-radius: 8px;
228
+ color: var(--text-main);
229
+ cursor: pointer;
230
+ transition: all 0.2s;
231
+ font-size: 12px;
232
+ }
233
+ .message-actions button:hover { background: rgba(255,255,255,0.15); transform: translateY(-1px); }
234
+
235
+ /* THOUGHT BOX STYLE */
236
+ .thought-box {
237
+ background: rgba(64, 156, 255, 0.12); /* azzurro chiaro */
238
+ border-left: 4px solid rgba(64, 156, 255, 0.9);
239
+ padding: 14px;
240
+ margin: 10px 0;
241
+ border-radius: 14px; /* bordi stondati */
242
+ font-style: italic;
243
+ color: #08354b;
244
+ font-size: 0.95em;
245
+ box-shadow: 0 6px 18px rgba(20,40,60,0.15);
246
+ }
247
+ .thought-header {
248
+ display: block;
249
+ font-size: 11px;
250
+ text-transform: uppercase;
251
+ letter-spacing: 1px;
252
+ margin-bottom: 8px;
253
+ color: var(--text-muted);
254
+ font-weight: bold;
255
+ }
256
+
257
+ /* --- INPUT AREA --- */
258
+ .input-container {
259
+ position: fixed;
260
+ bottom: 35px;
261
+ width: 100%;
262
+ max-width: 780px;
263
+ padding: 0 20px;
264
+ }
265
+
266
+ .input-box {
267
+ background: var(--bg-sidebar);
268
+ border: 1px solid var(--border-glass);
269
+ border-radius: 28px;
270
+ padding: 10px 10px 10px 25px;
271
+ display: flex;
272
+ align-items: center;
273
+ backdrop-filter: blur(20px);
274
+ box-shadow: var(--shadow-premium);
275
+ }
276
+
277
+ textarea {
278
+ flex: 1;
279
+ background: transparent;
280
+ border: none;
281
+ outline: none;
282
+ color: var(--text-main);
283
+ font-size: 16px;
284
+ padding: 12px 0;
285
+ resize: none;
286
+ max-height: 150px;
287
+ font-family: Arial, sans-serif;
288
+ }
289
+
290
+ .send-btn {
291
+ width: 46px; height: 46px;
292
+ background: var(--text-main);
293
+ border-radius: 50%;
294
+ border: none;
295
+ cursor: pointer;
296
+ margin-left: 15px;
297
+ display: flex; align-items: center; justify-content: center;
298
+ transition: var(--transition);
299
+ }
300
+
301
+ .send-btn:hover { transform: scale(1.05); background: #ffffff; }
302
+
303
+ /* Lista chat recenti nella sidebar */
304
+ .chat-list .chat-item {
305
+ padding: 10px 12px;
306
+ border-radius: 10px;
307
+ margin-bottom: 8px;
308
+ cursor: pointer;
309
+ transition: background 0.2s;
310
+ color: var(--text-main);
311
+ background: transparent;
312
+ font-size: 14px;
313
+ overflow: hidden;
314
+ text-overflow: ellipsis;
315
+ white-space: nowrap;
316
+ }
317
+ .chat-list .chat-item:hover { background: rgba(255,255,255,0.02); }
318
+ .chat-list .chat-item .meta { font-size: 11px; color: var(--text-muted); margin-top: 4px; }
319
+
320
+ /* --- CONTENUTI AGGIUNTIVI --- */
321
+ h1 { font-size: 16px; margin-top: 0; }
322
+ p { color: rgb(107, 114, 128); font-size: 15px; margin-bottom: 10px; margin-top: 5px; }
323
+ .card { max-width: 620px; margin: 0 auto; padding: 16px; border: 1px solid lightgray; border-radius: 16px; }
324
+ .card p:last-child { margin-bottom: 0; }
325
+
326
+ </style>
327
+ </head>
328
+ <body>
329
+
330
+ <div class="header">
331
+ <div class="menu-btn" id="hamburger">
332
+ <div class="menu-btn-line"></div>
333
+ <div class="menu-btn-line"></div>
334
+ <div class="menu-btn-line"></div>
335
+ </div>
336
+ <div class="new-chat-btn" id="newChatBtn" title="Nuova chat">+</div>
337
+ </div>
338
+
339
+ <aside class="sidebar" id="sidebar">
340
+ <div class="history-title">Cronologia</div>
341
+ <div class="chat-list">
342
+ <div style="padding: 15px 0; text-align: center; opacity: 0.4; font-style: italic;">Nessuna attività recente</div>
343
+ </div>
344
+
345
+ <div class="profile-section" id="profileBtn">
346
+ <div class="avatar">M</div>
347
+ <div style="flex: 1;">
348
+ <div style="font-size: 14px; font-weight: 600;">Utente Locale</div>
349
+ <div style="font-size: 11px; color: var(--text-muted);">Settings</div>
350
+ </div>
351
+ <span style="font-size: 16px; opacity: 0.6;">⚙️</span>
352
+ </div>
353
+ </aside>
354
+
355
+ <main class="main-content">
356
+ <div class="logo-container" id="logoMain">
357
+ <div class="logo-aura"></div>
358
+ <h1 class="logo-text">DAC</h1>
359
+ <p class="sub-text">M.INC.</p>
360
+ </div>
361
+
362
+ <div id="chatWindow"></div>
363
+
364
+ <div class="input-container">
365
+ <div class="input-box">
366
+ <textarea id="chatInput" placeholder="Chiedi a DAC..." rows="1"></textarea>
367
+ <button class="send-btn" id="sendBtn">
368
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg>
369
+ </button>
370
+ </div>
371
+ </div>
372
+ </main>
373
+
374
+ <script>
375
+ // Elementi DOM principali
376
+ const hamburger = document.getElementById('hamburger');
377
+ const sidebar = document.getElementById('sidebar');
378
+ const chatInput = document.getElementById('chatInput');
379
+ const sendBtn = document.getElementById('sendBtn');
380
+ const chatWindow = document.getElementById('chatWindow');
381
+ const logoMain = document.getElementById('logoMain');
382
+ const chatList = document.querySelector('.chat-list');
383
+ const newChatBtn = document.getElementById('newChatBtn');
384
+
385
+ // Contesto per controllo generazione
386
+ let currentAbortController = null;
387
+ let currentBotDiv = null;
388
+ let isGenerating = false;
389
+ let lastUserMessage = null;
390
+ let lastBotMessage = null;
391
+
392
+ // Toggle sidebar / hamburger animation
393
+ hamburger.addEventListener('click', () => {
394
+ hamburger.classList.toggle('active');
395
+ sidebar.classList.toggle('open');
396
+ });
397
+
398
+ // Chiudi sidebar cliccando il main
399
+ document.querySelector('.main-content').addEventListener('click', () => {
400
+ sidebar.classList.remove('open');
401
+ hamburger.classList.remove('active');
402
+ });
403
+
404
+ // Nuova chat
405
+ newChatBtn.addEventListener('click', () => {
406
+ chatWindow.innerHTML = '';
407
+ chatInput.value = '';
408
+ chatInput.style.height = 'auto';
409
+ logoMain.classList.remove('minimized');
410
+ lastUserMessage = null;
411
+ lastBotMessage = null;
412
+ if (currentAbortController) currentAbortController.abort();
413
+ setSendToStop(false);
414
+ });
415
+
416
+ // Auto-resize textarea
417
+ chatInput.addEventListener('input', function() {
418
+ this.style.height = 'auto';
419
+ this.style.height = (this.scrollHeight) + 'px';
420
+ });
421
+
422
+ // Funzione per inviare messaggi: mantiene lo streaming SSE-like
423
+ async function sendMessage() {
424
+ const message = chatInput.value.trim();
425
+ if (!message) return;
426
+
427
+ await generateResponse(message);
428
+ }
429
+
430
+ async function generateResponse(message) {
431
+ // UI: minimizza logo e reset input
432
+ logoMain.classList.add('minimized');
433
+ chatInput.value = '';
434
+ chatInput.style.height = 'auto';
435
+
436
+ // Mostra messaggio utente
437
+ appendMessage('user', message);
438
+ addRecentChat(message, 'user');
439
+ lastUserMessage = message;
440
+
441
+ // Placeholder per risposta bot
442
+ const botMsgDiv = document.createElement('div');
443
+ botMsgDiv.className = 'message bot-msg';
444
+ chatWindow.appendChild(botMsgDiv);
445
+ currentBotDiv = botMsgDiv;
446
+
447
+ let fullText = "";
448
+
449
+ // Imposta AbortController per poter fermare la richiesta
450
+ if (currentAbortController) {
451
+ try { currentAbortController.abort(); } catch (e) {}
452
+ }
453
+ currentAbortController = new AbortController();
454
+ const signal = currentAbortController.signal;
455
+
456
+ // Cambia il tasto send in stop
457
+ setSendToStop(true);
458
+ isGenerating = true;
459
+
460
+ try {
461
+ const response = await fetch('/chat', {
462
+ method: 'POST',
463
+ headers: { 'Content-Type': 'application/json' },
464
+ body: JSON.stringify({ message: message }),
465
+ signal
466
+ });
467
+
468
+ if (!response.body) {
469
+ const txt = await response.text();
470
+ fullText = txt;
471
+ renderContent(botMsgDiv, fullText);
472
+ addRecentChat(fullText, 'bot');
473
+ return;
474
+ }
475
+
476
+ const reader = response.body.getReader();
477
+ const decoder = new TextDecoder();
478
+
479
+ while (true) {
480
+ const { value, done } = await reader.read();
481
+ if (done) break;
482
+
483
+ const chunk = decoder.decode(value);
484
+ const lines = chunk.split('\n');
485
+
486
+ for (const line of lines) {
487
+ if (!line) continue;
488
+ // supporta formato SSE: "data: {...}"
489
+ if (line.startsWith('data: ')) {
490
+ try {
491
+ const data = JSON.parse(line.slice(6));
492
+ fullText += data.token || '';
493
+ } catch (e) {
494
+ // se non JSON, appende direttamente
495
+ fullText += line.replace(/^data: /, '');
496
+ }
497
+ } else {
498
+ // append plain chunks
499
+ fullText += line;
500
+ }
501
+ renderContent(botMsgDiv, fullText);
502
+ chatWindow.scrollTop = chatWindow.scrollHeight;
503
+ }
504
+ }
505
+
506
+ // Fine generazione
507
+ lastBotMessage = fullText;
508
+ addRecentChat(fullText, 'bot');
509
+ addMessageActions(botMsgDiv);
510
+ } catch (e) {
511
+ if (e.name === 'AbortError') {
512
+ // richiesta annullata dall'utente - il testo rimane visibile
513
+ if (currentBotDiv) {
514
+ if (!currentBotDiv.innerHTML || currentBotDiv.innerHTML.trim() === '') {
515
+ currentBotDiv.innerHTML = '<em>Generazione interrotta dall\'utente.</em>';
516
+ }
517
+ // Aggiungi i pulsanti di azione anche al testo parziale
518
+ addMessageActions(currentBotDiv);
519
+ }
520
+ } else {
521
+ if (currentBotDiv) currentBotDiv.innerHTML = 'Errore di connessione.';
522
+ }
523
+ } finally {
524
+ // ripristina pulsante
525
+ setSendToStop(false);
526
+ isGenerating = false;
527
+ currentAbortController = null;
528
+ }
529
+ }
530
+
531
+ function stopGeneration() {
532
+ if (currentAbortController) {
533
+ currentAbortController.abort();
534
+ }
535
+ // Assicurati che il testo rimanga visibile
536
+ if (currentBotDiv) {
537
+ // Se il div è vuoto, mostra un messaggio
538
+ if (!currentBotDiv.innerHTML || currentBotDiv.innerHTML.trim() === '') {
539
+ currentBotDiv.innerHTML = '<em>Generazione interrotta. Nessun testo generato.</em>';
540
+ }
541
+ }
542
+ }
543
+
544
+ function setSendToStop(flag) {
545
+ if (flag) {
546
+ sendBtn.innerHTML = '✖';
547
+ sendBtn.title = 'Interrompi generazione';
548
+ sendBtn.classList.add('stop-mode');
549
+ sendBtn.removeEventListener('click', sendMessage);
550
+ sendBtn.addEventListener('click', stopGeneration);
551
+ } else {
552
+ sendBtn.innerHTML = '<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg>';
553
+ sendBtn.title = 'Invia';
554
+ sendBtn.classList.remove('stop-mode');
555
+ sendBtn.removeEventListener('click', stopGeneration);
556
+ sendBtn.addEventListener('click', sendMessage);
557
+ }
558
+ }
559
+
560
+ // Rendering che supporta tag <think>
561
+ function renderContent(container, text) {
562
+ let html = text;
563
+
564
+ // Se ci sono tag <think>, trasformali in thought-box
565
+ if (html.includes('<think>')) {
566
+ if (html.includes('</think>')) {
567
+ // Pensiero completato
568
+ html = html.replace(/<think>/g, '<div class="thought-box"><span class="thought-header">💭 Pensiero</span>');
569
+ html = html.replace(/<\/think>/g, '</div><div class="answer-box">');
570
+ html += '</div>';
571
+ } else {
572
+ // Pensiero in corso
573
+ html = html.replace(/<think>/g, '<div class="thought-box"><span class="thought-header">💭 Sto pensando...</span>');
574
+ if (!html.endsWith('</div>')) html += '...</div>';
575
+ }
576
+ }
577
+
578
+ // Escape semplice: se il testo non contiene HTML (o contiene solo le nostre sostituzioni), inseriscilo come testo
579
+ // Qui assumiamo che il modello può emettere HTML intenzionalmente (ad es. <think>), quindi usiamo innerHTML dopo trasformazioni.
580
+ container.innerHTML = html;
581
+ }
582
+
583
+ // Aggiunge una voce nella lista chat recenti
584
+ function addRecentChat(text, role) {
585
+ try {
586
+ const item = document.createElement('div');
587
+ item.className = 'chat-item';
588
+ const snippet = text.replace(/<[^>]+>/g, '');
589
+ item.textContent = (role === 'user' ? 'Tu: ' : 'DAC: ') + snippet.slice(0, 80);
590
+ const meta = document.createElement('div');
591
+ meta.className = 'meta';
592
+ const now = new Date();
593
+ meta.textContent = now.toLocaleTimeString();
594
+ item.appendChild(meta);
595
+ // click per riportare il messaggio nell'input
596
+ item.addEventListener('click', () => {
597
+ chatInput.value = text.slice(0, 400);
598
+ chatInput.dispatchEvent(new Event('input'));
599
+ sidebar.classList.remove('open');
600
+ hamburger.classList.remove('active');
601
+ });
602
+ // inserisci in cima
603
+ if (chatList.firstChild) chatList.insertBefore(item, chatList.firstChild);
604
+ else chatList.appendChild(item);
605
+ // mantieni al massimo 20
606
+ while (chatList.querySelectorAll('.chat-item').length > 20) {
607
+ chatList.removeChild(chatList.lastChild);
608
+ }
609
+ } catch (e) { console.error(e); }
610
+ }
611
+
612
+ // Aggiunge pulsanti di azione al messaggio bot
613
+ function addMessageActions(botMsgDiv) {
614
+ const container = document.createElement('div');
615
+ container.className = 'message-actions';
616
+ const regenBtn = document.createElement('button');
617
+ regenBtn.textContent = '🔄 Rigenera';
618
+ regenBtn.addEventListener('click', () => {
619
+ if (lastUserMessage && !isGenerating) {
620
+ // rimuove il messaggio precedente e rigeneran
621
+ botMsgDiv.remove();
622
+ container.remove();
623
+ generateResponse(lastUserMessage);
624
+ }
625
+ });
626
+ container.appendChild(regenBtn);
627
+ botMsgDiv.parentNode.insertBefore(container, botMsgDiv.nextSibling);
628
+ }
629
+
630
+ function appendMessage(role, text) {
631
+ const div = document.createElement('div');
632
+ div.className = `message ${role}-msg`;
633
+ div.textContent = text;
634
+ chatWindow.appendChild(div);
635
+ chatWindow.scrollTop = chatWindow.scrollHeight;
636
+ }
637
+
638
+ // Invio con click e invio con Enter
639
+ sendBtn.addEventListener('click', sendMessage);
640
+ chatInput.addEventListener('keypress', (e) => {
641
+ if (e.key === 'Enter' && !e.shiftKey) {
642
+ e.preventDefault();
643
+ sendMessage();
644
+ }
645
+ });
646
+
647
+ </script>
648
+ </body>
649
+ </html>