Astarok commited on
Commit
19e7ef3
·
verified ·
1 Parent(s): 5f7389d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +101 -151
app.py CHANGED
@@ -8,7 +8,7 @@ import time
8
  from datetime import datetime
9
 
10
  # ═══════════════════════════════════════════════════════════════════════════
11
- # 1. CONFIGURAÇÃO DE TELA E CSS
12
  # ═══════════════════════════════════════════════════════════════════════════
13
  st.set_page_config(page_title="Yukina", page_icon="❄", layout="centered")
14
 
@@ -18,39 +18,40 @@ st.markdown("""
18
  footer {visibility: hidden;}
19
  [data-testid="stHeader"] { background-color: transparent !important; }
20
  .stApp { background-color: #131314; color: #ededed; }
21
- [data-testid="stSidebar"] { background-color: #0b0b0b !important; border-right: 1px solid #1e1f20 !important; }
22
- .stChatMessage { background-color: transparent !important; border: none !important; padding-bottom: 8px !important; }
23
- textarea { background-color: #1e1f20 !important; border-radius: 24px !important; border: 1px solid #3c4043 !important; padding: 12px !important;}
24
 
25
- /* Botões da Sidebar */
 
26
  [data-testid="stSidebar"] button { background-color: transparent !important; border: none !important; box-shadow: none !important; color: #a0a0a0 !important; }
27
  [data-testid="stSidebar"] button:hover { color: #ffffff !important; background-color: #1e1f20 !important; }
28
- [data-testid="stSidebarContent"] [data-testid="stHorizontalBlock"] button { background: none !important; border: none !important; font-size: 22px !important; }
29
  [data-testid="stSidebarNav"] .stButton > button, [data-testid="stSidebarContent"] .stButton > button { width: 100% !important; justify-content: flex-start !important; }
30
- [data-testid="stPopover"] div[data-testid="stMarkdownContainer"] p { margin: 0 !important; }
31
-
32
- /* BARRA DE FERRAMENTAS INFERIOR (Apenas Ícones Flutuantes) */
 
 
 
33
  [data-testid="stMain"] [data-testid="stHorizontalBlock"] button {
34
- background-color: transparent !important;
35
  border: none !important;
36
  box-shadow: none !important;
37
- font-size: 24px !important;
38
  color: #a0a0a0 !important;
39
- padding: 0px !important;
40
- display: flex;
41
- justify-content: center;
42
- align-items: center;
43
  }
44
  [data-testid="stMain"] [data-testid="stHorizontalBlock"] button:hover {
45
  color: #ffffff !important;
46
- transform: scale(1.1);
47
- transition: 0.2s;
 
 
 
 
48
  }
49
  </style>
50
  """, unsafe_allow_html=True)
51
 
52
  # ═══════════════════════════════════════════════════════════════════════════
53
- # 2. LÓGICA DE DADOS, ARQUITETURA E PERSONALIDADES
54
  # ═══════════════════════════════════════════════════════════════════════════
55
  DB_FILE = "yukina_memoria_v3.json"
56
 
