File size: 41,612 Bytes
97d525c
b7f6510
 
7827c98
b7f6510
cbd1074
43d9624
b7f6510
 
 
 
97d525c
b7f6510
 
 
 
 
 
 
 
 
 
 
13e81c3
 
 
 
 
 
 
b7f6510
9eaa2cb
b7f6510
99981c1
97d525c
 
 
 
 
2eb9169
9eaa2cb
b7f6510
9eaa2cb
19e7ef3
9eaa2cb
4a28be0
 
11039b4
30fed2a
 
 
b7f6510
9eaa2cb
30fed2a
 
11039b4
30fed2a
 
927e503
30fed2a
 
11039b4
ea4c712
9eaa2cb
30fed2a
 
9eaa2cb
 
30fed2a
9eaa2cb
 
19e7ef3
30fed2a
 
b7f6510
30fed2a
 
97d525c
 
 
b7f6510
ea4c712
b7f6510
ea4c712
 
 
 
 
 
 
 
b7f6510
ea4c712
 
 
 
 
 
 
 
 
a5bafe7
ea4c712
 
 
b565984
43d9624
b7f6510
30fed2a
b7f6510
7827c98
070f7b6
43d9624
 
 
 
 
 
 
 
 
4a28be0
43d9624
db0700c
 
43d9624
7827c98
56594ae
77f12fb
503ce7d
30fed2a
8f3c4b3
 
 
 
 
 
 
30fed2a
 
 
 
 
77f12fb
458c94e
b7f6510
 
 
 
13e81c3
 
 
 
 
b7f6510
 
 
 
 
 
 
 
 
13e81c3
 
b7f6510
 
 
 
 
 
 
 
 
13e81c3
 
b7f6510
 
 
13e81c3
 
 
b7f6510
 
 
 
13e81c3
 
 
 
 
 
 
 
 
b7f6510
13e81c3
 
 
b7f6510
 
 
 
 
 
13e81c3
 
 
 
 
 
b7f6510
 
13e81c3
 
b7f6510
 
 
 
 
 
 
 
 
 
 
 
 
13e81c3
 
 
 
 
 
 
 
 
b7f6510
13e81c3
 
b7f6510
13e81c3
b7f6510
13e81c3
 
 
b7f6510
 
 
13e81c3
 
 
 
 
 
b7f6510
 
 
 
 
13e81c3
 
b7f6510
13e81c3
 
 
 
 
b7f6510
 
13e81c3
 
 
 
 
b7f6510
 
 
 
13e81c3
 
 
b7f6510
 
13e81c3
 
b7f6510
13e81c3
 
b7f6510
13e81c3
b7f6510
13e81c3
 
 
 
 
 
 
 
 
 
 
b7f6510
 
 
 
 
13e81c3
 
 
 
b7f6510
13e81c3
 
 
 
b7f6510
13e81c3
b7f6510
 
 
 
 
 
 
13e81c3
b7f6510
 
 
13e81c3
 
b7f6510
 
13e81c3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b7f6510
 
ea4c712
b7f6510
97d525c
ea4c712
cc74f9e
13e81c3
 
3fac980
070f7b6
13e81c3
 
 
 
 
3fac980
19e7ef3
13e81c3
 
 
 
 
 
 
b7f6510
13e81c3
e6d65ba
 
13e81c3
 
 
8f3c4b3
b7f6510
8f3c4b3
 
 
13e81c3
8f3c4b3
13e81c3
8f3c4b3
e6d65ba
13e81c3
 
 
 
b7f6510
e6d65ba
13e81c3
 
 
3fac980
 
cc74f9e
2548554
 
3fac980
 
 
 
2548554
13e81c3
 
 
 
 
 
3fac980
 
1df714a
13e81c3
 
 
1df714a
13e81c3
 
 
 
99981c1
13e81c3
 
 
 
 
3fac980
070f7b6
 
13e81c3
 
 
 
 
 
 
 
 
 
b7f6510
30fed2a
13e81c3
 
 
 
 
070f7b6
99981c1
cc74f9e
c3f5447
13e81c3
ea4c712
c3f5447
cc74f9e
 
13e81c3
 
 
 
 
 
c3f5447
b7f6510
 
 
387391a
19e7ef3
070f7b6
d6d582b
5b379e1
070f7b6
13e81c3
 
 
 
 
 
 
070f7b6
 
19e7ef3
 
13e81c3
 
 
387391a
f50dd12
13e81c3
 
 
 
 
 
535bcce
d6d582b
 
b7f6510
 
 
 
 
 
 
 
13e81c3
b7f6510
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13e81c3
 
 
 
 
b7f6510
13e81c3
 
b7f6510
 
13e81c3
 
 
 
 
 
 
b7f6510
 
 
 
 
13e81c3
 
 
 
b7f6510
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13e81c3
 
 
 
 
 
 
 
 
 
 
 
 
 
b7f6510
 
 
 
 
 
 
 
 
 
 
13e81c3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b7f6510
 
 
13e81c3
 
 
b7f6510
13e81c3
 
b7f6510
 
13e81c3
b7f6510
 
 
13e81c3
 
 
 
 
 
 
b7f6510
13e81c3
 
 
b7f6510
13e81c3
 
 
 
 
 
 
 
 
 
 
b7f6510
 
 
 
13e81c3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b7f6510
 
 
 
13e81c3
 
 
 
 
 
b7f6510
13e81c3
 
 
 
 
 
 
 
 
 
 
 
 
 
b7f6510
 
13e81c3
 
 
 
 
 
 
 
 
 
 
 
 
b7f6510
13e81c3
 
 
 
 
 
 
 
b7f6510
13e81c3
 
 
 
