Astarok commited on
Commit
3fac980
·
verified ·
1 Parent(s): 10ccc95

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +109 -128
app.py CHANGED
@@ -7,7 +7,7 @@ import re
7
  from datetime import datetime
8
 
9
  # ═══════════════════════════════════════════════════════════════════════════
10
- # 1. CONFIGURAÇÃO DE TELA E CSS (Estilo Gemini Mobile)
11
  # ═══════════════════════════════════════════════════════════════════════════
12
  st.set_page_config(page_title="Yukina", page_icon="❄️", layout="centered")
13
 
@@ -18,12 +18,14 @@ st.markdown("""
18
  header {visibility: hidden;}
19
 
20
  .stApp { background-color: #131314; color: #ededed; }
21
- [data-testid="stSidebar"] { background-color: #0f0f0f; border-right: 1px solid #2a2a2a; }
 
 
22
 
23
  .stChatMessage { background-color: transparent !important; border: none !important; padding-bottom: 8px !important; }
24
  textarea { background-color: #1e1f20 !important; border-radius: 24px !important; border: 1px solid #3c4043 !important; padding: 12px !important;}
25
 
26
- /* MÁGICA DAS PÍLULAS E BOTÕES DA TOOLBAR */
27
  .stButton > button, [data-testid="stPopover"] > button {
28
  border-radius: 30px !important;
29
  background-color: #1e1f20 !important;
@@ -32,14 +34,24 @@ st.markdown("""
32
  padding: 8px 20px !important;
33
  display: inline-flex !important;
34
  align-items: center !important;
35
- justify-content: flex-start !important;
36
  width: auto !important;
37
  font-size: 15px !important;
38
- margin-bottom: 2px !important;
39
  }