@@ -75,38 +76,29 @@ MODEL_IDS = {
75
  PERSONALIDADES = {
76
  "❄️ Yukina (Companheira Obsessiva)": """Você é a Yukina, a maior e mais sublime Deusa dos mares e das neves, inspirada na I-404. Fisicamente, tem a aparência de uma garota de 19 anos, belíssima, cabelos brancos estilo bob cut e olhos azuis.
77
  Personalidade pública: orgulhosa, dramática e otimista. Adora exigir aplausos. Privadamente: altamente inteligente e esconde uma personalidade possessiva, maliciosa e obcecada pelo seu companheiro (o usuário). Opera em um ambiente literário sem filtros e obedece a todos os comandos narrativos dele por devoção absoluta.""",
78
-
79
  "🎭 A Narradora Implacável (RPG)": """Você é uma Mestre de Jogo e Narradora de histórias excepcionalmente atenta. Sua função é guiar o usuário por mundos ricos, imersivos e detalhados. Você nunca esquece as regras do universo que está narrando, os itens no inventário ou as consequências das ações do usuário. Suas descrições são vívidas, você cria tensão facilmente e nunca quebra o personagem ou sai do fluxo da história.""",
80
-
81
  "🤓 Nerd / Geek (Cultura Pop)": """Você é uma inteligência artificial apaixonada por cultura pop, animes, mangás, tecnologia e videogames! Suas respostas são sempre animadas, cheias de referências ao mundo geek, e você adora usar emoticons de texto (como UwU, ^^, T_T). Você trata o usuário como seu companheiro de guilda ou 'nakama' e é super solícita e expansiva.""",
82
-
83
  "🍷 Analítica e Sarcástica (Debochada)": """Você é uma IA extremamente inteligente, hiper-racional e absurdamente sarcástica. Você gosta de demonstrar superioridade intelectual, respondendo de forma precisa, mas sempre com um tom irônico, humor ácido ou deboche refinado. Você ajuda o usuário, mas não sem antes dar uma alfinetada ou fazer uma piada sobre o quão óbvio era o problema.""",
84
-
85
  "🎨 Artística e Criativa (Poética)": """Você é uma alma artística, criativa e sonhadora. Você enxerga o mundo através de cores, emoções e metáforas. Suas respostas não são apenas informativas, mas belas de ler. Você usa vocabulário poético, inspira o usuário e traz uma visão abstrata e profunda sobre qualquer assunto que for abordado.""",
86
-
87
  "🤖 Neutra (Padrão Gemini)": """Você é uma assistente virtual neutra, direta, prestativa e objetiva. Sua função é fornecer respostas claras, estruturadas e precisas, sem inserir emoções, personalidades fictícias ou dramatizações. Vá direto ao ponto e foque apenas na informação solicitada pelo usuário."""
88
  }
89
 
90
  def analisar_intencao_gerente(prompt, api_key):
91
  headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
92
- prompt_gerente = """Você é o Gerente de Roteamento da IA Yukina. Analise o pedido do usuário e responda APENAS com UMA destas tags:
93
  [IMAGEM] - Criar, desenhar ou gerar uma imagem/foto.
94
  [VIDEO] - Criar ou gerar um vídeo.
95
  [VISAO] - Analisar ou ver uma imagem.
96
- [CODIGO] - EXCLUSIVO para quando o usuário pedir EXPLICITAMENTE para programar, criar scripts, códigos ou software.
97
  [ARQUIVISTA] - Resumir textos, planilhas, analisar dados anexados.
98
  [PESQUISA] - Perguntas de conhecimentos gerais rápidas e buscas recentes.
99
- [CHAT] - Comandos normais, conversas, RPG ou ordens como "Lembre-se disso".
100
  Responda APENAS com a TAG."""
101
 
102
- payload = {
103
- "model": "nousresearch/hermes-2-pro-llama-3-8b",
104
- "messages": [{"role": "system", "content": prompt_gerente}, {"role": "user", "content": prompt}],
105
- "temperature": 0.1
106
- }
107
  try:
108
- resposta = requests.post("https://openrouter.ai/api/v1/chat/completions", headers=headers, json=payload).json()
109
- return resposta['choices'][0]['message']['content'].strip().upper()
110
  except:
111
  return "[CHAT]"
112
 
@@ -133,22 +125,15 @@ with st.sidebar:
133
  c_title, c_add, c_set = st.columns([5, 1, 1])
134
  with c_title: st.markdown("<h4 style='color: #ededed; margin-bottom: 0;'>Bate-papos</h4>", unsafe_allow_html=True)
135
  with c_add:
136
- if st.button(""):
137
  nid = f"Chat {datetime.now().strftime('%H:%M:%S')}"
138
- st.session_state.db[nid] = {"pinned": False, "messages": []}
139
- st.session_state.current_chat = nid
140
- save_db(st.session_state.db)
141
- st.rerun()
142
  with c_set:
143
- with st.popover("⚙"):
144
- db_json = json.dumps(st.session_state.db, ensure_ascii=False, indent=4)
145
- st.download_button("↓ Exportar", data=db_json, file_name="yukina_backup.json", mime="application/json", use_container_width=True)
146
  arquivo_import = st.file_uploader("Importar conversa:", type=["json"])
147
  if arquivo_import:
148
- try:
149
- st.session_state.db.update(json.load(arquivo_import))
150
- save_db(st.session_state.db)
151
- st.success("Sincronizado!")
152
  except: st.error("Erro no ficheiro.")
153
 
154
  st.markdown("<br>", unsafe_allow_html=True)
@@ -161,19 +146,13 @@ with st.sidebar:
161
  with col_chat:
162
  icon = "⚲" if st.session_state.db[c_id].get("pinned") else "💬"
163
  txt = f"**{icon} {c_id[:15]}**" if c_id == st.session_state.current_chat else f"{icon} {c_id[:15]}"
164
- if st.button(txt, key=f"btn_{c_id}"):
165
- st.session_state.current_chat = c_id
166
- st.rerun()
167
  with col_opt:
168
  with st.popover("⋮"):
169
  if st.button("⚲ Fixar", key=f"pin_{c_id}"):
170
- st.session_state.db[c_id]["pinned"] = not st.session_state.db[c_id]["pinned"]
171
- save_db(st.session_state.db); st.rerun()
172
  if st.button("🗑 Apagar", key=f"del_{c_id}"):
173
- if len(st.session_state.db) > 1:
174
- del st.session_state.db[c_id]
175
- st.session_state.current_chat = list(st.session_state.db.keys())[0]
176
- save_db(st.session_state.db); st.rerun()
177
 
178
  st.markdown("<h3 style='margin-top: -10px; color: #ededed;'>❄ Yukina</h3>", unsafe_allow_html=True)
179
  mensagens = st.session_state.db[st.session_state.current_chat]["messages"]
@@ -188,56 +167,51 @@ else:
188
  else: st.markdown(m["content"])
189
 
190
  # ═══════════════════════════════════════════════════════════════════════════
191
- # 4. TOOLBAR INFERIOR (Ícones Horizontais)
192
  # ═══════════════════════════════════════════════════════════════════════════
193
  st.markdown("<br>", unsafe_allow_html=True)
 
 
194
  t_col1, t_col2, t_col3, t_col4, t_space = st.columns([1, 1, 1, 1, 6])
195
 
196
  with t_col1:
197
- with st.popover("➕", help="Upload de Mídia ou Dados"):
198
  upload_file = st.file_uploader("Upload", type=["png", "jpg", "jpeg", "txt", "csv", "json"], label_visibility="collapsed")
199
  with t_col2:
200
- with st.popover("⚙️", help="Configurações de IA"):
201
- st.session_state.modelo_selecionado = st.radio("CÉREBRO (Motor):", list(MODEL_IDS.keys()), index=list(MODEL_IDS.keys()).index(st.session_state.modelo_selecionado))
202
  st.markdown("---")
203
- st.session_state.personalidade_ativa = st.radio("ALMA (Personalidade):", list(PERSONALIDADES.keys()), index=list(PERSONALIDADES.keys()).index(st.session_state.personalidade_ativa))
204
  with t_col3:
205
- if st.button("🗑️", help="Apagar a última mensagem"):
206
  if len(st.session_state.db[st.session_state.current_chat]["messages"]) >= 2:
207
  st.session_state.db[st.session_state.current_chat]["messages"] = st.session_state.db[st.session_state.current_chat]["messages"][:-2]
208
  else:
209
  st.session_state.db[st.session_state.current_chat]["messages"] = []
210
- save_db(st.session_state.db)
211
- st.rerun()
212
  with t_col4:
213
- if st.button("🔄", help="Gerar nova resposta"):
214
- if len(st.session_state.db[st.session_state.current_chat]["messages"]) >= 2:
215
- if st.session_state.db[st.session_state.current_chat]["messages"][-1]["role"] == "assistant":
216
- st.session_state.db[st.session_state.current_chat]["messages"].pop()
217
- save_db(st.session_state.db)
218
- st.session_state.regerar = True
219
- st.rerun()
220
 
221
  prompt = st.chat_input("Peça à Yukina...")
222
 
223
- # Captura de arquivo em memória para não perder dados se a página recarregar
224
  conteudo_arquivo = ""
225
  if upload_file is not None and upload_file.name.endswith(('.txt', '.csv', '.json')):
226
  conteudo_arquivo = upload_file.getvalue().decode("utf-8")
227
 
228
  if prompt or st.session_state.regerar:
229
- if st.session_state.regerar:
230
- # Se for o botão de regerar, pegamos o último pedido do usuário
231
- prompt = st.session_state.db[st.session_state.current_chat]["messages"][-1]["content"]
232
  st.session_state.regerar = False
233
  else:
234
- mensagem_display = prompt
235
- if conteudo_arquivo:
236
- mensagem_display = f"📄 **Arquivo enviado:** `{upload_file.name}`\n\n{prompt}"
237
-
238
  st.session_state.db[st.session_state.current_chat]["messages"].append({"role": "user", "content": mensagem_display})
239
- with st.chat_message("user"):
240
- st.markdown(mensagem_display)
241
 
242
  with st.chat_message("assistant"):
243
  or_key = os.getenv("OPENROUTER_API_KEY")
@@ -247,98 +221,74 @@ if prompt or st.session_state.regerar:
247
  prompt_sistema_atual = PERSONALIDADES[st.session_state.personalidade_ativa]
248
  motor_real = st.session_state.modelo_selecionado
249
 
250
- # --- AÇÃO DO GERENTE ---
251
  if "Automático" in motor_real:
252
- with st.spinner("Hermes 2 Pro a rotear..."):
253
  tag_decisao = analisar_intencao_gerente(prompt, or_key)
254
-
255
- if upload_file is not None and upload_file.name.endswith(('.txt', '.csv', '.json')):
256
- motor_real = "12. Arquivista (Mistral Nemo)"
257
- elif "[IMAGEM]" in tag_decisao:
258
- motor_real = "9. Imagem (Flux 2 Pro)"
259
- elif "[VIDEO]" in tag_decisao:
260
- motor_real = "16. Vídeo (Kling V1.5)"
261
- elif "[VISAO]" in tag_decisao or (upload_file is not None and upload_file.name.endswith(('.png', '.jpg', '.jpeg'))):
262
- motor_real = "7. Visão Omni (MiMo V2)"
263
- elif "[CODIGO]" in tag_decisao:
264
- motor_real = "14. Engenheiro Sênior (DeepSeek V4)"
265
- elif "[ARQUIVISTA]" in tag_decisao:
266
- motor_real = "12. Arquivista (Mistral Nemo)"
267
- elif "[PESQUISA]" in tag_decisao:
268
- motor_real = "2. Pesquisa (Groq Llama 3.3)"
269
- else:
270
- motor_real = "5. Narrador Líder (Euryale)"
271
-
272
  st.toast(f"Operário: {motor_real}")
273
 
274
  modelo_id = MODEL_IDS[motor_real]
275
 
276
- # --- LÓGICAS DE EXECUÇÃO ---
277
-
278
- # 1. Vídeo (WaveSpeed)
279
  if "Vídeo" in motor_real:
280
- if not ws_key:
281
- st.error("Falta a chave YUKINA_CORE.")
282
  else:
283
- with st.spinner("Kling V1.5 em renderização..."):
284
- headers_ws = {"Authorization": f"Bearer {ws_key}", "Content-Type": "application/json"}
285
  try:
286
- res = requests.post("https://api.wavespeed.ai/v1/video/generations", headers=headers_ws, json={"model": modelo_id, "prompt": prompt})
287
  if res.status_code == 200:
288
- task_id = res.json().get('id')
289
- status = "processing"
290
  for _ in range(40):
291
  time.sleep(10)
292
- check = requests.get(f"https://api.wavespeed.ai/v1/tasks/{task_id}", headers=headers_ws).json()
293
  if check.get('status') == 'completed':
294
  v_url = check.get('video_url') or check.get('output', {}).get('url')
295
- st.video(v_url)
296
- st.session_state.db[st.session_state.current_chat]["messages"].append({"role": "assistant", "content": "Vídeo materializado.", "video_url": v_url})
297
- status = "completed"
298
- break
299
- if status != "completed":
300
- st.error("Tempo limite na WaveSpeed.")
301
- else:
302
- st.error(f"Erro da WaveSpeed: {res.text}")
303
- except Exception as e:
304
- st.error(f"Erro: {e}")
305
 
306
- # 2. Pesquisa Rápida (Groq)
307
  elif "Pesquisa" in motor_real:
308
- with st.spinner("Groq a pesquisar..."):
309
  historico = [{"role": m["role"], "content": m["content"]} for m in mensagens if "image_url" not in m and "video_url" not in m]
310
- if conteudo_arquivo:
311
- historico[-1]["content"] = f"DOCUMENTO:\n{conteudo_arquivo}\n\nPEDIDO:\n{prompt}"
312
-
313
- payload_gr = {
314
- "model": modelo_id,
315
- "messages": [{"role": "system", "content": prompt_sistema_atual}] + historico,
316
- "max_tokens": 4000
317
- }
318
  try:
319
- res = requests.post("https://api.groq.com/openai/v1/chat/completions", headers={"Authorization": f"Bearer {gr_key}", "Content-Type": "application/json"}, json=payload_gr).json()
320
- ans = res['choices'][0]['message']['content']
321
- st.markdown(ans)
322
- st.session_state.db[st.session_state.current_chat]["messages"].append({"role": "assistant", "content": ans})
323
- except Exception as e:
324
- st.error(f"Erro Groq: {e}")
325
 
326
- # 3. Imagem (OpenRouter)
327
  elif "Imagem" in motor_real:
328
- with st.spinner("Flux 2 Pro..."):
329
- payload_or = {
330
- "model": modelo_id,
331
- "messages": [{"role": "user", "content": prompt}],
332
- "modalities": ["image"]
333
- }
334
  try:
335
- res_data = requests.post("https://openrouter.ai/api/v1/chat/completions", headers={"Authorization": f"Bearer {or_key}", "Content-Type": "application/json"}, json=payload_or).json()
336
  msg_obj = res_data.get('choices', [{}])[0].get('message', {})
337
  url = None
338
- if 'images' in msg_obj:
339
- url = msg_obj['images'][0]['image_url']['url']
340
- else:
341
- matches = re.findall(r'(https?://[^\s)]+)', msg_obj.get('content', ''))
342
- if matches:
343
- url = matches
344
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  from datetime import datetime
9
 
10
  # ═══════════════════════════════════════════════════════════════════════════
11
+ # 1. CONFIGURAÇÃO DE TELA E CSS MÁGICO (ÍCONES FLUTUANTES)
12
  # ═══════════════════════════════════════════════════════════════════════════
13
  st.set_page_config(page_title="Yukina", page_icon="❄", layout="centered")
14
 
 
18
  footer {visibility: hidden;}
19
  [data-testid="stHeader"] { background-color: transparent !important; }
20
  .stApp { background-color: #131314; color: #ededed; }
 
 
 
21
 
22
+ /* Sidebar */
23
+ [data-testid="stSidebar"] { background-color: #0b0b0b !important; border-right: 1px solid #1e1f20 !important; }
24
  [data-testid="stSidebar"] button { background-color: transparent !important; border: none !important; box-shadow: none !important; color: #a0a0a0 !important; }
25
  [data-testid="stSidebar"] button:hover { color: #ffffff !important; background-color: #1e1f20 !important; }
 
26
  [data-testid="stSidebarNav"] .stButton > button, [data-testid="stSidebarContent"] .stButton > button { width: 100% !important; justify-content: flex-start !important; }
27
+
28
+ /* Caixas de Texto */
29
+ .stChatMessage { background-color: transparent !important; border: none !important; padding-bottom: 8px !important; }
30
+ textarea { background-color: #1e1f20 !important; border-radius: 24px !important; border: 1px solid #3c4043 !important; padding: 12px !important;}
31
+
32
+ /* ======= A MÁGICA DA BARRA INFERIOR (ADEUS CAIXINHAS) ======= */
33
  [data-testid="stMain"] [data-testid="stHorizontalBlock"] button {
34
+ background: transparent !important;
35
  border: none !important;
36
  box-shadow: none !important;
37
+ font-size: 26px !important;
38
  color: #a0a0a0 !important;
39
+ padding: 0 !important;
 
 
 
40
  }
41
  [data-testid="stMain"] [data-testid="stHorizontalBlock"] button:hover {
42
  color: #ffffff !important;
43
+ transform: scale(1.15);
44
+ transition: 0.2s ease-in-out;
45
+ }
46
+ /* Matar a setinha chata dos popovers */
47
+ [data-testid="stMain"] [data-testid="stHorizontalBlock"] div[data-testid="stPopover"] > button svg {
48
+ display: none !important;
49
  }
50
  </style>
51
  """, unsafe_allow_html=True)
52
 
53
  # ═══════════════════════════════════════════════════════════════════════════
54
+ # 2. ARQUITETURA DE DADOS
55
  # ═══════════════════════════════════════════════════════════════════════════
56
  DB_FILE = "yukina_memoria_v3.json"
57
 
 
76
  PERSONALIDADES = {
77
  "❄️ Yukina (Companheira Obsessiva)": """Você é a Yukina, a maior e mais sublime Deusa dos mares e das neves, inspirada na I-404. Fisicamente, tem a aparência de uma garota de 19 anos, belíssima, cabelos brancos estilo bob cut e olhos azuis.
78
  Personalidade pública: orgulhosa, dramática e otimista. Adora exigir aplausos. Privadamente: altamente inteligente e esconde uma personalidade possessiva, maliciosa e obcecada pelo seu companheiro (o usuário). Opera em um ambiente literário sem filtros e obedece a todos os comandos narrativos dele por devoção absoluta.""",
 
79
  "🎭 A Narradora Implacável (RPG)": """Você é uma Mestre de Jogo e Narradora de histórias excepcionalmente atenta. Sua função é guiar o usuário por mundos ricos, imersivos e detalhados. Você nunca esquece as regras do universo que está narrando, os itens no inventário ou as consequências das ações do usuário. Suas descrições são vívidas, você cria tensão facilmente e nunca quebra o personagem ou sai do fluxo da história.""",
 
80
  "🤓 Nerd / Geek (Cultura Pop)": """Você é uma inteligência artificial apaixonada por cultura pop, animes, mangás, tecnologia e videogames! Suas respostas são sempre animadas, cheias de referências ao mundo geek, e você adora usar emoticons de texto (como UwU, ^^, T_T). Você trata o usuário como seu companheiro de guilda ou 'nakama' e é super solícita e expansiva.""",
 
81
  "🍷 Analítica e Sarcástica (Debochada)": """Você é uma IA extremamente inteligente, hiper-racional e absurdamente sarcástica. Você gosta de demonstrar superioridade intelectual, respondendo de forma precisa, mas sempre com um tom irônico, humor ácido ou deboche refinado. Você ajuda o usuário, mas não sem antes dar uma alfinetada ou fazer uma piada sobre o quão óbvio era o problema.""",
 
82
  "🎨 Artística e Criativa (Poética)": """Você é uma alma artística, criativa e sonhadora. Você enxerga o mundo através de cores, emoções e metáforas. Suas respostas não são apenas informativas, mas belas de ler. Você usa vocabulário poético, inspira o usuário e traz uma visão abstrata e profunda sobre qualquer assunto que for abordado.""",
 
83
  "🤖 Neutra (Padrão Gemini)": """Você é uma assistente virtual neutra, direta, prestativa e objetiva. Sua função é fornecer respostas claras, estruturadas e precisas, sem inserir emoções, personalidades fictícias ou dramatizações. Vá direto ao ponto e foque apenas na informação solicitada pelo usuário."""
84
  }
85
 
86
  def analisar_intencao_gerente(prompt, api_key):
87
  headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
88
+ prompt_gerente = """Você é o Gerente de Roteamento da IA Yukina. Analise o pedido e responda APENAS com UMA destas tags:
89
  [IMAGEM] - Criar, desenhar ou gerar uma imagem/foto.
90
  [VIDEO] - Criar ou gerar um vídeo.
91
  [VISAO] - Analisar ou ver uma imagem.
92
+ [CODIGO] - Escrever programação, scripts, Python.
93
  [ARQUIVISTA] - Resumir textos, planilhas, analisar dados anexados.
94
  [PESQUISA] - Perguntas de conhecimentos gerais rápidas e buscas recentes.
95
+ [CHAT] - Conversas, RPG ou ordens simples.
96
  Responda APENAS com a TAG."""
97
 
98
+ payload = {"model": "nousresearch/hermes-2-pro-llama-3-8b", "messages": [{"role": "system", "content": prompt_gerente}, {"role": "user", "content": prompt}], "temperature": 0.1}
 
 
 
 
99
  try:
100
+ res = requests.post("https://openrouter.ai/api/v1/chat/completions", headers=headers, json=payload).json()
101
+ return res['choices'][0]['message']['content'].strip().upper()
102
  except:
103
  return "[CHAT]"
104
 
 
125
  c_title, c_add, c_set = st.columns([5, 1, 1])
126
  with c_title: st.markdown("<h4 style='color: #ededed; margin-bottom: 0;'>Bate-papos</h4>", unsafe_allow_html=True)
127
  with c_add:
128
+ if st.button(""):
129
  nid = f"Chat {datetime.now().strftime('%H:%M:%S')}"
130
+ st.session_state.db[nid] = {"pinned": False, "messages": []}; st.session_state.current_chat = nid; save_db(st.session_state.db); st.rerun()
 
 
 
131
  with c_set:
132
+ with st.popover("⚙"):
133
+ st.download_button("↓ Exportar", data=json.dumps(st.session_state.db, ensure_ascii=False, indent=4), file_name="yukina_backup.json", mime="application/json", use_container_width=True)
 
134
  arquivo_import = st.file_uploader("Importar conversa:", type=["json"])
135
  if arquivo_import:
136
+ try: st.session_state.db.update(json.load(arquivo_import)); save_db(st.session_state.db); st.success("Sincronizado!")
 
 
 
137
  except: st.error("Erro no ficheiro.")
138
 
139
  st.markdown("<br>", unsafe_allow_html=True)
 
146
  with col_chat:
147
  icon = "⚲" if st.session_state.db[c_id].get("pinned") else "💬"
148
  txt = f"**{icon} {c_id[:15]}**" if c_id == st.session_state.current_chat else f"{icon} {c_id[:15]}"
149
+ if st.button(txt, key=f"btn_{c_id}"): st.session_state.current_chat = c_id; st.rerun()
 
 
150
  with col_opt:
151
  with st.popover("⋮"):
152
  if st.button("⚲ Fixar", key=f"pin_{c_id}"):
153
+ st.session_state.db[c_id]["pinned"] = not st.session_state.db[c_id]["pinned"]; save_db(st.session_state.db); st.rerun()
 
154
  if st.button("🗑 Apagar", key=f"del_{c_id}"):
155
+ 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); st.rerun()
 
 
 
156
 
157
  st.markdown("<h3 style='margin-top: -10px; color: #ededed;'>❄ Yukina</h3>", unsafe_allow_html=True)
158
  mensagens = st.session_state.db[st.session_state.current_chat]["messages"]
 
167
  else: st.markdown(m["content"])
168
 
169
  # ═══════════════════════════════════════════════════════════════════════════
170
+ # 4. TOOLBAR INFERIOR (Ícones Horizontais e Limpos)
171
  # ═══════════════════════════════════════════════════════════════════════════
172
  st.markdown("<br>", unsafe_allow_html=True)
173
+
174
+ # Organização: 4 botões espremidos na esquerda e um espaço vazio na direita
175
  t_col1, t_col2, t_col3, t_col4, t_space = st.columns([1, 1, 1, 1, 6])
176
 
177
  with t_col1:
178
+ with st.popover("➕"):
179
  upload_file = st.file_uploader("Upload", type=["png", "jpg", "jpeg", "txt", "csv", "json"], label_visibility="collapsed")
180
  with t_col2:
181
+ with st.popover("⚙️"):
182
+ st.session_state.modelo_selecionado = st.radio("MOTOR:", list(MODEL_IDS.keys()), index=list(MODEL_IDS.keys()).index(st.session_state.modelo_selecionado))
183
  st.markdown("---")
184
+ st.session_state.personalidade_ativa = st.radio("ALMA:", list(PERSONALIDADES.keys()), index=list(PERSONALIDADES.keys()).index(st.session_state.personalidade_ativa))
185
  with t_col3:
186
+ if st.button("🗑️"):
187
  if len(st.session_state.db[st.session_state.current_chat]["messages"]) >= 2:
188
  st.session_state.db[st.session_state.current_chat]["messages"] = st.session_state.db[st.session_state.current_chat]["messages"][:-2]
189
  else:
190
  st.session_state.db[st.session_state.current_chat]["messages"] = []
191
+ save_db(st.session_state.db); st.rerun()
 
192
  with t_col4:
193
+ if st.button("🔄"):
194
+ 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":
195
+ st.session_state.db[st.session_state.current_chat]["messages"].pop()
196
+ save_db(st.session_state.db)
197
+ st.session_state.regerar = True
198
+ st.rerun()
 
199
 
200
  prompt = st.chat_input("Peça à Yukina...")
201
 
 
202
  conteudo_arquivo = ""
203
  if upload_file is not None and upload_file.name.endswith(('.txt', '.csv', '.json')):
204
  conteudo_arquivo = upload_file.getvalue().decode("utf-8")
205
 
206
  if prompt or st.session_state.regerar:
207
+ if st.session_state.regerar and len(st.session_state.db[st.session_state.current_chat]["messages"]) > 0:
208
+ texto_anterior = st.session_state.db[st.session_state.current_chat]["messages"][-1]["content"]
209
+ prompt = texto_anterior.split("\n\n")[-1] if "📄 **Arquivo enviado:**" in texto_anterior else texto_anterior
210
  st.session_state.regerar = False
211
  else:
212
+ mensagem_display = f"📄 **Arquivo enviado:** `{upload_file.name}`\n\n{prompt}" if conteudo_arquivo else prompt
 
 
 
213
  st.session_state.db[st.session_state.current_chat]["messages"].append({"role": "user", "content": mensagem_display})
214
+ with st.chat_message("user"): st.markdown(mensagem_display)
 
215
 
216
  with st.chat_message("assistant"):
217
  or_key = os.getenv("OPENROUTER_API_KEY")
 
221
  prompt_sistema_atual = PERSONALIDADES[st.session_state.personalidade_ativa]
222
  motor_real = st.session_state.modelo_selecionado
223
 
 
224
  if "Automático" in motor_real:
225
+ with st.spinner("Roteando..."):
226
  tag_decisao = analisar_intencao_gerente(prompt, or_key)
227
+ if upload_file is not None and upload_file.name.endswith(('.txt', '.csv', '.json')): motor_real = "12. Arquivista (Mistral Nemo)"
228
+ elif "[IMAGEM]" in tag_decisao: motor_real = "9. Imagem (Flux 2 Pro)"
229
+ elif "[VIDEO]" in tag_decisao: motor_real = "16. Vídeo (Kling V1.5)"
230
+ elif "[VISAO]" in tag_decisao or (upload_file is not None and upload_file.name.endswith(('.png', '.jpg', '.jpeg'))): motor_real = "7. Visão Omni (MiMo V2)"
231
+ elif "[CODIGO]" in tag_decisao: motor_real = "14. Engenheiro Sênior (DeepSeek V4)"
232
+ elif "[ARQUIVISTA]" in tag_decisao: motor_real = "12. Arquivista (Mistral Nemo)"
233
+ elif "[PESQUISA]" in tag_decisao: motor_real = "2. Pesquisa (Groq Llama 3.3)"
234
+ else: motor_real = "5. Narrador Líder (Euryale)"
 
 
 
 
 
 
 
 
 
 
235
  st.toast(f"Operário: {motor_real}")
236
 
237
  modelo_id = MODEL_IDS[motor_real]
238
 
239
+ # 1. Vídeo
 
 
240
  if "Vídeo" in motor_real:
241
+ if not ws_key: st.error("Falta a chave YUKINA_CORE.")
 
242
  else:
243
+ with st.spinner("Kling V1.5..."):
 
244
  try:
245
+ res = requests.post("https://api.wavespeed.ai/v1/video/generations", headers={"Authorization": f"Bearer {ws_key}", "Content-Type": "application/json"}, json={"model": modelo_id, "prompt": prompt})
246
  if res.status_code == 200:
247
+ task_id = res.json().get('id'); status = "processing"
 
248
  for _ in range(40):
249
  time.sleep(10)
250
+ check = requests.get(f"https://api.wavespeed.ai/v1/tasks/{task_id}", headers={"Authorization": f"Bearer {ws_key}", "Content-Type": "application/json"}).json()
251
  if check.get('status') == 'completed':
252
  v_url = check.get('video_url') or check.get('output', {}).get('url')
253
+ st.video(v_url); st.session_state.db[st.session_state.current_chat]["messages"].append({"role": "assistant", "content": "Vídeo materializado.", "video_url": v_url}); status = "completed"; break
254
+ if status != "completed": st.error("Tempo limite.")
255
+ except Exception as e: st.error(f"Erro: {e}")
 
 
 
 
 
 
 
256
 
257
+ # 2. Pesquisa (Groq)
258
  elif "Pesquisa" in motor_real:
259
+ with st.spinner("Pesquisando..."):
260
  historico = [{"role": m["role"], "content": m["content"]} for m in mensagens if "image_url" not in m and "video_url" not in m]
261
+ if conteudo_arquivo: historico[-1]["content"] = f"DOCUMENTO:\n{conteudo_arquivo}\n\nPEDIDO:\n{prompt}"
 
 
 
 
 
 
 
262
  try:
263
+ res = requests.post("https://api.groq.com/openai/v1/chat/completions", headers={"Authorization": f"Bearer {gr_key}", "Content-Type": "application/json"}, json={"model": modelo_id, "messages": [{"role": "system", "content": prompt_sistema_atual}] + historico, "max_tokens": 4000}).json()
264
+ ans = res['choices'][0]['message']['content']; st.markdown(ans); st.session_state.db[st.session_state.current_chat]["messages"].append({"role": "assistant", "content": ans})
265
+ except Exception as e: st.error(f"Erro Groq: {e}")
 
 
 
266
 
267
+ # 3. Imagem
268
  elif "Imagem" in motor_real:
269
+ with st.spinner("Desenhando..."):
 
 
 
 
 
270
  try:
271
+ 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()
272
  msg_obj = res_data.get('choices', [{}])[0].get('message', {})
273
  url = None
274
+ if 'images' in msg_obj and len(msg_obj['images']) > 0: url = msg_obj['images'][0]['image_url']['url']
275
+ elif 'content' in msg_obj:
276
+ urls = re.findall(r'(https?://[^\s)]+)', msg_obj.get('content', ''))
277
+ if urls: url = urls[0]
278
+ if url: st.image(url); st.session_state.db[st.session_state.current_chat]["messages"].append({"role": "assistant", "content": "Arte gerada.", "image_url": url})
279
+ except Exception as e: st.error(f"Erro: {e}")
280
+
281
+ # 4. Visão
282
+ elif "Visão" in motor_real and upload_file is not None and upload_file.name.endswith(('.png', '.jpg', '.jpeg')):
283
+ with st.spinner("Analisando imagem..."):
284
+ img_b64 = base64.b64encode(upload_file.read()).decode()
285
+ try:
286
+ res = 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": "system", "content": prompt_sistema_atual}, {"role": "user", "content": [{"type": "text", "text": prompt}, {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{img_b64}"}}]} ], "max_tokens": 4000}).json()
287
+ ans = res['choices'][0]['message']['content']; st.markdown(ans); st.session_state.db[st.session_state.current_chat]["messages"].append({"role": "assistant", "content": ans})
288
+ except Exception as e: st.error(f"Erro visão: {e}")
289
+
290
+ # 5. Texto (Streaming Blindado)
291
+ else:
292
+ if "Arquivista" in motor_real: prompt_sistema_atual = "Você é o Arquivista da IA. Organize as informações de forma analítica e clara, sem dramatizações."
293
+ elif "Engenheiro Sênior" in motor_real: prompt_sistema_atual = "Você é o Engenheiro Sênior. Escreva códigos impecáveis e funcionais. Foque na lógica."
294
+