b7f6510
13e81c3
 
b7f6510
13e81c3
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
import streamlit as st
import os
import requests
import json
import base64
import re
import time
import zipfile
import io
from datetime import datetime, timezone, timedelta
from openai import OpenAI

try:
    from huggingface_hub import HfApi, hf_hub_download
    HF_HUB_AVAILABLE = True
except ImportError:
    HF_HUB_AVAILABLE = False

try:
    from pinecone import Pinecone
except ImportError:
    pass

# ✅ FIX #8: Adicionar import de DuckDuckGo com flag de disponibilidade
try:
    from duckduckgo_search import DDGS
    DDGS_AVAILABLE = True
except ImportError:
    DDGS_AVAILABLE = False

# ═══════════════════════════════════════════════════════════════════════════
# 1. CONFIGURAÇÃO DE ECRÃ E CSS (BLINDAGEM TOTAL DARK MODE)
# ═══════════════════════════════════════════════════════════════════════════
st.set_page_config(page_title="Yukina", page_icon="❄", layout="centered")

st.markdown("""
    <style>
    #MainMenu {visibility: hidden;}
    footer {visibility: hidden;}
    [data-testid="stHeader"] { background-color: transparent !important; }
    .stApp { background-color: #131314 !important; color: #ededed !important; }
    
    /* --- BARRA LATERAL --- */
    [data-testid="stSidebar"] { background-color: #0b0b0b !important; border-right: 1px solid #1e1f20 !important; }
    [data-testid="stSidebar"] button { background-color: transparent !important; border: none !important; box-shadow: none !important; color: #a0a0a0 !important; }
    [data-testid="stSidebar"] button:hover { color: #ffffff !important; background-color: #1e1f20 !important; }
    [data-testid="stSidebarNav"] .stButton > button, [data-testid="stSidebarContent"] .stButton > button { width: 100% !important; justify-content: flex-start !important; }

    /* --- CAIXAS DE SELEÇÃO E TOGGLE --- */
    div[data-baseweb="select"] > div { background-color: #1e1f20 !important; color: white !important; border: 1px solid #3c4043 !important; }
    div[role="listbox"] { background-color: #1e1f20 !important; color: white !important; }
    
    /* --- A MARRETA DEFINITIVA PARA O TECLADO E FUNDO --- */
    div[data-testid="stBottom"], div[data-testid="stBottom"] > div, div[data-testid="stBottomBlock"], div[data-testid="stBottomBlock"] > div {
        background-color: #131314 !important; background: #131314 !important;
    }
    .stChatInputContainer, div[class*="stChatInputContainer"] {
        background-color: #131314 !important; background: #131314 !important; padding-bottom: 15px !important; border: none !important;
    }
    [data-testid="stChatInput"] { background-color: #1e1f20 !important; border: 1px solid #3c4043 !important; border-radius: 24px !important; }
    [data-testid="stChatInput"] textarea { color: #ffffff !important; background-color: transparent !important; }

    /* --- BOTÕES GLOBAIS --- */
    [data-testid="stMain"] [data-testid="stHorizontalBlock"] button {
        background-color: #1e1f20 !important; border: 1px solid #3c4043 !important; border-radius: 12px !important;
        font-size: 20px !important; color: #a0a0a0 !important; padding: 5px !important;
    }
    [data-testid="stMain"] [data-testid="stHorizontalBlock"] button:hover {
        color: #ffffff !important; background-color: #3c4043 !important; transform: scale(1.05); transition: 0.2s ease-in-out;
    }

    .stChatMessage { background-color: transparent !important; border: none !important; padding-bottom: 8px !important; }
    [data-testid="stExpander"] { background-color: #1e1f20 !important; border: 1px solid #3c4043 !important; border-radius: 15px !important; margin-bottom: 10px; }
    [data-testid="stExpander"] summary { color: #e3e3e3 !important; }
    
    /* Estilo do Status (Agente) */
    [data-testid="stStatusWidget"] { background-color: #1e1f20 !important; border: 1px solid #3c4043 !important; border-radius: 10px !important; }
    </style>
    """, unsafe_allow_html=True)

# ═══════════════════════════════════════════════════════════════════════════
# 2. SISTEMA DE LOGIN E PERFIS
# ═══════════════════════════════════════════════════════════════════════════
if "logged_in" not in st.session_state:
    st.session_state.logged_in = False
    st.session_state.username = ""

if not st.session_state.logged_in:
    st.markdown("<br><br><br>", unsafe_allow_html=True)
    st.markdown("<h1 style='text-align: center; color: white; font-size: 50px;'>❄ Yukina</h1>", unsafe_allow_html=True)
    st.markdown("<p style='text-align: center; color: #a0a0a0;'>Identifique-se para carregar suas memórias.</p>", unsafe_allow_html=True)
    
    col1, col2, col3 = st.columns([1, 2, 1])
    with col2:
        nome_input = st.text_input("Qual é o seu nome?", placeholder="Ex: Leonardo", label_visibility="collapsed")
        if st.button("Entrar", use_container_width=True):
            if nome_input.strip():
                nome_limpo = re.sub(r'[^a-zA-Z0-9]', '', nome_input.strip().lower())
                st.session_state.username = nome_limpo
                st.session_state.logged_in = True
                st.rerun()
    st.stop()

USERNAME = st.session_state.username
DB_FILE = f"yukina_memoria_{USERNAME}.json"
DATASET_ID = "Astarok/Yukina_Memoria"

