Astarok commited on
Commit
cc74f9e
·
verified ·
1 Parent(s): 99981c1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +77 -192
app.py CHANGED
@@ -7,7 +7,7 @@ import re
7
  from datetime import datetime
8
 
9
  # ═══════════════════════════════════════════════════════════════════════════
10
- # 1. CONFIGURAÇÃO DE TELA E CSS
11
  # ═══════════════════════════════════════════════════════════════════════════
12
  st.set_page_config(page_title="Yukina", page_icon="❄", layout="centered")
13
 
@@ -19,72 +19,57 @@ st.markdown("""
19
 
20
  .stApp { background-color: #131314; color: #ededed; }
21
 
22
- /* 1. LARGURA DA BARRA LATERAL (Mais larga no celular) */
23
- @media (max-width: 768px) {
24
- [data-testid="stSidebar"] { min-width: 85vw !important; max-width: 85vw !important; }
 
 
 
25
  }
26
- @media (min-width: 769px) {
27
- [data-testid="stSidebar"] { min-width: 350px !important; max-width: 350px !important; }
28
- }
29
-
30
- [data-testid="stSidebar"] { background-color: #0b0b0b; border-right: 1px solid #1e1f20; }
31
 
32
  .stChatMessage { background-color: transparent !important; border: none !important; padding-bottom: 8px !important; }
33
  textarea { background-color: #1e1f20 !important; border-radius: 24px !important; border: 1px solid #3c4043 !important; padding: 12px !important;}
34
 
35
- /* Botões Pílula e Popovers (Geral) */
36
  .stButton > button, [data-testid="stPopover"] > button {
37
  border-radius: 30px !important;
38
  background-color: #1e1f20 !important;
39
  border: 1px solid #3c4043 !important;
40
  color: #e3e3e3 !important;
41
  padding: 8px 20px !important;
42
- display: inline-flex !important;
43
- align-items: center !important;
44
- width: auto !important;
45
  font-size: 15px !important;
46
  }
47
- .stButton > button:hover, [data-testid="stPopover"] > button:hover { background-color: #2a2b2f !important; border-color: #888 !important; }
48
-
49
- /* Botões da Sidebar (Chats) - Transparentes e sem borda */
50
- [data-testid="stSidebar"] .stButton > button,
51
- [data-testid="stSidebar"] [data-testid="stPopover"] > button {
52
- width: 100% !important;
53
- border: none !important;
54
- background-color: transparent !important;
55
- justify-content: flex-start !important;
56
- padding: 10px 15px !important;
57
- border-radius: 12px !important;
58
- }
59
- [data-testid="stSidebar"] .stButton > button:hover,
60
- [data-testid="stSidebar"] [data-testid="stPopover"] > button:hover { background-color: #1e1f20 !important; }
61
 
62
- /* 2. MÁGICA: Remove a caixa dos ícones + e ⚙ no TOPO da Sidebar */
63
- [data-testid="stSidebar"] [data-testid="stHorizontalBlock"]:first-of-type button {
64
  background-color: transparent !important;
65
  border: none !important;
66
- padding: 0 !important;
67
- display: flex !important;
68
- justify-content: center !important;
69
- align-items: center !important;
70
  color: #a0a0a0 !important;
71
- font-size: 20px !important;
72
  }
73
- [data-testid="stSidebar"] [data-testid="stHorizontalBlock"]:first-of-type button:hover {
74
  color: #ffffff !important;
75
- background-color: transparent !important;
 
 
 
 
 
 
 
76
  }
77
 
78
- /* Esconde a setinha do popover */
 
 
79
  [data-testid="stPopover"] div[data-testid="stMarkdownContainer"] p { margin: 0 !important; }
80
  </style>
81
  """, unsafe_allow_html=True)
82
 
83
  # ═══════════════════════════════════════════════════════════════════════════
84
- # 2. LÓGICA DE DADOS E MEMÓRIA
85
  # ═══════════════════════════════════════════════════════════════��═══════════
86
  DB_FILE = "yukina_memoria_v3.json"
