feat(web): migra Conversa e Reacoes para o painel novo

- Conversa: chat empurrado por SSE, indicador de "pensando", gesto da
  resposta com repetir, voz com aviso claro quando o contexto nao e seguro
  e leitura em voz alta so de respostas novas
- Memoria unificada: lembrancas da conversa + rotina/preferencias em abas;
  apagar tudo com confirmacao inline (sem confirm nativo)
- Reacoes: biblioteca com miniaturas estaticas (sem iframe por cartao),
  busca, mostrar no celular, feedback, editar em <dialog>, duplicar,
  ativar e excluir com Desfazer no toast; criar com IA; diario; grade de
  gestos com estrela para favoritos; uma unica previa lateral
- treinamento recarrega por evento SSE ao terminar, sem polling
- corrige icones encolhidos em botoes .pequeno.icone

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
julio
2026-10-05 19:51:45 -03:00
co-authored by Claude Opus 5.5
parent c4b53a8976
commit 947c0a3127
9 changed files with 1137 additions and 9 deletions
+42 -1
View File
@@ -17,7 +17,10 @@ spec = importlib.util.spec_from_file_location('kiosk_web', ROOT / 'kiosk-server.
kiosk = importlib.util.module_from_spec(spec)
spec.loader.exec_module(kiosk)
from braco import Braco
from cerebro import Cerebro
from conversa import Conversa
from servos import ControleServos
from treinamento import Treinamento
TOKEN = 'token-de-teste-123'
@@ -38,6 +41,10 @@ def main(capturas=None):
server.controle = kiosk.ControlePC(tmp / 'app.json')
server.servos = ControleServos(tmp / 'servos.json', HW())
server.braco = Braco(tmp / 'braco.json', server.servos)
server.cerebro = Cerebro(tmp / 'ia.json', server.controle, kiosk.enfileirar)
server.conversa = Conversa(tmp / 'memoria.db', server.cerebro)
server.treinamento = Treinamento(tmp / 'treinamento.db', iniciar=False)
server.conversa.treinamento = server.treinamento
kiosk.ligar_eventos(server)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
@@ -109,6 +116,40 @@ def testar(base, server, capturas):
page.get_by_label('Buscar ajuste').fill('')
page.screenshot(path=str(Path(capturas) / 'ajustes.png'), full_page=True)
# 6b. conversa: pergunta de hora é respondida pelo relógio (sem IA) e chega por SSE
page.get_by_role('link', name='Conversa').first.click()
page.get_by_label('Mensagem').fill('Que horas são?')
page.get_by_role('button', name='Enviar').click()
expect(page.get_by_text('no horário de São Paulo')).to_be_visible(timeout=5000)
page.get_by_label('Nova lembrança').fill('Meu nome é Júlio')
page.get_by_role('button', name='Adicionar').click()
expect(page.get_by_text('Meu nome é Júlio')).to_be_visible(timeout=5000)
page.get_by_role('tab', name='Rotina e preferências').click()
page.get_by_label('Nova rotina').fill('Treino às 7h')
page.get_by_role('button', name='Adicionar').click()
expect(page.get_by_text('Treino às 7h')).to_be_visible(timeout=5000)
if capturas:
page.screenshot(path=str(Path(capturas) / 'conversa.png'), full_page=True)
# 6c. reações: criar emoji, mostrar no celular, excluir e desfazer pelo toast
page.get_by_role('link', name='Reações').first.click()
page.get_by_role('button', name='Novo emoji').click()
page.get_by_label('Nome').fill('Ideia brilhante')
page.get_by_label('Quando usar').fill('Quando tiver uma ideia')
page.get_by_role('radio', name='🚀').click()
page.get_by_role('button', name='Salvar').click()
cartao = page.locator('li.reacao', has_text='Ideia brilhante')
expect(cartao).to_be_visible(timeout=5000)
cartao.get_by_role('button', name='Mostrar').click()
page.wait_for_timeout(400)
assert server.treinamento.expressao['receita']['nome'] == 'Ideia brilhante'
cartao.get_by_role('button', name='Excluir').click()
expect(cartao).to_have_count(0, timeout=5000)
page.get_by_role('button', name='Desfazer').last.click()
expect(page.locator('li.reacao', has_text='Ideia brilhante')).to_be_visible(timeout=5000)
if capturas:
page.screenshot(path=str(Path(capturas) / 'reacoes.png'), full_page=True)
# 7. celular: barra inferior e nada de rolagem horizontal
page.set_viewport_size({'width': 400, 'height': 860})
page.get_by_role('link', name='Ao vivo').click()
@@ -129,7 +170,7 @@ def testar(base, server, capturas):
assert not erros, erros
browser.close()
print('OK: token, SSE, dock 1-9, paleta, E-Stop, ajustes otimistas, celular 400px, tema claro')
print('OK: token, SSE, dock 1-9, paleta, E-Stop, ajustes otimistas, conversa, memória, reações com desfazer, celular 400px, tema')
if __name__ == '__main__':