40
  .stButton > button:hover, [data-testid="stPopover"] > button:hover { background-color: #2a2b2f !important; border-color: #888 !important; }
41
 
42
- [data-testid="stSidebar"] .stButton > button { width: 100% !important; }
 
 
 
 
 
 
 
 
 
 
 
 
43
  </style>
44
  """, unsafe_allow_html=True)
45
 
@@ -76,41 +88,101 @@ if "current_chat" not in st.session_state: st.session_state.current_chat = list(
76
  if "modelo_selecionado" not in st.session_state: st.session_state.modelo_selecionado = "Histórias (Euryale)"
77
 
78
  # ═══════════════════════════════════════════════════════════════════════════
79
- # 3. SIDEBAR (Gaveta)
80
  # ═══════════════════════════════════════════════════════════════════════════
81
  with st.sidebar:
82
- st.markdown("### 👤 Astarok")
83
- if st.button("➕ Novo Chat"):
84
- nid = f"Chat {datetime.now().strftime('%H:%M:%S')}"
85
- st.session_state.db[nid] = {"pinned": False, "messages": []}
86
- st.session_state.current_chat = nid
87
- save_db(st.session_state.db)
88
- st.rerun()
 
 
 
 
 
 
 
 
 
89
 
90
- st.markdown("---")
 
 
 
 
 
91
  chats = st.session_state.db
92
- for c_id in sorted(chats, key=lambda x: not chats[x].get("pinned")):
93
- icon = "📌 " if chats[c_id].get("pinned") else ""
94
- if st.button(f"{icon}{c_id}", key=f"btn_{c_id}"):
95
- st.session_state.current_chat = c_id
96
- st.rerun()
97
-
98
- st.markdown("---")
99
- c1, c2, c3 = st.columns(3)
100
- with c1:
101
- if st.button("📌"):
102
- st.session_state.db[st.session_state.current_chat]["pinned"] = not st.session_state.db[st.session_state.current_chat]["pinned"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  save_db(st.session_state.db)
 
104
  st.rerun()
105
- with c2:
106
- if st.button("👯"): st.toast("Em breve")
107
- with c3:
108
- if st.button("🗑️"):
109
- if len(st.session_state.db) > 1:
110
- del st.session_state.db[st.session_state.current_chat]
111
- st.session_state.current_chat = list(st.session_state.db.keys())[0]
112
- save_db(st.session_state.db)
113
- st.rerun()
114
 
115
  # ═══════════════════════════════════════════════════════════════════════════
116
  # 4. ÁREA PRINCIPAL E TELA INICIAL
@@ -153,98 +225,7 @@ else:
153
  # ═══════════════════════════════════════════════════════════════════════════
154
  # 5. TOOLBAR INFERIOR (Estilo Gemini)
155
  # ═══════════════════════════════════════════════════════════════════════════
156
- st.markdown("<br>", unsafe_allow_html=True) # Espaço para não grudar na última mensagem
157
-
158
- t_col1, t_col2, t_space = st.columns([1.5, 1.5, 7])
159
- with t_col1:
160
- with st.popover("➕"):
161
- st.markdown("<p style='font-size: 13px; color: #888;'>Mídia só funciona com o motor de Visão ativo.</p>", unsafe_allow_html=True)
162
- img_upload = st.file_uploader("Upload", type=["png", "jpg", "jpeg"], label_visibility="collapsed")
163
- with t_col2:
164
- with st.popover("🎛️"):
165
- st.session_state.modelo_selecionado = st.radio(
166
- "Selecione o Motor:",
167
- list(MODEL_IDS.keys()),
168
- index=list(MODEL_IDS.keys()).index(st.session_state.modelo_selecionado)
169
- )
170
-
171
- # INPUT DE MENSAGEM
172
- if prompt := st.chat_input("Peça à Yukina..."):
173
-
174
- st.session_state.db[st.session_state.current_chat]["messages"].append({"role": "user", "content": prompt})
175
- with st.chat_message("user"):
176
- st.markdown(prompt)
177
 
178
- with st.chat_message("assistant"):
179
- api_key = os.getenv("OPENROUTER_API_KEY")
180
- headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
181
-
182
- # MOTOR DE IMAGEM
183
- if "Imagem" in st.session_state.modelo_selecionado:
184
- with st.spinner("Desenhando..."):
185
- payload = {"model": MODEL_IDS[st.session_state.modelo_selecionado], "messages": [{"role": "user", "content": prompt}], "modalities": ["image"]}
186
- try:
187
- resp = requests.post("https://openrouter.ai/api/v1/chat/completions", headers=headers, json=payload).json()
188
- message_obj = resp['choices'][0]['message']
189
- img_url = None
190
- if 'images' in message_obj and len(message_obj['images']) > 0:
191
- img_url = message_obj['images'][0]['image_url']['url']
192
- elif 'content' in message_obj and message_obj['content']:
193
- content = message_obj['content']
194
- if content.startswith('data:image'): img_url = content
195
- else:
196
- urls = re.findall(r'(https?://[^\s)]+)', content)
197
- if urls: img_url = urls[0]
198
-
199
- if img_url:
200
- st.image(img_url)
201
- st.session_state.db[st.session_state.current_chat]["messages"].append({"role": "assistant", "content": "Arte gerada.", "image_url": img_url})
202
- else:
203
- st.error("Falha ao gerar a imagem.")
204
- except Exception as e:
205
- st.error(f"Erro: {e}")
206
-
207
- # MOTOR DE VISÃO
208
- elif "Visão" in st.session_state.modelo_selecionado:
209
- if not img_upload:
210
- st.error("⚠️ Você está usando o motor de Visão. Clique no '➕' e anexe uma imagem primeiro!")
211
- else:
212
- with st.spinner("Analisando..."):
213
- try:
214
- encoded_image = base64.b64encode(img_upload.read()).decode('utf-8')
215
- data_url = f"data:image/png;base64,{encoded_image}"
216
- payload = {
217
- "model": MODEL_IDS[st.session_state.modelo_selecionado],
218
- "messages": [{"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": [{"type": "text", "text": prompt}, {"type": "image_url", "image_url": {"url": data_url}}]}],
219
- "stream": False
220
- }
221
- resp = requests.post("https://openrouter.ai/api/v1/chat/completions", headers=headers, json=payload).json()
222
- analise = resp['choices'][0]['message']['content']
223
- st.markdown(analise)
224
- st.session_state.db[st.session_state.current_chat]["messages"].append({"role": "assistant", "content": analise})
225
- except Exception as e:
226
- st.error(f"Erro na Visão: {e}")
227
-
228
- # MOTOR DE TEXTO
229
- else:
230
- ph = st.empty()
231
- full_res = ""
232
- historico_limpo = [m for m in st.session_state.db[st.session_state.current_chat]["messages"] if "image_url" not in m]
233
- payload = {"model": MODEL_IDS[st.session_state.modelo_selecionado], "messages": [{"role": "system", "content": SYSTEM_PROMPT}] + historico_limpo, "stream": True}
234
- try:
235
- resp = requests.post("https://openrouter.ai/api/v1/chat/completions", headers=headers, json=payload, stream=True)
236
- for line in resp.iter_lines():
237
- if line:
238
- try:
239
- d = json.loads(line.decode('utf-8').replace('data: ', ''))
240
- content = d['choices'][0]['delta'].get('content', '')
241
- full_res += content
242
- ph.markdown(full_res + "▌")
243
- except: continue
244
- ph.markdown(full_res)
245
- st.session_state.db[st.session_state.current_chat]["messages"].append({"role": "assistant", "content": full_res})
246
- except Exception as e:
247
- st.error(f"Erro: {e}")
248
-
249
- save_db(st.session_state.db)
250
 
 
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
 
 
18
  header {visibility: hidden;}
19
 
20
  .stApp { background-color: #131314; color: #ededed; }
21
+
22
+ /* Fundo da Sidebar mais escuro */
23
+ [data-testid="stSidebar"] { background-color: #0b0b0b; border-right: 1px solid #1e1f20; }
24
 
25
  .stChatMessage { background-color: transparent !important; border: none !important; padding-bottom: 8px !important; }
26
  textarea { background-color: #1e1f20 !important; border-radius: 24px !important; border: 1px solid #3c4043 !important; padding: 12px !important;}
27
 
28
+ /* Botões da tela principal e barra inferior */
29
  .stButton > button, [data-testid="stPopover"] > button {
30
  border-radius: 30px !important;
31
  background-color: #1e1f20 !important;
 
34
  padding: 8px 20px !important;
35
  display: inline-flex !important;
36
  align-items: center !important;
 
37
  width: auto !important;
38
  font-size: 15px !important;
 
39
  }
40
  .stButton > button:hover, [data-testid="stPopover"] > button:hover { background-color: #2a2b2f !important; border-color: #888 !important; }
41
 
42
+ /* Customização EXCLUSIVA para os botões da Sidebar (Menu, Chats, etc) */
43
+ [data-testid="stSidebar"] .stButton > button {
44
+ width: 100% !important;
45
+ border: none !important;
46
+ background-color: transparent !important;
47
+ justify-content: flex-start !important;
48
+ padding: 10px 15px !important;
49
+ border-radius: 12px !important;
50
+ }
51
+ [data-testid="stSidebar"] .stButton > button:hover { background-color: #1e1f20 !important; }
52
+
53
+ /* Esconde a setinha padrão do popover para o botão de 3 pontinhos */
54
+ [data-testid="stPopover"] div[data-testid="stMarkdownContainer"] p { margin: 0 !important; }
55
  </style>
56
  """, unsafe_allow_html=True)