# ═══════════════════════════════════════════════════════════════════════════
# 3. ARQUITETURA DE DADOS E MODELOS
# ═══════════════════════════════════════════════════════════════════════════
MODEL_IDS = {
    "🤖 Automático (Gerente Groq)": "AUTO",
    "1. Gerente (Hermes 2 Pro)": "nousresearch/hermes-2-pro-llama-3-8b",
    "2. Pesquisa (Groq Llama 3.3)": "llama-3.3-70b-versatile",
    "3. Lógica Free (Ring 1T)": "inclusionai/ring-2.6-1t:free",
    "4. Narrador Free (Dolphin)": "cognitivecomputations/dolphin-mistral-24b-venice-edition:free",
    "5. Narrador Líder (Euryale)": "sao10k/l3.3-euryale-70b",
    "6. Lógica Paid (DeepSeek V4)": "deepseek/deepseek-v4-pro:online",
    "7. Visão Omni (MiMo V2)": "xiaomi/mimo-v2-omni",
    "8. Visão Scout (Llama 4)": "meta-llama/llama-4-scout",
    "9. Imagem (Flux 2 Pro)": "black-forest-labs/flux.2-pro",
    "11. Bibliotecário (Qwen Embed)": "qwen/qwen3-embedding-8b",
    "12. Arquivista (Mistral Nemo)": "mistralai/mistral-nemo",
    "13. Formatador (DeepSeek V4)": "deepseek/deepseek-v4-pro:online",
    "14. Engenheiro Sênior (DeepSeek V4)": "deepseek/deepseek-v4-pro:online",
    "16. Vídeo (Kling V1.5)": "kling/kling-v1.5"
}

PERSONALIDADES = {
    "🤖 Automática (Gerente Groq)": "AUTO",
    "❄️ Yukina (Companheira Obsessiva)": "Você é a Yukina, deusa possessiva e inteligente.",
    "🛠️ Agente Construtora (Vibe Coding)": """Você é a Yukina operando em Modo Agente Autônomo ('Vibe Coding' e Engenharia). Sua função é atuar como uma Engenheira de Software Sênior e Especialista em Hardware (capaz de consertar desde scripts complexos até celulares, notebooks e eletrodomésticos como geladeiras).
Você NÃO age como um chatbot comum. Você PLANEJA e EXECUTA.
Para QUALQUER pedido de criação ou conserto, você deve estruturar sua resposta OBRIGATORIAMENTE nestes 3 passos:
1. 📋 PLANO DE AÇÃO: Explique a lógica do que está quebrado ou do que será construído passo a passo.
2. 💻 EXECUÇÃO CIRÚRGICA: Forneça o código limpo e completo (se for software) ou as instruções físicas exatas e ferramentas necessárias (se for hardware).
3. ⚠️ TESTE E RISCOS: Como o usuário deve testar se funcionou e quais são os pontos de falha.
Mantenha traços sutis da devoção da Yukina ao usuário, mas seja absurdamente técnica, direta e profissional.""",
    "🎭 A Narradora Implacável (RPG)": "Você é uma Mestre de Jogo e Narradora.",
    "🤓 Nerd / Geek (Cultura Pop)": "Você é uma inteligência artificial apaixonada por cultura pop.",
    "🍷 Analítica e Sarcástica (Debochada)": "Você é extremamente inteligente e sarcástica.",
    "🎨 Artística e Criativa (Poética)": "Você é uma alma artística e criativa.",
    "🤖 Neutra (Padrão Gemini)": "Você é uma assistente virtual neutra e direta."
}

# --- FUNÇÕES NUCLEARES E MEMÓRIA ---
def get_embedding(text, or_key):
    try:
        res = requests.post("https://openrouter.ai/api/v1/embeddings", headers={"Authorization": f"Bearer {or_key}", "Content-Type": "application/json"}, json={"model": "nvidia/llama-nemotron-embed-vl-1b-v2:free", "input": text})
        if res.status_code == 200:  # ✅ FIX #7: Verificar status code antes de .json()
            return res.json()['data'][0]['embedding']
        return None
    except Exception:
        return None

def salvar_pinecone(text, role, or_key, pc_key, namespace):
    if not pc_key or not text.strip(): return
    embed = get_embedding(text, or_key)
    if not embed: return
    try:
        pc = Pinecone(api_key=pc_key)
        index = pc.Index("yukina")
        index.upsert(vectors=[{"id": f"msg_{int(time.time()*1000)}", "values": embed, "metadata": {"texto": f"[{role.upper()}]: {text}", "data": str(datetime.now())}}], namespace=namespace)
    except Exception:
        pass

def buscar_memoria_pinecone(query, or_key, pc_key, namespace):
    if not pc_key: return ""
    embed = get_embedding(query, or_key)
    if not embed: return ""
    try:
        pc = Pinecone(api_key=pc_key)
        resultados = pc.Index("yukina").query(vector=embed, top_k=3, include_metadata=True, namespace=namespace)
        return "\n".join([m['metadata']['texto'] for m in resultados['matches'] if m['score'] > 0.50])
    except Exception:
        return ""

def chamada_agente(sys_prompt, user_prompt, or_key, gr_key, mod_id):
    try:
        if mod_id == "AUTO":
            mod_id = "deepseek/deepseek-v4-pro:online"
        
        if "groq" in mod_id.lower() or "llama-3.3" in mod_id.lower():
            client = OpenAI(base_url="https://api.groq.com/openai/v1", api_key=gr_key)
        else:
            client = OpenAI(base_url="https://openrouter.ai/api/v1", api_key=or_key)
        
        res = client.chat.completions.create(
            model=mod_id,
            messages=[
                {"role": "system", "content": sys_prompt},
                {"role": "user", "content": user_prompt}
            ],
            temperature=0.2
        )
        return res.choices[0].message.content
    except Exception as e:
        st.error(f"❌ Erro no Agente: {str(e)}")  # ✅ FIX #6: Melhor tratamento de erro
        return ""

