tawasuyu/takiy
> takiy (Quechua: to sing). Music in Rust — synthesis, sequencing, playback — on Llimphi.
⎇ 5802bf9ee1f162194bcbfdc797293f2d20d829ae
02_ruway/takiy/takiy-app-llimphi/src/escucha.rs
History
Who touched what
raw
chore: refresco desde el monorepo (pin tawasuyu.git v0.3.0)
1//! Modal **Escuchar** — las dos escuchas IA de takiy sobre la pista activa.
2//!
3//! Espejo del modal de [`crate::genesis`]: un scrim + card centrada, con un
4//! selector arriba entre las dos escuchas y los controles de cada una. El
5//! motor (`takiy-escucha` / `takiy-escucha-llm`) es agnóstico y determinista;
6//! esto es sólo su frente Llimphi. A diferencia de Génesis, el resultado no
7//! abre un proyecto nuevo: **agrega pistas** a la pieza abierta.
8
9use llimphi_theme::Theme;
10use llimphi_ui::View;
11use llimphi_widget_segmented::{segmented_view, SegmentedPalette};
12
13use crate::genesis::{
14 chip, dialog_row, label_cell, labeled, modal, pill_button, row_h, stepper, text_grow,
15};
16use crate::msg::Msg;
17use rimay_localize::t;
18
19/// Cuál de las dos escuchas está seleccionada en el modal.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub(crate) enum EscuchaModo {
22 /// Escucha 1: seguir el hilo (predecir/expandir/completar).
23 Continuar,
24 /// Escucha 2: descifrar (deducir acordes + loop de ritmo).
25 Deducir,
26}
27
28impl EscuchaModo {
29 fn idx(self) -> usize {
30 matches!(self, EscuchaModo::Deducir) as usize
31 }
32 pub(crate) fn from_idx(i: usize) -> Self {
33 if i == 1 {
34 EscuchaModo::Deducir
35 } else {
36 EscuchaModo::Continuar
37 }
38 }
39}
40
41/// Estado del modal Escuchar.
42pub(crate) struct EscuchaForm {
43 pub(crate) modo: EscuchaModo,
44 /// Compases nuevos a generar (escucha 1).
45 pub(crate) compases: usize,
46 /// Semilla de la continuación (escucha 1).
47 pub(crate) seed: u64,
48 /// Usar el LLM real para refinar el resultado.
49 pub(crate) ia: bool,
50 /// Descripción de la fuente (la pista activa) para orientar al usuario.
51 pub(crate) fuente: String,
52}
53
54impl EscuchaForm {
55 /// Formulario nuevo con defaults sensatos. `fuente` describe la pista
56 /// activa de la que partirá la escucha.
57 pub(crate) fn new(fuente: String) -> Self {
58 Self { modo: EscuchaModo::Continuar, compases: 4, seed: 1, ia: false, fuente }
59 }
60}
61
62/// El overlay modal de Escuchar. `backend`/`model` son el LLM elegido (global).
63pub(crate) fn overlay(
64 form: &EscuchaForm,
65 backend: Option<takiy_escucha_llm::BackendKind>,
66 model: Option<&str>,
67 theme: &Theme,
68) -> View<Msg> {
69 let seg = SegmentedPalette::from_theme(theme);
70 let mut rows: Vec<View<Msg>> = Vec::new();
71
72 // Título.
73 rows.push(row_h(26.0).children(vec![text_grow("Escuchar — dos escuchas IA", 14.0, theme.fg_text)]));
74
75 // Fuente (la pista activa).
76 rows.push(row_h(18.0).children(vec![text_grow(
77 &format!("Fuente: {}", form.fuente),
78 11.0,
79 theme.accent,
80 )]));
81
82 // Selector de escucha.
83 rows.push(labeled(
84 theme,
85 "Escucha",
86 segmented_view(
87 &[&t("takiy-follow-thread"), "Descifrar"],
88 form.modo.idx(),
89 Msg::EscuchaSetMode,
90 &seg,
91 ),
92 ));
93
94 match form.modo {
95 EscuchaModo::Continuar => {
96 // Compases a agregar.
97 rows.push(dialog_row(vec![
98 label_cell(theme, "Compases"),
99 stepper(theme, &form.compases.to_string(), Msg::EscuchaBars(-1), Msg::EscuchaBars(1)),
100 ]));
101 // Semilla + reroll.
102 rows.push(dialog_row(vec![
103 label_cell(theme, "Semilla"),
104 text_grow(&form.seed.to_string(), 12.0, theme.fg_muted),
105 pill_button(theme, &t("takiy-another-variant"), Msg::EscuchaReroll, false),
106 ]));
107 rows.push(hint(theme, &t("takiy-add-ia-tracks")));
108 }
109 EscuchaModo::Deducir => {
110 rows.push(hint(theme, &t("takiy-deduce-chords")));
111 rows.push(hint(theme, &t("takiy-deduce-chords-2")));
112 }
113 }
114
115 // IA real (LLM) — refina el baseline determinista.
116 rows.push(dialog_row(vec![
117 label_cell(theme, "IA"),
118 chip(
119 theme,
120 if form.ia { "IA real (LLM) · on" } else { "IA real (LLM) · off" },
121 form.ia,
122 Msg::EscuchaToggleIa,
123 ),
124 ]));
125 if form.ia {
126 rows.push(dialog_row(vec![
127 label_cell(theme, "Backend"),
128 chip(theme, crate::llmcfg::label(backend), false, Msg::CycleLlmBackend),
129 ]));
130 if crate::llmcfg::hay_eleccion_de_modelo(backend) {
131 rows.push(dialog_row(vec![
132 label_cell(theme, "Modelo"),
133 chip(theme, crate::llmcfg::model_label(model), false, Msg::CycleLlmModel),
134 ]));
135 }
136 }
137
138 // Botones.
139 rows.push(buttons(theme));
140
141 modal(rows, theme, Msg::EscuchaAbsorb)
142}
143
144/// Fila de ayuda tenue a ancho completo.
145fn hint(theme: &Theme, s: &str) -> View<Msg> {
146 row_h(16.0).children(vec![text_grow(s, 10.0, theme.fg_muted)])
147}
148
149/// Fila de botones «Cancelar / Escuchar» alineada a la derecha.
150fn buttons(theme: &Theme) -> View<Msg> {
151 use llimphi_ui::llimphi_layout::taffy::prelude::{length, percent, FlexDirection, Size, Style};
152 use llimphi_ui::llimphi_layout::taffy::{AlignItems, JustifyContent, Rect};
153 View::new(Style {
154 flex_direction: FlexDirection::Row,
155 size: Size { width: percent(1.0_f32), height: length(30.0_f32) },
156 flex_shrink: 0.0,
157 align_items: Some(AlignItems::CENTER),
158 justify_content: Some(JustifyContent::END),
159 gap: Size { width: length(8.0_f32), height: length(0.0_f32) },
160 margin: Rect {
161 left: length(0.0_f32),
162 right: length(0.0_f32),
163 top: length(6.0_f32),
164 bottom: length(0.0_f32),
165 },
166 ..Default::default()
167 })
168 .children(vec![
169 pill_button(theme, "Cancelar", Msg::EscuchaCancel, false),
170 pill_button(theme, "Escuchar", Msg::EscuchaRun, true),
171 ])
172}