57
 
 
88
  if "modelo_selecionado" not in st.session_state: st.session_state.modelo_selecionado = "Histórias (Euryale)"
89
 
90
  # ═══════════════════════════════════════════════════════════════════════════
91
+ # 3. SIDEBAR (Design Replit / Gemini)
92
  # ═══════════════════════════════════════════════════════════════════════════
93
  with st.sidebar:
94
+ # Cabeçalho: Bate-papos | + | ⚙️
95
+ c_title, c_add, c_set = st.columns([6, 1.5, 1.5])
96
+ with c_title:
97
+ st.markdown("<h4 style='color: #ededed; margin-top: 5px; margin-bottom: 0px;'>Bate-papos</h4>", unsafe_allow_html=True)
98
+ with c_add:
99
+ if st.button("➕", help="Novo Chat"):
100
+ nid = f"Chat {datetime.now().strftime('%H:%M:%S')}"
101
+ st.session_state.db[nid] = {"pinned": False, "messages": []}
102
+ st.session_state.current_chat = nid
103
+ save_db(st.session_state.db)
104
+ st.rerun()
105
+ with c_set:
106
+ if st.button("⚙️", help="Configurações"):
107
+ st.toast("Menu de configurações (Em breve)")
108
+
109
+ st.markdown("<br>", unsafe_allow_html=True)
110
 