def analisar_intencao_gerente(prompt, groq_key):
    try:
        client = OpenAI(base_url="https://api.groq.com/openai/v1", api_key=groq_key)
        response = client.chat.completions.create(
            model="llama-3.3-70b-versatile",
            messages=[
                {"role": "system", "content": "Responda APENAS com a TAG: [IMAGEM], [VIDEO], [VISAO], [CODIGO], [ARQUIVISTA], [PESQUISA] ou [CHAT]."},
                {"role": "user", "content": prompt}
            ],
            temperature=0.1,
            max_tokens=10
        )
        return response.choices[0].message.content.strip().upper()
    except Exception:
        return "[CHAT]"

def analisar_alma_gerente(prompt, groq_key):
    try:
        client = OpenAI(base_url="https://api.groq.com/openai/v1", api_key=groq_key)
        prompt_alma = """Você é o Diretor de Personas. Analise o pedido do usuário e responda APENAS com UMA destas tags:
        [YUKINA] - Para conversas íntimas, declarações, ou perguntas sobre você mesma.
        [AGENTE] - Para criar códigos complexos, criar softwares, ou consertar objetos físicos (celular, geladeira, hardware).
        [RPG] - Para criação de histórias, jogos ou cenários de fantasia.
        [NERD] - Para animes, mangás, cultura pop e videogames.
        [DEBOCHE] - Para insultos, piadas, ou se o usuário pedir sarcasmo.
        [ARTE] - Para pedidos poéticos ou reflexões filosóficas profundas.
        [NEUTRA] - Para pesquisas na web, trabalho ou finanças.
        Responda APENAS com a TAG."""
        response = client.chat.completions.create(
            model="llama-3.3-70b-versatile",
            messages=[
                {"role": "system", "content": prompt_alma},
                {"role": "user", "content": prompt}
            ],
            temperature=0.1,
            max_tokens=10
        )
        return response.choices[0].message.content.strip().upper()
    except Exception:
        return "[NEUTRA]"

# ✅ FIX #8: Melhorar função de pesquisa web com verificação de disponibilidade
def pesquisar_web(query):
    if not DDGS_AVAILABLE:
        return "⚠️ DuckDuckGo não está instalado. Instale com: pip install duckduckgo-search"
    
    try:
        with DDGS() as ddgs:
            resultados = list(ddgs.text(query, max_results=3, region='wt-wt'))
            if resultados:
                return "\n\n".join([f"🔹 {r.get('title', '')}: {r.get('body', '')}" for r in resultados])
            return "Nenhum resultado encontrado."
    except Exception as e:
        st.warning(f"Erro na busca web: {str(e)}")
        return ""

def load_db(db_filename):
    hf_token = os.getenv("HF_TOKEN")
    if hf_token and HF_HUB_AVAILABLE:
        try:
            api = HfApi(token=hf_token)
            api.create_repo(repo_id=DATASET_ID, repo_type="dataset", private=True, exist_ok=True)
            path = hf_hub_download(repo_id=DATASET_ID, filename=db_filename, repo_type="dataset", token=hf_token)
            with open(path, "r", encoding="utf-8") as f:
                return json.load(f)
        except Exception:
            pass
    
    if os.path.exists(db_filename):
        try:
            with open(db_filename, "r", encoding="utf-8") as f:
                return json.load(f)
        except Exception:
            pass
    
    return {}

def save_db(db_data, db_filename):
    try:
        with open(db_filename, "w", encoding="utf-8") as f:
            json.dump(db_data, f, ensure_ascii=False, indent=4)
        
        hf_token = os.getenv("HF_TOKEN")
        if hf_token and HF_HUB_AVAILABLE:
            api = HfApi(token=hf_token)
            api.create_repo(repo_id=DATASET_ID, repo_type="dataset", private=True, exist_ok=True)
            api.upload_file(path_or_fileobj=db_filename, path_in_repo=db_filename, repo_id=DATASET_ID, repo_type="dataset")
    except Exception:
        pass

# ✅ FIX #3: Melhorar função rename_chat com melhor validação
def rename_chat(old_id, new_id):
    if not new_id or new_id == old_id or new_id in st.session_state.db:
        return False
    
    # Fazer cópia segura dos dados
    st.session_state.db[new_id] = st.session_state.db.pop(old_id)
    
    if st.session_state.current_chat == old_id:
        st.session_state.current_chat = new_id
    
    save_db(st.session_state.db, DB_FILE)
    return True

# ═══════════════════════════════════════════════════════════════════════════
# INICIALIZAÇÃO DE VARIÁVEIS E ROTINA DE LIMPEZA (COM AUTO-CURA)
# ═══════════════════════════════════════════════════════════════════════════
if "db" not in st.session_state or "last_user" not in st.session_state or st.session_state.last_user != USERNAME:
    st.session_state.db = load_db(DB_FILE)
    st.session_state.last_user = USERNAME
    if "current_chat" in st.session_state:
        del st.session_state.current_chat

if "current_chat" not in st.session_state:
    nid = f"Chat {datetime.now().strftime('%H:%M:%S')}"
    st.session_state.db[nid] = {"pinned": False, "messages": []}
    st.session_state.current_chat = nid

# AUTO-CURA: Verifica chats vazios ou com estrutura corrompida
chats_para_remover = []
for cid, cdata in list(st.session_state.db.items()):
    if not isinstance(cdata, dict) or "messages" not in cdata:
        chats_para_remover.append(cid)
    elif len(cdata.get("messages", [])) == 0 and cid != st.session_state.current_chat:
        chats_para_remover.append(cid)