87
-
88
  MODEL_IDS = {
89
  "Busca (Llama Groq)": "meta-llama/llama-3.1-70b-instruct",
90
  "Pesquisa (DeepRes)": "alibaba/tongyi-deepresearch-30b-a3b",
@@ -94,7 +79,6 @@ MODEL_IDS = {
94
  "Imagem (Flux Pro)": "black-forest-labs/flux.2-pro",
95
  "Histórias (Euryale)": "sao10k/l3.3-euryale-70b"
96
  }
97
-
98
  SYSTEM_PROMPT = "Você é a Yukina, assistente audaz e narradora de RPG sem filtros."
99
 
100
  def load_db():
@@ -113,15 +97,15 @@ if "current_chat" not in st.session_state: st.session_state.current_chat = list(
113
  if "modelo_selecionado" not in st.session_state: st.session_state.modelo_selecionado = "Histórias (Euryale)"
114
 
115
  # ═══════════════════════════════════════════════════════════════════════════
116
- # 3. SIDEBAR (Design Slim & Icons Minimalistas)
117
  # ═══════════════════════════════════════════════════════════════════════════
118
  with st.sidebar:
119
- # Cabeçalho: Bate-papos | + | ⚙
120
- c_title, c_add, c_set = st.columns([6, 1.5, 1.5])
121
  with c_title:
122
- st.markdown("<h4 style='color: #ededed; margin-top: 5px; margin-bottom: 0px;'>Bate-papos</h4>", unsafe_allow_html=True)
123
  with c_add:
124
- if st.button("+", help="Novo Chat"):
125
  nid = f"Chat {datetime.now().strftime('%H:%M:%S')}"
126
  st.session_state.db[nid] = {"pinned": False, "messages": []}
127
  st.session_state.current_chat = nid
@@ -129,196 +113,97 @@ with st.sidebar:
129
  st.rerun()
130
  with c_set:
131
  with st.popover("⚙"):
132
- st.markdown("<p style='font-size: 14px; font-weight: bold; margin-bottom: 0;'>Configurações</p>", unsafe_allow_html=True)
133
- st.markdown("<p style='font-size: 11px; color: #888; margin-top: 0;'>BACKUP E RESTAURAÇÃO</p>", unsafe_allow_html=True)
134
-
135
  db_json = json.dumps(st.session_state.db, ensure_ascii=False, indent=4)
136
  st.download_button("↓ Exportar (.json)", data=db_json, file_name="yukina_backup.json", mime="application/json", use_container_width=True)
137
-
138
- st.markdown("<p style='font-size: 12px; color: #888; margin-top: 10px; margin-bottom: 5px;'>Importar conversa:</p>", unsafe_allow_html=True)
139
- arquivo_import = st.file_uploader("Carregar", type=["json"], label_visibility="collapsed")
140
- if arquivo_import is not None:
141
  try:
142
- dados_importados = json.load(arquivo_import)
143
- st.session_state.db.update(dados_importados)
144
  save_db(st.session_state.db)
145
- st.success("Importado com sucesso!")
146
- except:
147
- st.error("Erro no arquivo.")
148
 
149
  st.markdown("<br>", unsafe_allow_html=True)
150
- busca = st.text_input("Pesquisar", placeholder="Pesquisar...", label_visibility="collapsed")
151
- st.markdown("<br>", unsafe_allow_html=True)
152
-
153
  chats = st.session_state.db
154
  chats_exibidos = [c for c in chats if busca.lower() in c.lower()] if busca else list(chats.keys())
155
  chats_exibidos.sort(key=lambda x: not chats[x].get("pinned"))
156
 
157
- # Renderiza os chats com ícones minimalistas
158
  for c_id in chats_exibidos:
159
  col_chat, col_opt = st.columns([8, 2])
160
-
161
  with col_chat:
162
  icon = "⚲" if chats[c_id].get("pinned") else "💬"
163
- nome_exibicao = f"**{icon} {c_id[:18]}**" if c_id == st.session_state.current_chat else f"{icon} {c_id[:18]}"
164
- if st.button(nome_exibicao, key=f"btn_{c_id}"):
165
  st.session_state.current_chat = c_id
166
  st.rerun()
167
-
168
  with col_opt:
169
  with st.popover("⋮"):
170
  if st.button("⚲ Fixar", key=f"pin_{c_id}"):
171
  st.session_state.db[c_id]["pinned"] = not st.session_state.db[c_id]["pinned"]
172
  save_db(st.session_state.db)
173
  st.rerun()
174
- if st.button("⎘ Duplicar", key=f"dup_{c_id}"):
175
- novo_nome = f"{c_id} (Cópia)"
176
- st.session_state.db[novo_nome] = st.session_state.db[c_id].copy()
177
- st.session_state.db[novo_nome]["pinned"] = False
178
- save_db(st.session_state.db)
179
- st.rerun()
180
  if st.button("🗑 Apagar", key=f"del_{c_id}"):
181
  if len(st.session_state.db) > 1:
182
  del st.session_state.db[c_id]
183
- if st.session_state.current_chat == c_id:
184
- st.session_state.current_chat = list(st.session_state.db.keys())[0]
185
- save_db(st.session_state.db)
186
- st.rerun()
187
 
188
  # ═══════════════════════════════════════════════════════════════════════════
189
- # 4. ÁREA PRINCIPAL E TELA INICIAL
190
  # ═══════════════════════════════════════════════════════════════════════════
191
  st.markdown("<h3 style='margin-top: -10px; color: #ededed;'>❄ Yukina</h3>", unsafe_allow_html=True)
 
192
 
193
- mensagens_atuais = st.session_state.db[st.session_state.current_chat]["messages"]
194
-
195
- # TELA INICIAL (Pílulas Minimalistas)
196
- if len(mensagens_atuais) == 0:
197
- st.markdown("<br><br>", unsafe_allow_html=True)
198
- st.markdown("<h3 style='color: #e3e3e3; font-weight: 400; margin-bottom: 0;'>Olá, Astarok</h3>", unsafe_allow_html=True)
199
- st.markdown("<h1 style='color: #e3e3e3; font-size: 32px; line-height: 1.2; margin-top: 5px; margin-bottom: 30px;'>O que você acha que devemos fazer agora?</h1>", unsafe_allow_html=True)
200
-
201
- if st.button("❄ Conversar com Yukina", use_container_width=False):
202
- st.session_state.modelo_selecionado = "Histórias (Euryale)"
203
- st.rerun()
204
- if st.button("🖼 Criar imagem", use_container_width=False):
205
- st.session_state.modelo_selecionado = "Imagem (Flux Pro)"
206
- st.rerun()
207
- if st.button("👁 Analisar foto", use_container_width=False):
208
- st.session_state.modelo_selecionado = "Visão (Qwen VL)"
209
- st.rerun()
210
- if st.button("📜 Criar história", use_container_width=False):
211
- st.session_state.modelo_selecionado = "Histórias (Euryale)"
212
- st.rerun()
213
- if st.button("⌕ Pesquisar algo", use_container_width=False):
214
- st.session_state.modelo_selecionado = "Busca (Llama Groq)"
215
- st.rerun()
216
-
217
- # TELA DE CHAT
218
  else:
219
- for msg in mensagens_atuais:
220
- with st.chat_message(msg["role"]):
221
- if "image_url" in msg:
222
- st.image(msg["image_url"])
223
- else:
224
- st.markdown(msg["content"])
225
 
226
- # ═══════════════════════════════════════════════════════════════════════════
227
- # 5. TOOLBAR INFERIOR (Estilo Gemini)
228
- # ═══════════════════════════════════════════════════════════════════════════
229
  st.markdown("<br>", unsafe_allow_html=True)
230
-
231
  t_col1, t_col2, t_space = st.columns([1.5, 1.5, 7])
232
-
233
  with t_col1:
234
- with st.popover("+"):
235
- st.markdown("<p style='font-size: 13px; color: #888;'>Mídia (Necessita motor de Visão ativo).</p>", unsafe_allow_html=True)
236
- img_upload = st.file_uploader("Upload", type=["png", "jpg", "jpeg"], label_visibility="collapsed")
237
  with t_col2:
238
- with st.popover("⚙"):
239
- st.session_state.modelo_selecionado = st.radio(
240
- "Motor Ativo:",
241
- list(MODEL_IDS.keys()),
242
- index=list(MODEL_IDS.keys()).index(st.session_state.modelo_selecionado)
243
- )
244
 
245
- # INPUT DE MENSAGEM
246
  if prompt := st.chat_input("Peça à Yukina..."):
247
-
248
  st.session_state.db[st.session_state.current_chat]["messages"].append({"role": "user", "content": prompt})
249
- with st.chat_message("user"):
250
- st.markdown(prompt)
251
-
252
  with st.chat_message("assistant"):
253
  api_key = os.getenv("OPENROUTER_API_KEY")
254
  headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
255
-
256
- # MOTOR DE IMAGEM
257
  if "Imagem" in st.session_state.modelo_selecionado:
258
- with st.spinner("Desenhando..."):
259
- payload = {"model": MODEL_IDS[st.session_state.modelo_selecionado], "messages": [{"role": "user", "content": prompt}], "modalities": ["image"]}
260
- try:
261
- resp = requests.post("https://openrouter.ai/api/v1/chat/completions", headers=headers, json=payload).json()
262
- message_obj = resp['choices'][0]['message']
263
- img_url = None
264
- if 'images' in message_obj and len(message_obj['images']) > 0:
265
- img_url = message_obj['images'][0]['image_url']['url']
266
- elif 'content' in message_obj and message_obj['content']:
267
- content = message_obj['content']
268
- if content.startswith('data:image'): img_url = content
269
- else:
270
- urls = re.findall(r'(https?://[^\s)]+)', content)
271
- if urls: img_url = urls[0]
272
-
273
- if img_url:
274
- st.image(img_url)
275
- st.session_state.db[st.session_state.current_chat]["messages"].append({"role": "assistant", "content": "Arte gerada.", "image_url": img_url})
276
- else:
277
- st.error("Falha ao gerar a imagem.")
278
- except Exception as e:
279
- st.error(f"Erro: {e}")
280
-
281
- # MOTOR DE VISÃO
282
- elif "Visão" in st.session_state.modelo_selecionado:
283
- if not img_upload:
284
- st.error("⚠️ Você está usando o motor de Visão. Clique no '+' e anexe uma imagem primeiro!")
285
- else:
286
- with st.spinner("Analisando..."):
287
- try:
288
- encoded_image = base64.b64encode(img_upload.read()).decode('utf-8')
289
- data_url = f"data:image/png;base64,{encoded_image}"
290
- payload = {
291
- "model": MODEL_IDS[st.session_state.modelo_selecionado],
292
- "messages": [{"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": [{"type": "text", "text": prompt}, {"type": "image_url", "image_url": {"url": data_url}}]}],
293
- "stream": False
294
- }
295
- resp = requests.post("https://openrouter.ai/api/v1/chat/completions", headers=headers, json=payload).json()
296
- analise = resp['choices'][0]['message']['content']
297
- st.markdown(analise)
298
- st.session_state.db[st.session_state.current_chat]["messages"].append({"role": "assistant", "content": analise})
299
- except Exception as e:
300
- st.error(f"Erro na Visão: {e}")
301
-
302
- # MOTOR DE TEXTO
303
  else:
304
- ph = st.empty()
305
- full_res = ""
306
- historico_limpo = [m for m in st.session_state.db[st.session_state.current_chat]["messages"] if "image_url" not in m]
307
- payload = {"model": MODEL_IDS[st.session_state.modelo_selecionado], "messages": [{"role": "system", "content": SYSTEM_PROMPT}] + historico_limpo, "stream": True}
308
- try:
309
- resp = requests.post("https://openrouter.ai/api/v1/chat/completions", headers=headers, json=payload, stream=True)
310
- for line in resp.iter_lines():
311
- if line:
312
- try:
313
- d = json.loads(line.decode('utf-8').replace('data: ', ''))
314
- content = d['choices'][0]['delta'].get('content', '')
315
- full_res += content
316
- ph.markdown(full_res + "▌")
317
- except: continue
318
- ph.markdown(full_res)
319
- st.session_state.db[st.session_state.current_chat]["messages"].append({"role": "assistant", "content": full_res})
320
- except Exception as e:
321
- st.error(f"Erro: {e}")
322
-
323
  save_db(st.session_state.db)
324
 
 
7
  from datetime import datetime
8
 
9
  # ═══════════════════════════════════════════════════════════════════════════
10
+ # 1. CONFIGURAÇÃO DE TELA E CSS (Visual Premium & Sidebar Ajustada)
11
  # ═══════════════════════════════════════════════════════════════════════════
12
  st.set_page_config(page_title="Yukina", page_icon="❄", layout="centered")
13
 
 
19
 
20
  .stApp { background-color: #131314; color: #ededed; }
21
 
22
+ /* 1. LARGURA DA BARRA LATERAL (Ajustada para +15% do padrão) */
23
+ [data-testid="stSidebar"] {
24
+ min-width: 350px !important;
25
+ max-width: 350px !important;
26
+ background-color: #0b0b0b;
27
+ border-right: 1px solid #1e1f20;
28
  }
 
 
 
 
 
29
 
30
  .stChatMessage { background-color: transparent !important; border: none !important; padding-bottom: 8px !important; }
31
  textarea { background-color: #1e1f20 !important; border-radius: 24px !important; border: 1px solid #3c4043 !important; padding: 12px !important;}
32
 
33
+ /* Botões Pílula e Popovers */
34
  .stButton > button, [data-testid="stPopover"] > button {
35
  border-radius: 30px !important;
36
  background-color: #1e1f20 !important;
37
  border: 1px solid #3c4043 !important;
38
  color: #e3e3e3 !important;
39
  padding: 8px 20px !important;
 
 
 
40
  font-size: 15px !important;
41
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
 
43
+ /* 2. REMOVER CAIXINHAS CINZAS DOS ÍCONES (+ e ⚙) */
44
+ [data-testid="stSidebar"] button {
45
  background-color: transparent !important;
46
  border: none !important;
47
+ box-shadow: none !important;
 
 
 
48
  color: #a0a0a0 !important;
 
49
  }
50
+ [data-testid="stSidebar"] button:hover {
51
  color: #ffffff !important;
52
+ background-color: #1e1f20 !important;
53
+ }
54
+
55
+ /* Ajuste específico para os ícones do topo não terem fundo nenhum */
56
+ [data-testid="stSidebarContent"] [data-testid="stHorizontalBlock"] button {
57
+ background: none !important;
58
+ border: none !important;
59
+ font-size: 22px !important;
60
  }
61
 
62
+ /* 3. BOTÕES DE CHAT (Transparentes) */
63
+ [data-testid="stSidebarNav"] .stButton > button { width: 100% !important; justify-content: flex-start !important; }
64
+
65
  [data-testid="stPopover"] div[data-testid="stMarkdownContainer"] p { margin: 0 !important; }
66
  </style>
67
  """, unsafe_allow_html=True)
68
 
69
  # ═══════════════════════════════════════════════════════════════════════════
70
+ # 2. LÓGICA DE DADOS
71
  # ═══════════════════════════════════════════════════════════════��═══════════
72
  DB_FILE = "yukina_memoria_v3.json"
 
73
  MODEL_IDS = {
74
  "Busca (Llama Groq)": "meta-llama/llama-3.1-70b-instruct",
75
  "Pesquisa (DeepRes)": "alibaba/tongyi-deepresearch-30b-a3b",
 
79
  "Imagem (Flux Pro)": "black-forest-labs/flux.2-pro",
80
  "Histórias (Euryale)": "sao10k/l3.3-euryale-70b"
81
  }
 
82
  SYSTEM_PROMPT = "Você é a Yukina, assistente audaz e narradora de RPG sem filtros."
83
 
84
  def load_db():
 
97
  if "modelo_selecionado" not in st.session_state: st.session_state.modelo_selecionado = "Histórias (Euryale)"
98
 
99
  # ═══════════════════════════════════════════════════════════════════════════
100
+ # 3. SIDEBAR (Design Slim)
101
  # ═══════════════════════════════════════════════════════════════════════════
102
  with st.sidebar:
103
+ # Cabeçalho minimalista
104
+ c_title, c_add, c_set = st.columns([5, 1, 1])
105
  with c_title:
106
+ st.markdown("<h4 style='color: #ededed; margin-bottom: 0;'>Bate-papos</h4>", unsafe_allow_html=True)
107
  with c_add:
108
+ if st.button("+"):
109
  nid = f"Chat {datetime.now().strftime('%H:%M:%S')}"
110
  st.session_state.db[nid] = {"pinned": False, "messages": []}
111
  st.session_state.current_chat = nid
 
113
  st.rerun()
114
  with c_set:
115
  with st.popover("⚙"):
116
+ st.markdown("**Configurações**")
 
 
117
  db_json = json.dumps(st.session_state.db, ensure_ascii=False, indent=4)
118
  st.download_button("↓ Exportar (.json)", data=db_json, file_name="yukina_backup.json", mime="application/json", use_container_width=True)
119
+ arquivo_import = st.file_uploader("Importar conversa:", type=["json"])
120
+ if arquivo_import:
 
 
121
  try:
122
+ st.session_state.db.update(json.load(arquivo_import))
 
123
  save_db(st.session_state.db)
124
+ st.success("Sincronizado!")
125
+ except: st.error("Erro no ficheiro.")
 
126
 
127
  st.markdown("<br>", unsafe_allow_html=True)
128
+ busca = st.text_input("Pesquisar", placeholder="🔍 Pesquisar...", label_visibility="collapsed")
129
+
 
130
  chats = st.session_state.db
131
  chats_exibidos = [c for c in chats if busca.lower() in c.lower()] if busca else list(chats.keys())
132
  chats_exibidos.sort(key=lambda x: not chats[x].get("pinned"))
133
 
 
134
  for c_id in chats_exibidos:
135
  col_chat, col_opt = st.columns([8, 2])
 
136
  with col_chat:
137
  icon = "⚲" if chats[c_id].get("pinned") else "💬"
138
+ txt = f"**{icon} {c_id[:15]}**" if c_id == st.session_state.current_chat else f"{icon} {c_id[:15]}"
139
+ if st.button(txt, key=f"btn_{c_id}"):
140
  st.session_state.current_chat = c_id
141
  st.rerun()
 
142
  with col_opt:
143
  with st.popover("⋮"):
144
  if st.button("⚲ Fixar", key=f"pin_{c_id}"):
145
  st.session_state.db[c_id]["pinned"] = not st.session_state.db[c_id]["pinned"]
146
  save_db(st.session_state.db)
147
  st.rerun()
148
+ if st.button("⎘ Copiar", key=f"dup_{c_id}"):
149
+ nid = f"{c_id}_c"
150
+ st.session_state.db[nid] = st.session_state.db[c_id].copy()
151
+ save_db(st.session_state.db); st.rerun()
 
 
152
  if st.button("🗑 Apagar", key=f"del_{c_id}"):
153
  if len(st.session_state.db) > 1:
154
  del st.session_state.db[c_id]
155
+ st.session_state.current_chat = list(st.session_state.db.keys())[0]
156
+ save_db(st.session_state.db); st.rerun()
 
 
157
 
158
  # ═══════════════════════════════════════════════════════════════════════════
159
+ # 4. ÁREA PRINCIPAL
160
  # ═══════════════════════════════════════════════════════════════════════════
161
  st.markdown("<h3 style='margin-top: -10px; color: #ededed;'>❄ Yukina</h3>", unsafe_allow_html=True)
162
+ mensagens = st.session_state.db[st.session_state.current_chat]["messages"]
163
 
164
+ if len(mensagens) == 0:
165
+ st.markdown("<br><br><h3 style='color: #888;'>Olá, Astarok</h3><h1 style='color: #fff;'>O que vamos fazer?</h1>", unsafe_allow_html=True)
166
+ for acao, mot in [("❄ Conversar com Yukina", "Histórias (Euryale)"), ("🖼 Criar imagem", "Imagem (Flux Pro)"), ("👁 Analisar foto", "Visão (Qwen VL)"), ("📜 Criar história", "Histórias (Euryale)"), ("⌕ Pesquisar", "Busca (Llama Groq)")]:
167
+ if st.button(acao): st.session_state.modelo_selecionado = mot; st.rerun()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  else:
169
+ for m in mensagens:
170
+ with st.chat_message(m["role"]):
171
+ if "image_url" in m: st.image(m["image_url"])
172
+ else: st.markdown(m["content"])
 
 
173
 
 
 
 
174
  st.markdown("<br>", unsafe_allow_html=True)
 
175
  t_col1, t_col2, t_space = st.columns([1.5, 1.5, 7])
 
176
  with t_col1:
177
+ with st.popover("+"): img_upload = st.file_uploader("Mídia", type=["png", "jpg", "jpeg"])
 
 
178
  with t_col2:
179
+ with st.popover("⚙"): st.session_state.modelo_selecionado = st.radio("Motor:", list(MODEL_IDS.keys()), index=list(MODEL_IDS.keys()).index(st.session_state.modelo_selecionado))
 
 
 
 
 
180
 
 
181
  if prompt := st.chat_input("Peça à Yukina..."):
 
182
  st.session_state.db[st.session_state.current_chat]["messages"].append({"role": "user", "content": prompt})
183
+ with st.chat_message("user"): st.markdown(prompt)
 
 
184
  with st.chat_message("assistant"):
185
  api_key = os.getenv("OPENROUTER_API_KEY")
186
  headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
 
 
187
  if "Imagem" in st.session_state.modelo_selecionado:
188
+ res = requests.post("https://openrouter.ai/api/v1/chat/completions", headers=headers, json={"model": MODEL_IDS[st.session_state.modelo_selecionado], "messages": [{"role": "user", "content": prompt}], "modalities": ["image"]}).json()
189
+ url = res['choices'][0]['message']['images'][0]['image_url']['url']
190
+ st.image(url)
191
+ st.session_state.db[st.session_state.current_chat]["messages"].append({"role": "assistant", "content": "Arte.", "image_url": url})
192
+ elif "Visão" in st.session_state.modelo_selecionado and img_upload:
193
+ img_b64 = base64.b64encode(img_upload.read()).decode()
194
+ res = requests.post("https://openrouter.ai/api/v1/chat/completions", headers=headers, json={"model": MODEL_IDS[st.session_state.modelo_selecionado], "messages": [{"role": "user", "content": [{"type": "text", "text": prompt}, {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{img_b64}"}}]} ]}).json()
195
+ ans = res['choices'][0]['message']['content']
196
+ st.markdown(ans); st.session_state.db[st.session_state.current_chat]["messages"].append({"role": "assistant", "content": ans})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
  else:
198
+ ph = st.empty(); full = ""
199
+ resp = requests.post("https://openrouter.ai/api/v1/chat/completions", headers=headers, json={"model": MODEL_IDS[st.session_state.modelo_selecionado], "messages": [{"role": "system", "content": SYSTEM_PROMPT}] + [m for m in mensagens if "image_url" not in m], "stream": True}, stream=True)
200
+ for l in resp.iter_lines():
201
+ if l:
202
+ try:
203
+ d = json.loads(l.decode().replace('data: ', ''))
204
+ full += d['choices'][0]['delta'].get('content', '')
205
+ ph.markdown(full + "▌")
206
+ except: pass
207
+ ph.markdown(full); st.session_state.db[st.session_state.current_chat]["messages"].append({"role": "assistant", "content": full})
 
 
 
 
 
 
 
 
 
208
  save_db(st.session_state.db)
209