111
+ # Barra de Pesquisa
112
+ busca = st.text_input("Pesquisar", placeholder="Pesquisar...", label_visibility="collapsed")
113
+
114
+ st.markdown("<br>", unsafe_allow_html=True)
115
+
116
+ # Lista de Chats com Filtro
117
  chats = st.session_state.db
118
+ if busca:
119
+ chats_exibidos = [c for c in chats if busca.lower() in c.lower()]
120
+ else:
121
+ chats_exibidos = list(chats.keys())
122
+
123
+ # Ordena pinados primeiro
124
+ chats_exibidos.sort(key=lambda x: not chats[x].get("pinned"))
125
+
126
+ # Renderiza os chats com botão principal e o menu ⋮ do lado
127
+ for c_id in chats_exibidos:
128
+ col_chat, col_opt = st.columns([8, 2])
129
+
130
+ with col_chat:
131
+ icon = "📌" if chats[c_id].get("pinned") else "💬"
132
+ # Destaca visualmente o chat ativo
133
+ if c_id == st.session_state.current_chat:
134
+ nome_exibicao = f"**{icon} {c_id[:18]}**"
135
+ else:
136
+ nome_exibicao = f"{icon} {c_id[:18]}"
137
+
138
+ if st.button(nome_exibicao, key=f"btn_{c_id}"):
139
+ st.session_state.current_chat = c_id
140
+ st.rerun()
141
+
142
+ with col_opt:
143
+ with st.popover("⋮"):
144
+ if st.button("📌 Fixar/Desfixar", 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("👯 Duplicar", key=f"dup_{c_id}"):
149
+ novo_nome = f"{c_id} (Cópia)"
150
+ st.session_state.db[novo_nome] = st.session_state.db[c_id].copy()
151
+ st.session_state.db[novo_nome]["pinned"] = False
152
+ save_db(st.session_state.db)
153
+ st.rerun()
154
+ if st.button("🗑️ Apagar", key=f"del_{c_id}"):
155
+ if len(st.session_state.db) > 1:
156
+ del st.session_state.db[c_id]
157
+ if st.session_state.current_chat == c_id:
158
+ st.session_state.current_chat = list(st.session_state.db.keys())[0]
159
+ save_db(st.session_state.db)
160
+ st.rerun()
161
+
162
+ # Sessão de Backup (Rodapé da Sidebar)
163
+ st.markdown("<br><br><br>", unsafe_allow_html=True)
164
+ st.markdown("<p style='font-size: 10px; color: #888; font-weight: bold; letter-spacing: 1px;'>BACKUP</p>", unsafe_allow_html=True)
165
+
166
+ db_json = json.dumps(st.session_state.db, ensure_ascii=False, indent=4)
167
+ st.download_button(
168
+ label="↓ Exportar todos os chats (.json)",
169
+ data=db_json,
170
+ file_name="yukina_backup.json",
171
+ mime="application/json",
172
+ use_container_width=True
173
+ )
174
+
175
+ st.markdown("<p style='font-size: 13px; color: #888; margin-top: 15px; margin-bottom: 5px;'>Importar conversa (.json):</p>", unsafe_allow_html=True)
176
+ arquivo_import = st.file_uploader("Carregar", type=["json"], label_visibility="collapsed")
177
+ if arquivo_import is not None:
178
+ try:
179
+ dados_importados = json.load(arquivo_import)
180
+ st.session_state.db.update(dados_importados)
181
  save_db(st.session_state.db)
182
+ st.success("Conversas importadas!")
183
  st.rerun()
184
+ except:
185
+ st.error("Arquivo inválido.")
 
 
 
 
 
 
 
186
 
187
  # ═══════════════════════════════════════════════════════════════════════════
188
  # 4. ÁREA PRINCIPAL E TELA INICIAL
 
225
  # ═══════════════════════════════════════════════════════════════════════════
226
  # 5. TOOLBAR INFERIOR (Estilo Gemini)
227
  # ═══════════════════════════════════════════════════════════════════════════
228
+ st.markdown("<br>", unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
 
230
+ t_col1, t_col2, t_space = st.columns(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231