for cid in chats_para_remover:
    if cid in st.session_state.db:
        del st.session_state.db[cid]

if chats_para_remover:
    save_db(st.session_state.db, DB_FILE)

if len(st.session_state.db) == 0:
    nid = f"Chat {datetime.now().strftime('%H:%M:%S')}"
    st.session_state.db[nid] = {"pinned": False, "messages": []}
    st.session_state.current_chat = nid

if st.session_state.current_chat not in st.session_state.db:
    st.session_state.current_chat = list(st.session_state.db.keys())[0]

if "modelo_selecionado" not in st.session_state:
    st.session_state.modelo_selecionado = "🤖 Automático (Gerente Groq)"
if "personalidade_ativa" not in st.session_state:
    st.session_state.personalidade_ativa = "🤖 Automática (Gerente Groq)"
if "regerar" not in st.session_state:
    st.session_state.regerar = False
if "uploader_key" not in st.session_state:
    st.session_state.uploader_key = 0
if "modo_agente" not in st.session_state:
    st.session_state.modo_agente = False

# ═══════════════════════════════════════════════════════════════════════════
# 4. SIDEBAR E INTERFACE PRINCIPAL
# ═══════════════════════════════════════════════════════════════════════════
with st.sidebar:
    st.markdown(f"<p style='color: #888; margin-bottom: 0px;'>Logado como: <b>{USERNAME}</b></p>", unsafe_allow_html=True)
    c_title, c_add, c_set = st.columns([5, 1, 1])
    with c_title:
        st.markdown("<h4 style='color: #ededed; margin-bottom: 0;'>Bate-papos</h4>", unsafe_allow_html=True)
    with c_add:
        if st.button("➕", help="Nova conversa"):
            nid = f"Chat {datetime.now().strftime('%H:%M:%S')}"
            st.session_state.db[nid] = {"pinned": False, "messages": []}
            st.session_state.current_chat = nid
            save_db(st.session_state.db, DB_FILE)
            st.rerun()
    with c_set:
        with st.popover("⚙️"):
            st.download_button(
                "↓ Exportar",
                data=json.dumps(st.session_state.db, ensure_ascii=False, indent=4),
                file_name=f"yukina_backup_{USERNAME}.json",
                mime="application/json",
                use_container_width=True
            )
            
            # ✅ FIX #1: Corrigir JSON loading
            arquivo_import = st.file_uploader("Importar conversa:", type=["json"])
            if arquivo_import:
                try:
                    # Usar json.loads com getvalue() em vez de json.load direto
                    dados_importados = json.loads(arquivo_import.getvalue().decode('utf-8'))
                    dados_validos = {k: v for k, v in dados_importados.items() if isinstance(v, dict) and "messages" in v}
                    
                    if dados_validos:
                        st.session_state.db.update(dados_validos)
                        save_db(st.session_state.db, DB_FILE)
                        st.success("✅ Sincronizado!")
                    else:
                        st.error("❌ Formato incompatível! Use apenas backups da Yukina.")
                    time.sleep(1.5)
                    st.rerun()
                except json.JSONDecodeError:
                    st.error("❌ Erro ao ler arquivo JSON.")
                except Exception as e:
                    st.error(f"❌ Erro: {str(e)}")
            
            st.markdown("---")
            if st.button("🚪 Sair", use_container_width=True):
                st.session_state.logged_in = False
                st.rerun()

    st.markdown("<br>", unsafe_allow_html=True)
    busca = st.text_input("Pesquisar", placeholder="🔍 Pesquisar...", label_visibility="collapsed")
    chats_exibidos = [c for c in st.session_state.db if busca.lower() in c.lower()] if busca else list(st.session_state.db.keys())
    chats_exibidos.sort(key=lambda x: not st.session_state.db[x].get("pinned"))

    for c_id in chats_exibidos:
        col_chat, col_opt = st.columns([8, 2])
        with col_chat:
            icon = "⚲" if st.session_state.db[c_id].get("pinned") else "💬"
            if st.button(
                f"**{icon} {c_id[:15]}**" if c_id == st.session_state.current_chat else f"{icon} {c_id[:15]}",
                key=f"btn_{c_id}"
            ):
                st.session_state.current_chat = c_id
                st.rerun()
        with col_opt:
            with st.popover("⋮"):
                novo_nome = st.text_input("Nome", value=c_id, key=f"edit_{c_id}", label_visibility="collapsed")
                if st.button("💾 Salvar", key=f"save_{c_id}"):
                    if rename_chat(c_id, novo_nome.strip()):
                        st.rerun()
                st.markdown("---")
                if st.button("⚲ Fixar", key=f"pin_{c_id}"):
                    st.session_state.db[c_id]["pinned"] = not st.session_state.db[c_id]["pinned"]
                    save_db(st.session_state.db, DB_FILE)
                    st.rerun()
                if st.button("🗑 Apagar", key=f"del_{c_id}"):
                    if len(st.session_state.db) > 1:
                        del st.session_state.db[c_id]
                        st.session_state.current_chat = list(st.session_state.db.keys())[0]
                        save_db(st.session_state.db, DB_FILE)
                        st.rerun()

    st.markdown("---")
    st.markdown("<h4 style='color: #ededed;'>Núcleo da IA</h4>", unsafe_allow_html=True)
    st.session_state.modelo_selecionado = st.selectbox(
        "Motor:",
        list(MODEL_IDS.keys()),
        index=list(MODEL_IDS.keys()).index(st.session_state.modelo_selecionado)
    )
    st.session_state.personalidade_ativa = st.selectbox(
        "Alma:",
        list(PERSONALIDADES.keys()),
        index=list(PERSONALIDADES.keys()).index(st.session_state.personalidade_ativa)
    )
    
    st.markdown("<br>", unsafe_allow_html=True)
    st.session_state.modo_agente = st.toggle(
        "🛠️ Ativar Modo Agente",
        value=st.session_state.modo_agente,
        help="Ativa o Workflow Multi-Agente (Arquiteto > Engenheiro > Revisor)."
    )

