Astarok commited on
Commit
b7f6510
·
verified ·
1 Parent(s): 3da8d96

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +338 -53
app.py CHANGED
@@ -1,10 +1,29 @@
1
  import streamlit as st
 
 
2
  import json
 
3
  import re
4
  import time
5
- from datetime import datetime
 
 
 
6
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  # 1. CONFIGURAÇÃO DE ECRÃ E CSS (BLINDAGEM TOTAL DARK MODE)
 
8
  st.set_page_config(page_title="Yukina", page_icon="❄", layout="centered")
9
 
10
  st.markdown("""
@@ -13,7 +32,7 @@ st.markdown("""
13
  footer {visibility: hidden;}
14
  [data-testid="stHeader"] { background-color: transparent !important; }
15
  .stApp { background-color: #131314 !important; color: #ededed !important; }
16
-
17
  /* --- BARRA LATERAL --- */
18
  [data-testid="stSidebar"] { background-color: #0b0b0b !important; border-right: 1px solid #1e1f20 !important; }
19
  [data-testid="stSidebar"] button { background-color: transparent !important; border: none !important; box-shadow: none !important; color: #a0a0a0 !important; }
@@ -23,7 +42,7 @@ st.markdown("""
23
  /* --- CAIXAS DE SELEÇÃO E TOGGLE --- */
24
  div[data-baseweb="select"] > div { background-color: #1e1f20 !important; color: white !important; border: 1px solid #3c4043 !important; }
25
  div[role="listbox"] { background-color: #1e1f20 !important; color: white !important; }
26
-
27
  /* --- A MARRETA DEFINITIVA PARA O TECLADO E FUNDO --- */
28
  div[data-testid="stBottom"], div[data-testid="stBottom"] > div, div[data-testid="stBottomBlock"], div[data-testid="stBottomBlock"] > div {
29
  background-color: #131314 !important; background: #131314 !important;
@@ -46,13 +65,15 @@ st.markdown("""
46
  .stChatMessage { background-color: transparent !important; border: none !important; padding-bottom: 8px !important; }
47
  [data-testid="stExpander"] { background-color: #1e1f20 !important; border: 1px solid #3c4043 !important; border-radius: 15px !important; margin-bottom: 10px; }
48
  [data-testid="stExpander"] summary { color: #e3e3e3 !important; }
49
-
50
  /* Estilo do Status (Agente) */
51
  [data-testid="stStatusWidget"] { background-color: #1e1f20 !important; border: 1px solid #3c4043 !important; border-radius: 10px !important; }
52
  </style>
53
  """, unsafe_allow_html=True)
54
 
 
55
  # 2. SISTEMA DE LOGIN E PERFIS
 
56
  if "logged_in" not in st.session_state:
57
  st.session_state.logged_in = False
58
  st.session_state.username = ""
@@ -61,7 +82,7 @@ if not st.session_state.logged_in:
61
  st.markdown("<br><br><br>", unsafe_allow_html=True)
62
  st.markdown("<h1 style='text-align: center; color: white; font-size: 50px;'>❄ Yukina</h1>", unsafe_allow_html=True)
63
  st.markdown("<p style='text-align: center; color: #a0a0a0;'>Identifique-se para carregar suas memórias.</p>", unsafe_allow_html=True)
64
-
65
  col1, col2, col3 = st.columns([1, 2, 1])
66
  with col2:
67
  nome_input = st.text_input("Qual é o seu nome?", placeholder="Ex: Leonardo", label_visibility="collapsed")
@@ -77,7 +98,9 @@ USERNAME = st.session_state.username
77
  DB_FILE = f"yukina_memoria_{USERNAME}.json"
78
  DATASET_ID = "Astarok/Yukina_Memoria"
79
 
 
80
  # 3. ARQUITETURA DE DADOS E MODELOS
 
81
  MODEL_IDS = {
82
  "🤖 Automático (Gerente Groq)": "AUTO",
83
  "1. Gerente (Hermes 2 Pro)": "nousresearch/hermes-2-pro-llama-3-8b",
@@ -113,38 +136,146 @@ Mantenha traços sutis da devoção da Yukina ao usuário, mas seja absurdamente
113
  "🤖 Neutra (Padrão Gemini)": "Você é uma assistente virtual neutra e direta."
114
  }
115
 
116
- # Carregar banco de dados
117
- try:
118
- with open(DB_FILE, "r") as f:
119
- st.session_state.db = json.load(f)
120
- except FileNotFoundError:
121
- st.session_state.db = {}
122
-
123
- # Função para salvar o banco de dados
124
- def save_db(db, file):
125
- with open(file, "w") as f:
126
- json.dump(db, f, ensure_ascii=False, indent=4)
127
-
128
- # Função para renomear um chat
129
- def rename_chat(old_name, new_name):
130
- if old_name in st.session_state.db:
131
- st.session_state.db[new_name] = st.session_state.db.pop(old_name)
132
- return True
133
- return False
134
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  # 4. SIDEBAR E INTERFACE PRINCIPAL
136
- if "current_chat" not in st.session_state:
137
- st.session_state.current_chat = list(st.session_state.db.keys())[0] if st.session_state.db else None
138
-
139
- if "modelo_selecionado" not in st.session_state:
140
- st.session_state.modelo_selecionado = list(MODEL_IDS.keys())[0]
141
-
142
- if "personalidade_ativa" not in st.session_state:
143
- st.session_state.personalidade_ativa = list(PERSONALIDADES.keys())[0]
144
-
145
- if "modo_agente" not in st.session_state:
146
- st.session_state.modo_agente = False
147
-
148
  with st.sidebar:
149
  st.markdown(f"<p style='color: #888; margin-bottom: 0px;'>Logado como: <b>{USERNAME}</b></p>", unsafe_allow_html=True)
150
  c_title, c_add, c_set = st.columns([5, 1, 1])
@@ -155,14 +286,14 @@ with st.sidebar:
155
  with c_set:
156
  with st.popover("⚙️"):
157
  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)
158
-
159
  # --- FILTRO DE IMPORTAÇÃO ---
160
  arquivo_import = st.file_uploader("Importar conversa:", type=["json"])
161
  if arquivo_import:
162
  try:
163
  dados_importados = json.load(arquivo_import)
164
  dados_validos = {k: v for k, v in dados_importados.items() if isinstance(v, dict) and "messages" in v}
165
-
166
  if dados_validos:
167
  st.session_state.db.update(dados_validos)
168
  save_db(st.session_state.db, DB_FILE)
@@ -173,7 +304,7 @@ with st.sidebar:
173
  st.rerun()
174
  except:
175
  st.error("Erro no ficheiro.")
176
-
177
  st.markdown("---")
178
  if st.button("🚪 Sair", use_container_width=True): st.session_state.logged_in = False; st.rerun()
179
 
@@ -201,11 +332,10 @@ with st.sidebar:
201
  st.markdown("<h4 style='color: #ededed;'>Núcleo da IA</h4>", unsafe_allow_html=True)
202
  st.session_state.modelo_selecionado = st.selectbox("Motor:", list(MODEL_IDS.keys()), index=list(MODEL_IDS.keys()).index(st.session_state.modelo_selecionado))
203
  st.session_state.personalidade_ativa = st.selectbox("Alma:", list(PERSONALIDADES.keys()), index=list(PERSONALIDADES.keys()).index(st.session_state.personalidade_ativa))
204
-
205
  st.markdown("<br>", unsafe_allow_html=True)
206
  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).")
207
 
208
- # 5. TOOLBAR INFERIOR E PROCESSAMENTO
209
  st.markdown("<h3 style='margin-top: -10px; color: #ededed;'>❄ Yukina</h3>", unsafe_allow_html=True)
210
  mensagens = st.session_state.db[st.session_state.current_chat]["messages"]
211
 
@@ -218,6 +348,9 @@ else:
218
  elif "video_url" in m: st.video(m["video_url"])
219
  else: st.markdown(m["content"])
220
 
 
 
 
221
  st.markdown("<br>", unsafe_allow_html=True)
222
 
223
  t_col1, t_col2, t_space = st.columns([1, 1, 8])
@@ -231,7 +364,7 @@ with t_col2:
231
  if st.button("🔄", help="Regerar resposta"):
232
  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":
233
  st.session_state.db[st.session_state.current_chat]["messages"].pop()
234
- save_db(st.session_state.db, DB_FILE); st.rerun()
235
 
236
  # --- ATUALIZAÇÃO DA GAVETA NATIVA PARA ACEITAR QUALQUER ARQUIVO E NÃO BLOQUEAR ZIP NO ANDROID ---
237
  with st.expander("📂 Abrir Galeria / Anexar Ficheiros"):
@@ -239,14 +372,166 @@ with st.expander("📂 Abrir Galeria / Anexar Ficheiros"):
239
 
240
  prompt = st.chat_input("Peça à Yukina...")
241
 
242
- # --- TESTE
243
- def test_code():
244
- # Adicione aqui o código para testar a funcionalidade
245
- pass
246
-
247
- def main():
248
- # Adicione aqui o código para executar a funcionalidade
249
- pass
250
-
251
- if __name__ == "__main__":
252
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import streamlit as st
2
+ import os
3
+ import requests
4
  import json
5
+ import base64
6
  import re
7
  import time
8
+ import zipfile
9
+ import io
10
+ from datetime import datetime, timezone, timedelta
11
+ from openai import OpenAI
12
 
13
+ try:
14
+ from huggingface_hub import HfApi, hf_hub_download
15
+ HF_HUB_AVAILABLE = True
16
+ except ImportError:
17
+ HF_HUB_AVAILABLE = False
18
+
19
+ try:
20
+ from pinecone import Pinecone
21
+ except ImportError:
22
+ pass
23
+
24
+ # ═══════════════════════════════════════════════════════════════════════════
25
  # 1. CONFIGURAÇÃO DE ECRÃ E CSS (BLINDAGEM TOTAL DARK MODE)
26
+ # ═══════════════════════════════════════════════════════════════════════════
27
  st.set_page_config(page_title="Yukina", page_icon="❄", layout="centered")
28
 
29
  st.markdown("""
 
32
  footer {visibility: hidden;}
33
  [data-testid="stHeader"] { background-color: transparent !important; }
34
  .stApp { background-color: #131314 !important; color: #ededed !important; }
35
+
36
  /* --- BARRA LATERAL --- */
37
  [data-testid="stSidebar"] { background-color: #0b0b0b !important; border-right: 1px solid #1e1f20 !important; }
38
  [data-testid="stSidebar"] button { background-color: transparent !important; border: none !important; box-shadow: none !important; color: #a0a0a0 !important; }
 
42
  /* --- CAIXAS DE SELEÇÃO E TOGGLE --- */
43
  div[data-baseweb="select"] > div { background-color: #1e1f20 !important; color: white !important; border: 1px solid #3c4043 !important; }
44
  div[role="listbox"] { background-color: #1e1f20 !important; color: white !important; }
45
+
46
  /* --- A MARRETA DEFINITIVA PARA O TECLADO E FUNDO --- */
47
  div[data-testid="stBottom"], div[data-testid="stBottom"] > div, div[data-testid="stBottomBlock"], div[data-testid="stBottomBlock"] > div {
48
  background-color: #131314 !important; background: #131314 !important;
 
65
  .stChatMessage { background-color: transparent !important; border: none !important; padding-bottom: 8px !important; }
66
  [data-testid="stExpander"] { background-color: #1e1f20 !important; border: 1px solid #3c4043 !important; border-radius: 15px !important; margin-bottom: 10px; }
67
  [data-testid="stExpander"] summary { color: #e3e3e3 !important; }
68
+
69
  /* Estilo do Status (Agente) */
70
  [data-testid="stStatusWidget"] { background-color: #1e1f20 !important; border: 1px solid #3c4043 !important; border-radius: 10px !important; }
71
  </style>
72
  """, unsafe_allow_html=True)
73
 
74
+ # ═══════════════════════════════════════════════════════════════════════════
75
  # 2. SISTEMA DE LOGIN E PERFIS
76
+ # ═══════════════════════════════════════════════════════════════════════════
77
  if "logged_in" not in st.session_state:
78
  st.session_state.logged_in = False
79
  st.session_state.username = ""
 
82
  st.markdown("<br><br><br>", unsafe_allow_html=True)
83
  st.markdown("<h1 style='text-align: center; color: white; font-size: 50px;'>❄ Yukina</h1>", unsafe_allow_html=True)
84
  st.markdown("<p style='text-align: center; color: #a0a0a0;'>Identifique-se para carregar suas memórias.</p>", unsafe_allow_html=True)
85
+
86
  col1, col2, col3 = st.columns([1, 2, 1])
87
  with col2:
88
  nome_input = st.text_input("Qual é o seu nome?", placeholder="Ex: Leonardo", label_visibility="collapsed")
 
98
  DB_FILE = f"yukina_memoria_{USERNAME}.json"
99
  DATASET_ID = "Astarok/Yukina_Memoria"
100
 
101
+ # ═══════════════════════════════════════════════════════════════════════════
102
  # 3. ARQUITETURA DE DADOS E MODELOS
103
+ # ═══════════════════════════════════════════════════════════════════════════
104
  MODEL_IDS = {
105
  "🤖 Automático (Gerente Groq)": "AUTO",
106
  "1. Gerente (Hermes 2 Pro)": "nousresearch/hermes-2-pro-llama-3-8b",
 
136
  "🤖 Neutra (Padrão Gemini)": "Você é uma assistente virtual neutra e direta."
137
  }
138
 
139
+ # --- FUNÇÕES NUCLEARES E MEMÓRIA ---
140
+ def get_embedding(text, or_key):
141
+ try:
142
+ 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})
143
+ return res.json()['data'][0]['embedding'] if res.status_code == 200 else None
144
+ except Exception: return None
145
+
146
+ def salvar_pinecone(text, role, or_key, pc_key, namespace):
147
+ if not pc_key or not text.strip(): return
148
+ embed = get_embedding(text, or_key)
149
+ if not embed: return
150
+ try:
151
+ pc = Pinecone(api_key=pc_key)
152
+ index = pc.Index("yukina")
153
+ index.upsert(vectors=[{"id": f"msg_{int(time.time()*1000)}", "values": embed, "metadata": {"texto": f"[{role.upper()}]: {text}", "data": str(datetime.now())}}], namespace=namespace)
154
+ except Exception: pass
155
+
156
+ def buscar_memoria_pinecone(query, or_key, pc_key, namespace):
157
+ if not pc_key: return ""
158
+ embed = get_embedding(query, or_key)
159
+ if not embed: return ""
160
+ try:
161
+ pc = Pinecone(api_key=pc_key)
162
+ resultados = pc.Index("yukina").query(vector=embed, top_k=3, include_metadata=True, namespace=namespace)
163
+ return "\n".join([m['metadata']['texto'] for m in resultados['matches'] if m['score'] > 0.50])
164
+ except Exception: return ""
165
+
166
+ def chamada_agente(sys_prompt, user_prompt, or_key, gr_key, mod_id):
167
+ try:
168
+ if mod_id == "AUTO": mod_id = "deepseek/deepseek-v4-pro:online"
169
+ if "groq" in mod_id.lower() or "llama-3.3" in mod_id.lower():
170
+ client = OpenAI(base_url="https://api.groq.com/openai/v1", api_key=gr_key)
171
+ else:
172
+ client = OpenAI(base_url="https://openrouter.ai/api/v1", api_key=or_key)
173
+ res = client.chat.completions.create(model=mod_id, messages=[{"role": "system", "content": sys_prompt}, {"role": "user", "content": user_prompt}], temperature=0.2)
174
+ return res.choices[0].message.content
175
+ except Exception as e: return f"Falha na conexão do agente: {e}"
176
+
177
+ def analisar_intencao_gerente(prompt, groq_key):
178
+ try:
179
+ client = OpenAI(base_url="https://api.groq.com/openai/v1", api_key=groq_key)
180
+ response = client.chat.completions.create(
181
+ model="llama-3.3-70b-versatile",
182
+ messages=[{"role": "system", "content": "Responda APENAS com a TAG: [IMAGEM], [VIDEO], [VISAO], [CODIGO], [ARQUIVISTA], [PESQUISA] ou [CHAT]."}, {"role": "user", "content": prompt}],
183
+ temperature=0.1, max_tokens=10
184
+ )
185
+ return response.choices[0].message.content.strip().upper()
186
+ except Exception: return "[CHAT]"
187
+
188
+ def analisar_alma_gerente(prompt, groq_key):
189
+ try:
190
+ client = OpenAI(base_url="https://api.groq.com/openai/v1", api_key=groq_key)
191
+ prompt_alma = """Você é o Diretor de Personas. Analise o pedido do usuário e responda APENAS com UMA destas tags:
192
+ [YUKINA] - Para conversas íntimas, declarações, ou perguntas sobre você mesma.
193
+ [AGENTE] - Para criar códigos complexos, criar softwares, ou consertar objetos físicos (celular, geladeira, hardware).
194
+ [RPG] - Para criação de histórias, jogos ou cenários de fantasia.
195
+ [NERD] - Para animes, mangás, cultura pop e videogames.
196
+ [DEBOCHE] - Para insultos, piadas, ou se o usuário pedir sarcasmo.
197
+ [ARTE] - Para pedidos poéticos ou reflexões filosóficas profundas.
198
+ [NEUTRA] - Para pesquisas na web, trabalho ou finanças.
199
+ Responda APENAS com a TAG."""
200
+ 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)
201
+ return response.choices[0].message.content.strip().upper()
202
+ except Exception: return "[NEUTRA]"
203
+
204
+ def pesquisar_web(query):
205
+ try:
206
+ from duckduckgo_search import DDGS
207
+ with DDGS() as ddgs:
208
+ resultados = list(ddgs.text(query, max_results=3, region='wt-wt'))
209
+ return "\n\n".join([f"🔹 {r.get('title', '')}: {r.get('body', '')}" for r in resultados]) if resultados else ""
210
+ except Exception: return ""
211
+
212
+ def load_db(db_filename):
213
+ hf_token = os.getenv("HF_TOKEN")
214
+ if hf_token and HF_HUB_AVAILABLE:
215
+ try:
216
+ api = HfApi(token=hf_token); api.create_repo(repo_id=DATASET_ID, repo_type="dataset", private=True, exist_ok=True)
217
+ path = hf_hub_download(repo_id=DATASET_ID, filename=db_filename, repo_type="dataset", token=hf_token)
218
+ with open(path, "r", encoding="utf-8") as f: return json.load(f)
219
+ except Exception: pass
220
+ if os.path.exists(db_filename):
221
+ try:
222
+ with open(db_filename, "r", encoding="utf-8") as f: return json.load(f)
223
+ except Exception: pass
224
+ return {}
225
+
226
+ def save_db(db_data, db_filename):
227
+ try:
228
+ with open(db_filename, "w", encoding="utf-8") as f: json.dump(db_data, f, ensure_ascii=False, indent=4)
229
+ hf_token = os.getenv("HF_TOKEN")
230
+ if hf_token and HF_HUB_AVAILABLE:
231
+ api = HfApi(token=hf_token); api.create_repo(repo_id=DATASET_ID, repo_type="dataset", private=True, exist_ok=True)
232
+ api.upload_file(path_or_fileobj=db_filename, path_in_repo=db_filename, repo_id=DATASET_ID, repo_type="dataset")
233
+ except Exception: pass
234
+
235
+ def rename_chat(old_id, new_id):
236
+ if not new_id or new_id == old_id or new_id in st.session_state.db: return False
237
+ st.session_state.db = {new_id if k == old_id else k: v for k, v in st.session_state.db.items()}
238
+ if st.session_state.current_chat == old_id: st.session_state.current_chat = new_id
239
+ save_db(st.session_state.db, DB_FILE); return True
240
+
241
+ # ═══════════════════════════════════════════════════════════════════════════
242
+ # INICIALIZAÇÃO DE VARIÁVEIS E ROTINA DE LIMPEZA (COM AUTO-CURA)
243
+ # ═══════════════════════════════════════════════════════════════════════════
244
+ if "db" not in st.session_state or "last_user" not in st.session_state or st.session_state.last_user != USERNAME:
245
+ st.session_state.db = load_db(DB_FILE); st.session_state.last_user = USERNAME
246
+ if "current_chat" in st.session_state: del st.session_state.current_chat
247
+
248
+ if "current_chat" not in st.session_state:
249
+ nid = f"Chat {datetime.now().strftime('%H:%M:%S')}"; st.session_state.db[nid] = {"pinned": False, "messages": []}; st.session_state.current_chat = nid
250
+
251
+ # AUTO-CURA: Verifica chats vazios ou com estrutura corrompida (como os do Grok)
252
+ chats_para_remover = []
253
+ for cid, cdata in list(st.session_state.db.items()):
254
+ if not isinstance(cdata, dict) or "messages" not in cdata:
255
+ chats_para_remover.append(cid)
256
+ elif len(cdata.get("messages", [])) == 0 and cid != st.session_state.current_chat:
257
+ chats_para_remover.append(cid)
258
+
259
+ for cid in chats_para_remover:
260
+ if cid in st.session_state.db:
261
+ del st.session_state.db[cid]
262
+
263
+ if chats_para_remover: save_db(st.session_state.db, DB_FILE)
264
+
265
+ if len(st.session_state.db) == 0:
266
+ nid = f"Chat {datetime.now().strftime('%H:%M:%S')}"; st.session_state.db[nid] = {"pinned": False, "messages": []}; st.session_state.current_chat = nid
267
+
268
+ if st.session_state.current_chat not in st.session_state.db: st.session_state.current_chat = list(st.session_state.db.keys())[0]
269
+
270
+ if "modelo_selecionado" not in st.session_state: st.session_state.modelo_selecionado = "🤖 Automático (Gerente Groq)"
271
+ if "personalidade_ativa" not in st.session_state: st.session_state.personalidade_ativa = "🤖 Automática (Gerente Groq)"
272
+ if "regerar" not in st.session_state: st.session_state.regerar = False
273
+ if "uploader_key" not in st.session_state: st.session_state.uploader_key = 0
274
+ if "modo_agente" not in st.session_state: st.session_state.modo_agente = False
275
+
276
+ # ═══════════════════════════════════════════════════════════════════════════
277
  # 4. SIDEBAR E INTERFACE PRINCIPAL
278
+ # ═══════════════════════════════════════════════════════════════════════════
 
 
 
 
 
 
 
 
 
 
 
279
  with st.sidebar:
280
  st.markdown(f"<p style='color: #888; margin-bottom: 0px;'>Logado como: <b>{USERNAME}</b></p>", unsafe_allow_html=True)
281
  c_title, c_add, c_set = st.columns([5, 1, 1])
 
286
  with c_set:
287
  with st.popover("⚙️"):
288
  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)
289
+
290
  # --- FILTRO DE IMPORTAÇÃO ---
291
  arquivo_import = st.file_uploader("Importar conversa:", type=["json"])
292
  if arquivo_import:
293
  try:
294
  dados_importados = json.load(arquivo_import)
295
  dados_validos = {k: v for k, v in dados_importados.items() if isinstance(v, dict) and "messages" in v}
296
+
297
  if dados_validos:
298
  st.session_state.db.update(dados_validos)
299
  save_db(st.session_state.db, DB_FILE)
 
304
  st.rerun()
305
  except:
306
  st.error("Erro no ficheiro.")
307
+
308
  st.markdown("---")
309
  if st.button("🚪 Sair", use_container_width=True): st.session_state.logged_in = False; st.rerun()
310
 
 
332
  st.markdown("<h4 style='color: #ededed;'>Núcleo da IA</h4>", unsafe_allow_html=True)
333
  st.session_state.modelo_selecionado = st.selectbox("Motor:", list(MODEL_IDS.keys()), index=list(MODEL_IDS.keys()).index(st.session_state.modelo_selecionado))
334
  st.session_state.personalidade_ativa = st.selectbox("Alma:", list(PERSONALIDADES.keys()), index=list(PERSONALIDADES.keys()).index(st.session_state.personalidade_ativa))
335
+
336
  st.markdown("<br>", unsafe_allow_html=True)
337
  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).")
338
 
 
339
  st.markdown("<h3 style='margin-top: -10px; color: #ededed;'>❄ Yukina</h3>", unsafe_allow_html=True)
340
  mensagens = st.session_state.db[st.session_state.current_chat]["messages"]
341
 
 
348
  elif "video_url" in m: st.video(m["video_url"])
349
  else: st.markdown(m["content"])
350
 
351
+ # ═══════════════════════════════════════════════════════════════════════════
352
+ # 5. TOOLBAR INFERIOR E PROCESSAMENTO
353
+ # ═══════════════════════════════════════════════════════════════════════════
354
  st.markdown("<br>", unsafe_allow_html=True)
355
 
356
  t_col1, t_col2, t_space = st.columns([1, 1, 8])
 
364
  if st.button("🔄", help="Regerar resposta"):
365
  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":
366
  st.session_state.db[st.session_state.current_chat]["messages"].pop()
367
+ save_db(st.session_state.db, DB_FILE); st.session_state.regerar = True; st.rerun()
368
 
369
  # --- ATUALIZAÇÃO DA GAVETA NATIVA PARA ACEITAR QUALQUER ARQUIVO E NÃO BLOQUEAR ZIP NO ANDROID ---
370
  with st.expander("📂 Abrir Galeria / Anexar Ficheiros"):
 
372
 
373
  prompt = st.chat_input("Peça à Yukina...")
374
 
375
+ conteudo_arquivo = ""
376
+ nomes_arquivos = []
377
+ imagens_b64 = []
378
+
379
+ if upload_files:
380
+ for f in upload_files:
381
+ nomes_arquivos.append(f.name)
382
+
383
+ # Leitura Inteligente de ZIP (Pastas Compactadas)
384
+ if f.name.endswith('.zip'):
385
+ try:
386
+ with zipfile.ZipFile(f, 'r') as zip_ref:
387
+ for file_info in zip_ref.infolist():
388
+ if not file_info.is_dir() and not file_info.filename.startswith('__MACOSX'):
389
+ if file_info.filename.endswith(('.txt', '.csv', '.json', '.py', '.html', '.md', '.js', '.css')):
390
+ with zip_ref.open(file_info) as extracted_file:
391
+ conteudo_arquivo += f"\n\n--- Arquivo Extraído do ZIP: {file_info.filename} ---\n" + extracted_file.read().decode('utf-8', errors='ignore')
392
+ except Exception as e:
393
+ st.toast(f"⚠️ Erro ao descompactar {f.name}: {e}")
394
+
395
+ # Leitura de Código/Texto Avulso
396
+ elif f.name.endswith(('.txt', '.csv', '.json', '.py', '.html', '.md', '.js', '.css')):
397
+ conteudo_arquivo += f"\n\n--- Conteúdo de: {f.name} ---\n" + f.getvalue().decode("utf-8")
398
+
399
+ # Leitura de Imagens
400
+ elif f.name.endswith(('.png', '.jpg', '.jpeg')):
401
+ imagens_b64.append(base64.b64encode(f.read()).decode())
402
+
403
+ tem_texto = len(conteudo_arquivo) > 0
404
+ tem_imagem = len(imagens_b64) > 0
405
+
406
+ if prompt or st.session_state.regerar:
407
+ if st.session_state.regerar and len(st.session_state.db[st.session_state.current_chat]["messages"]) > 0:
408
+ texto_anterior = st.session_state.db[st.session_state.current_chat]["messages"][-1]["content"]
409
+ prompt = texto_anterior.split("\n\n\n", 1)[-1] if "📄 **Ficheiros enviados:**" in texto_anterior else texto_anterior
410
+ st.session_state.regerar = False
411
+ else:
412
+ if nomes_arquivos: mensagem_display = f"📄 **Ficheiros enviados:** {', '.join([f'`{n}`' for n in nomes_arquivos])}\n\n\n{prompt}"
413
+ else: mensagem_display = prompt
414
+ st.session_state.db[st.session_state.current_chat]["messages"].append({"role": "user", "content": mensagem_display})
415
+ with st.chat_message("user"): st.markdown(mensagem_display)
416
+
417
+ if len(st.session_state.db[st.session_state.current_chat]["messages"]) == 1 and st.session_state.current_chat.startswith("Chat "):
418
+ limpo = prompt.strip(); novo_nome = limpo[:25] + "..." if len(limpo) > 25 else limpo
419
+ base_nome = novo_nome; contador = 1
420
+ while novo_nome in st.session_state.db: novo_nome = f"{base_nome} ({contador})"; contador += 1
421
+ rename_chat(st.session_state.current_chat, novo_nome)
422
+
423
+ # ═══════════════════════════════════════════════════════════════════════════
424
+ # EXECUÇÃO DO PEDIDO (MODO AGENTE OU MODO NORMAL)
425
+ # ═══════════════════════════════════════════════════════════════════════════
426
+ or_key = os.getenv("OPENROUTER_API_KEY"); gr_key = os.getenv("GROQ_API_KEY")
427
+ ws_key = os.getenv("YUKINA_CORE"); pc_key = os.getenv("PINECONE_API_KEY")
428
+ modelo_id = MODEL_IDS[st.session_state.modelo_selecionado]
429
+
430
+ if st.session_state.modo_agente and not tem_imagem:
431
+ with st.chat_message("assistant"):
432
+ with st.status("🛠️ **Agente Yukina Trabalhando...**", expanded=True) as status:
433
+ st.write("🧠 **1. Arquiteto:** Analisando a estrutura...")
434
+ 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."
435
+ pedido_completo = f"ANEXOS:\n{conteudo_arquivo}\n\nPEDIDO:\n{prompt}" if tem_texto else prompt
436
+ plano = chamada_agente(sys_arq, pedido_completo, or_key, gr_key, modelo_id)
437
+ st.markdown(f"> *Plano concebido.*")
438
+
439
+ st.write("💻 **2. Engenheiro:** Escrevendo a solução base...")
440
+ 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."
441
+ codigo = chamada_agente(sys_eng, f"PLANO ESTRUTURAL:\n{plano}\n\nOBJETIVO ORIGINAL DO USUÁRIO:\n{prompt}", or_key, gr_key, modelo_id)
442
+ st.markdown(f"> *Estrutura materializada.*")
443
+
444
+ st.write("🔍 **3. Revisor:** Procurando falhas e polindo...")
445
+ 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."
446
+ 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)
447
+
448
+ status.update(label="✅ **Solução Multi-Agente Concluída!**", state="complete", expanded=False)
449
+
450
+ st.markdown(final)
451
+ st.session_state.db[st.session_state.current_chat]["messages"].append({"role": "assistant", "content": final})
452
+ salvar_pinecone(final, "Yukina (Agente)", or_key, pc_key, USERNAME)
453
+
454
+ else:
455
+ with st.chat_message("assistant"):
456
+ if st.session_state.personalidade_ativa == "🤖 Automática (Gerente Groq)":
457
+ tag_alma = analisar_alma_gerente(prompt, gr_key)
458
+ if "[YUKINA]" in tag_alma: prompt_sistema_atual = PERSONALIDADES["❄️ Yukina (Companheira Obsessiva)"]
459
+ elif "[AGENTE]" in tag_alma: prompt_sistema_atual = PERSONALIDADES["🛠️ Agente Construtora (Vibe Coding)"]
460
+ elif "[RPG]" in tag_alma: prompt_sistema_atual = PERSONALIDADES["🎭 A Narradora Implacável (RPG)"]
461
+ elif "[NERD]" in tag_alma: prompt_sistema_atual = PERSONALIDADES["🤓 Nerd / Geek (Cultura Pop)"]
462
+ elif "[DEBOCHE]" in tag_alma: prompt_sistema_atual = PERSONALIDADES["🍷 Analítica e Sarcástica (Debochada)"]
463
+ elif "[ARTE]" in tag_alma: prompt_sistema_atual = PERSONALIDADES["🎨 Artística e Criativa (Poética)"]
464
+ else: prompt_sistema_atual = PERSONALIDADES["🤖 Neutra (Padrão Gemini)"]
465
+ st.toast(f"🎭 Alma assumida: {tag_alma}")
466
+ else:
467
+ prompt_sistema_atual = PERSONALIDADES.get(st.session_state.personalidade_ativa, PERSONALIDADES["🤖 Neutra (Padrão Gemini)"])
468
+
469
+ agora = datetime.now(timezone(timedelta(hours=-3)))
470
+ prompt_sistema_atual += f"\n\n[INFO]: Data/Hora local: {agora.strftime('%Y-%m-%d %H:%M:%S')}."
471
+
472
+ motor_real = st.session_state.modelo_selecionado
473
+ if "Automático" in motor_real:
474
+ with st.spinner("Roteando..."):
475
+ tag_decisao = analisar_intencao_gerente(prompt, gr_key)
476
+ if tem_texto: motor_real = "12. Arquivista (Mistral Nemo)"
477
+ elif "[IMAGEM]" in tag_decisao: motor_real = "9. Imagem (Flux 2 Pro)"
478
+ elif "[VIDEO]" in tag_decisao: motor_real = "16. Vídeo (Kling V1.5)"
479
+ elif "[VISAO]" in tag_decisao or tem_imagem: motor_real = "7. Visão Omni (MiMo V2)"
480
+ elif "[CODIGO]" in tag_decisao: motor_real = "14. Engenheiro Sênior (DeepSeek V4)"
481
+ elif "[PESQUISA]" in tag_decisao: motor_real = "2. Pesquisa (Groq Llama 3.3)"
482
+ else: motor_real = "5. Narrador Líder (Euryale)"
483
+ st.toast(f"⚙️ Operário: {motor_real}")
484
+
485
+ if pc_key and "Imagem" not in motor_real and "Vídeo" not in motor_real and "Visão" not in motor_real:
486
+ memoria_profunda = buscar_memoria_pinecone(prompt, or_key, pc_key, USERNAME)
487
+ if memoria_profunda: prompt_sistema_atual += f"\n\n[MEMÓRIAS]:\n{memoria_profunda}"; st.toast("🧠 Memória ativada.")
488
+
489
+ salvar_pinecone(prompt, "Usuário", or_key, pc_key, USERNAME); modelo_id = MODEL_IDS[motor_real]
490
+
491
+ if "Vídeo" in motor_real:
492
+ st.error("Geração de vídeo temporariamente desativada ou aguardando integração estável.")
493
+
494
+ elif "Pesquisa" in motor_real:
495
+ with st.spinner("Pesquisando na Web..."):
496
+ historico = [{"role": m["role"], "content": m["content"]} for m in mensagens if "image_url" not in m]
497
+ contexto_web = pesquisar_web(prompt)
498
+ if contexto_web: historico[-1]["content"] = f"DADOS DA WEB:\n{contexto_web}\n\nPEDIDO:\n{prompt}"
499
+ try:
500
+ 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)
501
+ 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)
502
+ except Exception as e: st.error(f"Erro: {e}")
503
+
504
+ elif "Imagem" in motor_real:
505
+ with st.spinner("Desenhando..."):
506
+ try:
507
+ 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()
508
+ msg_obj = res_data.get('choices', [{}])[0].get('message', {}); url = None
509
+ if 'images' in msg_obj: url = msg_obj['images'][0]['image_url']['url']
510
+ elif 'content' in msg_obj: urls = re.findall(r'(https?://[^\s)]+)', msg_obj.get('content', '')); url = urls[0] if urls else None
511
+ if url: st.image(url); st.session_state.db[st.session_state.current_chat]["messages"].append({"role": "assistant", "content": "Arte gerada.", "image_url": url})
512
+ except Exception as e: st.error(f"Erro Imagem: {e}")
513
+
514
+ elif "Visão" in motor_real and tem_imagem:
515
+ with st.spinner("Analisando imagens..."):
516
+ content_list = [{"type": "text", "text": prompt}]
517
+ for img_b64 in imagens_b64: content_list.append({"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{img_b64}"}})
518
+ try:
519
+ 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)
520
+ 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)
521
+ except Exception as e: st.error(f"Erro Visão: {e}")
522
+
523
+ else:
524
+ historico = [{"role": m["role"], "content": m["content"]} for m in mensagens if "image_url" not in m]
525
+ if tem_texto and len(historico) > 0: historico[-1]["content"] = f"DOCUMENTOS:\n{conteudo_arquivo}\n\nPEDIDO:\n{prompt}"
526
+ ph = st.empty(); full = ""
527
+ try:
528
+ 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)
529
+ for chunk in resp:
530
+ if chunk.choices and chunk.choices[0].delta.content: full += chunk.choices[0].delta.content; ph.markdown(full + "▌")
531
+ ph.markdown(full)
532
+ except Exception as e: st.error(f"Erro API: {e}")
533
+ finally:
534
+ if full.strip() and len(st.session_state.db[st.session_state.current_chat]["messages"]) == len(mensagens):
535
+ st.session_state.db[st.session_state.current_chat]["messages"].append({"role": "assistant", "content": full}); salvar_pinecone(full, "Yukina", or_key, pc_key, USERNAME)
536
+
537
+ st.session_state.uploader_key += 1; save_db(st.session_state.db, DB_FILE); st.rerun()