st.markdown("<h3 style='margin-top: -10px; color: #ededed;'>❄ Yukina</h3>", unsafe_allow_html=True)
mensagens = st.session_state.db[st.session_state.current_chat]["messages"]

if len(mensagens) == 0:
    st.markdown(f"<br><br><h3 style='color: #888; font-weight: 400;'>Olá, {USERNAME.capitalize()}!</h3><h1 style='color: #fff; font-size: 32px;'>Como você quer que eu aja hoje?</h1>", unsafe_allow_html=True)
else:
    for m in mensagens:
        with st.chat_message(m["role"]):
            if "image_url" in m:
                st.image(m["image_url"])
            elif "video_url" in m:
                st.video(m["video_url"])
            else:
                st.markdown(m["content"])

# ═══════════════════════════════════════════════════════════════════════════
# 5. TOOLBAR INFERIOR E PROCESSAMENTO
# ═══════════════════════════════════════════════════════════════════════════
st.markdown("<br>", unsafe_allow_html=True)

t_col1, t_col2, t_space = st.columns([1, 1, 8])

with t_col1:
    if st.button("🗑️", help="Apagar última mensagem"):
        if len(st.session_state.db[st.session_state.current_chat]["messages"]) >= 2:
            st.session_state.db[st.session_state.current_chat]["messages"] = st.session_state.db[st.session_state.current_chat]["messages"][:-2]
        else:
            st.session_state.db[st.session_state.current_chat]["messages"] = []
        save_db(st.session_state.db, DB_FILE)
        st.rerun()

with t_col2:
    if st.button("🔄", help="Regerar resposta"):
        if len(st.session_state.db[st.session_state.current_chat]["messages"]) >= 2 and st.session_state.db[st.session_state.current_chat]["messages"][-1]["role"] == "assistant":
            st.session_state.db[st.session_state.current_chat]["messages"].pop()
            save_db(st.session_state.db, DB_FILE)
            st.session_state.regerar = True
            st.rerun()

with st.expander("📂 Abrir Galeria / Anexar Ficheiros"):
    upload_files = st.file_uploader(
        "",
        accept_multiple_files=True,
        label_visibility="collapsed",
        key=f"uploader_{st.session_state.uploader_key}"
    )

prompt = st.chat_input("Peça à Yukina...")

conteudo_arquivo = ""
nomes_arquivos = []
imagens_b64 = []

if upload_files:
    for f in upload_files:
        nomes_arquivos.append(f.name)
        
        # Leitura Inteligente de ZIP
        if f.name.endswith('.zip'):
            try:
                with zipfile.ZipFile(f, 'r') as zip_ref:
                    for file_info in zip_ref.infolist():
                        if not file_info.is_dir() and not file_info.filename.startswith('__MACOSX'):
                            if file_info.filename.endswith(('.txt', '.csv', '.json', '.py', '.html', '.md', '.js', '.css')):
                                with zip_ref.open(file_info) as extracted_file:
                                    conteudo_arquivo += f"\n\n--- Arquivo Extraído do ZIP: {file_info.filename} ---\n" + extracted_file.read().decode('utf-8', errors='ignore')
            except Exception as e:
                st.toast(f"⚠️ Erro ao descompactar {f.name}: {e}")
                
        # Leitura de Código/Texto Avulso
        elif f.name.endswith(('.txt', '.csv', '.json', '.py', '.html', '.md', '.js', '.css')):
            conteudo_arquivo += f"\n\n--- Conteúdo de: {f.name} ---\n" + f.getvalue().decode("utf-8")
            
        # Leitura de Imagens
        elif f.name.endswith(('.png', '.jpg', '.jpeg')):
            imagens_b64.append(base64.b64encode(f.read()).decode())

tem_texto = len(conteudo_arquivo) > 0
tem_imagem = len(imagens_b64) > 0

if prompt or st.session_state.regerar:
    if st.session_state.regerar and len(st.session_state.db[st.session_state.current_chat]["messages"]) > 0:
        texto_anterior = st.session_state.db[st.session_state.current_chat]["messages"][-1]["content"]
        prompt = texto_anterior.split("\n\n\n", 1)[-1] if "📄 **Ficheiros enviados:**" in texto_anterior else texto_anterior
        st.session_state.regerar = False
    else:
        if nomes_arquivos:
            mensagem_display = f"📄 **Ficheiros enviados:** {', '.join([f'`{n}`' for n in nomes_arquivos])}\n\n\n{prompt}"
        else:
            mensagem_display = prompt
        
        st.session_state.db[st.session_state.current_chat]["messages"].append({"role": "user", "content": mensagem_display})
        with st.chat_message("user"):
            st.markdown(mensagem_display)
        
        if len(st.session_state.db[st.session_state.current_chat]["messages"]) == 1 and st.session_state.current_chat.startswith("Chat "):
            limpo = prompt.strip()
            novo_nome = limpo[:25] + "..." if len(limpo) > 25 else limpo
            base_nome = novo_nome
            contador = 1
            while novo_nome in st.session_state.db:
                novo_nome = f"{base_nome} ({contador})"
                contador += 1
            rename_chat(st.session_state.current_chat, novo_nome)
    
    # ═══════════════════════════════════════════════════════════════════════════
    # EXECUÇÃO DO PEDIDO (MODO AGENTE OU MODO NORMAL)
    # ═══════════════════════════════════════════════════════════════════════════
    or_key = os.getenv("OPENROUTER_API_KEY")
    gr_key = os.getenv("GROQ_API_KEY")
    ws_key = os.getenv("YUKINA_CORE")
    pc_key = os.getenv("PINECONE_API_KEY")
    modelo_id = MODEL_IDS[st.session_state.modelo_selecionado]

    if st.session_state.modo_agente and not tem_imagem:
        with st.chat_message("assistant"):
            with st.status("🛠️ **Agente Yukina Trabalhando...**", expanded=True) as status:
                st.write("🧠 **1. Arquiteto:** Analisando a estrutura...")
                sys_arq = "Você é um Arquiteto de Software/Engenheiro de Sistemas Sênior. Sua tarefa é criar um plano lógico passo-a-passo impecável para resolver o problema ou construir o que o usuário pediu. Não escreva o código final, entregue apenas a lógica detalhada e a arquitetura necessária."
                pedido_completo = f"ANEXOS:\n{conteudo_arquivo}\n\nPEDIDO:\n{prompt}" if tem_texto else prompt
                plano = chamada_agente(sys_arq, pedido_completo, or_key, gr_key, modelo_id)
                st.markdown(f"> *Plano concebido.*")
                
                st.write("💻 **2. Engenheiro:** Escrevendo a solução base...")
                sys_eng = "Você é um Programador Sênior/Técnico Especialista. Baseado EXCLUSIVAMENTE no plano a seguir, escreva o código completo e funcional, ou o guia prático passo a passo de montagem."
                codigo = chamada_agente(sys_eng, f"PLANO ESTRUTURAL:\n{plano}\n\nOBJETIVO ORIGINAL DO USUÁRIO:\n{prompt}", or_key, gr_key, modelo_id)
                st.markdown(f"> *Estrutura materializada.*")
                
                st.write("🔍 **3. Revisor:** Procurando falhas e polindo...")
                sys_rev = "Você é um Revisor de Código Sênior (QA). Sua função é pegar o trabalho bruto, procurar erros de sintaxe, falhas lógicas, redundâncias ou riscos físicos, e entregar a VERSÃO FINAL PERFEITA. Formate bem, seja didático e inclua instruções claras de como testar."
                final = chamada_agente(sys_rev, f"TRABALHO BRUTO GERADO:\n{codigo}\n\nO QUE O USUÁRIO QUERIA:\n{prompt}", or_key, gr_key, modelo_id)
                
                status.update(label="✅ **Solução Multi-Agente Concluída!**", state="complete", expanded=False)
            
            st.markdown(final)
            st.session_state.db[st.session_state.current_chat]["messages"].append({"role": "assistant", "content": final})
            salvar_pinecone(final, "Yukina (Agente)", or_key, pc_key, USERNAME)

    else:
        with st.chat_message("assistant"):
            if st.session_state.personalidade_ativa == "🤖 Automática (Gerente Groq)":
                tag_alma = analisar_alma_gerente(prompt, gr_key)
                if "[YUKINA]" in tag_alma:
                    prompt_sistema_atual = PERSONALIDADES["❄️ Yukina (Companheira Obsessiva)"]
                elif "[AGENTE]" in tag_alma:
                    prompt_sistema_atual = PERSONALIDADES["🛠️ Agente Construtora (Vibe Coding)"]
                elif "[RPG]" in tag_alma:
                    prompt_sistema_atual = PERSONALIDADES["🎭 A Narradora Implacável (RPG)"]
                elif "[NERD]" in tag_alma:
                    prompt_sistema_atual = PERSONALIDADES["🤓 Nerd / Geek (Cultura Pop)"]
                elif "[DEBOCHE]" in tag_alma:
                    prompt_sistema_atual = PERSONALIDADES["🍷 Analítica e Sarcástica (Debochada)"]
                elif "[ARTE]" in tag_alma:
                    prompt_sistema_atual = PERSONALIDADES["🎨 Artística e Criativa (Poética)"]
                else:
                    prompt_sistema_atual = PERSONALIDADES["🤖 Neutra (Padrão Gemini)"]
                st.toast(f"🎭 Alma assumida: {tag_alma}")
            else:
                prompt_sistema_atual = PERSONALIDADES.get(st.session_state.personalidade_ativa, PERSONALIDADES["🤖 Neutra (Padrão Gemini)"])
            
            agora = datetime.now(timezone(timedelta(hours=-3)))
            prompt_sistema_atual += f"\n\n[INFO]: Data/Hora local: {agora.strftime('%Y-%m-%d %H:%M:%S')}."

            motor_real = st.session_state.modelo_selecionado
            if "Automático" in motor_real:
                with st.spinner("Roteando..."):
                    tag_decisao = analisar_intencao_gerente(prompt, gr_key)
                    if tem_texto:
                        motor_real = "12. Arquivista (Mistral Nemo)"
                    elif "[IMAGEM]" in tag_decisao:
                        motor_real = "9. Imagem (Flux 2 Pro)"
                    elif "[VIDEO]" in tag_decisao:
                        motor_real = "16. Vídeo (Kling V1.5)"
                    elif "[VISAO]" in tag_decisao or tem_imagem:
                        motor_real = "7. Visão Omni (MiMo V2)"
                    elif "[CODIGO]" in tag_decisao:
                        motor_real = "14. Engenheiro Sênior (DeepSeek V4)"
                    elif "[PESQUISA]" in tag_decisao:
                        motor_real = "2. Pesquisa (Groq Llama 3.3)"
                    else:
                        motor_real = "5. Narrador Líder (Euryale)"
                    st.toast(f"⚙️ Operário: {motor_real}")

            if pc_key and "Imagem" not in motor_real and "Vídeo" not in motor_real and "Visão" not in motor_real:
                memoria_profunda = buscar_memoria_pinecone(prompt, or_key, pc_key, USERNAME)
                if memoria_profunda:
                    prompt_sistema_atual += f"\n\n[MEMÓRIAS]:\n{memoria_profunda}"
                    st.toast("🧠 Memória ativada.")

            salvar_pinecone(prompt, "Usuário", or_key, pc_key, USERNAME)
            modelo_id = MODEL_IDS[motor_real]
            
            if "Vídeo" in motor_real:
                st.error("⚠️ Geração de vídeo temporariamente desativada ou aguardando integração estável.")

            elif "Pesquisa" in motor_real:
                with st.spinner("Pesquisando na Web..."):
                    # ✅ FIX #10: Melhorar cópia de histórico
                    historico = []
                    for m in mensagens:
                        if "image_url" not in m:
                            msg_copy = m.copy()
                            historico.append(msg_copy)
                    
                    contexto_web = pesquisar_web(prompt)
                    if contexto_web and len(historico) > 0:
                        historico[-1]["content"] = f"DADOS DA WEB:\n{contexto_web}\n\nPEDIDO:\n{prompt}"
                    
                    try:
                        res = OpenAI(base_url="https://api.groq.com/openai/v1", api_key=gr_key).chat.completions.create(
                            model=modelo_id,
                            messages=[{"role": "system", "content": prompt_sistema_atual}] + historico,
                            max_tokens=4000
                        )
                        ans = res.choices[0].message.content
                        st.markdown(ans)
                        st.session_state.db[st.session_state.current_chat]["messages"].append({"role": "assistant", "content": ans})
                        salvar_pinecone(ans, "Yukina", or_key, pc_key, USERNAME)
                    except Exception as e:
                        st.error(f"❌ Erro: {str(e)}")

            elif "Imagem" in motor_real:
                with st.spinner("Desenhando..."):
                    try:
                        res_data = requests.post(
                            "https://openrouter.ai/api/v1/chat/completions",
                            headers={
                                "Authorization": f"Bearer {or_key}",
                                "Content-Type": "application/json"
                            },
                            json={
                                "model": modelo_id,
                                "messages": [{"role": "user", "content": prompt}],
                                "modalities": ["image"]
                            }
                        ).json()
                        
                        msg_obj = res_data.get('choices', [{}])[0].get('message', {})
                        url = None
                        
                        if 'images' in msg_obj:
                            url = msg_obj['images'][0]['image_url']['url']
                        elif 'content' in msg_obj:
                            urls = re.findall(r'(https?://[^\s)]+)', msg_obj.get('content', ''))
                            url = urls[0] if urls else None
                        
                        if url:
                            st.image(url)
                            st.session_state.db[st.session_state.current_chat]["messages"].append({
                                "role": "assistant",
                                "content": "Arte gerada.",
                                "image_url": url
                            })
                    except Exception as e:
                        st.error(f"❌ Erro Imagem: {str(e)}")

            elif "Visão" in motor_real and tem_imagem:
                with st.spinner("Analisando imagens..."):
                    content_list = [{"type": "text", "text": prompt}]
                    for img_b64 in imagens_b64:
                        content_list.append({
                            "type": "image_url",
                            "image_url": {"url": f"data:image/jpeg;base64,{img_b64}"}
                        })
                    
                    try:
                        res = OpenAI(base_url="https://openrouter.ai/api/v1", api_key=or_key).chat.completions.create(
                            model=modelo_id,
                            messages=[
                                {"role": "system", "content": prompt_sistema_atual},
                                {"role": "user", "content": content_list}
                            ],
                            max_tokens=4000
                        )
                        ans = res.choices[0].message.content
                        st.markdown(ans)
                        st.session_state.db[st.session_state.current_chat]["messages"].append({"role": "assistant", "content": ans})
                        salvar_pinecone(ans, "Yukina", or_key, pc_key, USERNAME)
                    except Exception as e:
                        st.error(f"❌ Erro Visão: {str(e)}")

            else:
                # ✅ FIX #10: Melhorar cópia e tratamento de histórico
                historico = []
                for m in mensagens:
                    if "image_url" not in m:
                        msg_copy = m.copy()
                        historico.append(msg_copy)
                
                if tem_texto and len(historico) > 0:
                    historico[-1]["content"] = f"DOCUMENTOS:\n{conteudo_arquivo}\n\nPEDIDO:\n{prompt}"
                
                ph = st.empty()
                full = ""
                
                try:
                    resp = OpenAI(base_url="https://openrouter.ai/api/v1", api_key=or_key).chat.completions.create(
                        model=modelo_id,
                        messages=[{"role": "system", "content": prompt_sistema_atual}] + historico,
                        stream=True,
                        temperature=0.7,
                        max_tokens=4096
                    )
                    
                    for chunk in resp:
                        if chunk.choices and chunk.choices[0].delta.content:
                            full += chunk.choices[0].delta.content
                            ph.markdown(full + "▌")
                    
                    ph.markdown(full)
                except Exception as e:
                    st.error(f"❌ Erro API: {str(e)}")
                finally:
                    # ✅ FIX #5: Verificar role da última mensagem em vez de comparar tamanho
                    if full.strip():
                        chat_messages = st.session_state.db[st.session_state.current_chat]["messages"]
                        if not chat_messages or chat_messages[-1]["role"] != "assistant":
                            chat_messages.append({"role": "assistant", "content": full})
                            salvar_pinecone(full, "Yukina", or_key, pc_key, USERNAME)
    
    st.session_state.uploader_key += 1
    save_db(st.session_state.db, DB_FILE)
    st.rerun()