binary-equalab 3.0.0b1__py3-none-any.whl → 3.0.0b3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- binary_equalab/cli.py +26 -12
- binary_equalab/engine.py +57 -0
- binary_equalab/services/kimi_service.py +127 -0
- binary_equalab/sonify.py +15 -0
- binary_equalab/tui.py +252 -0
- {binary_equalab-3.0.0b1.dist-info → binary_equalab-3.0.0b3.dist-info}/METADATA +17 -2
- binary_equalab-3.0.0b3.dist-info/RECORD +16 -0
- binary_equalab-3.0.0b1.dist-info/RECORD +0 -14
- {binary_equalab-3.0.0b1.dist-info → binary_equalab-3.0.0b3.dist-info}/WHEEL +0 -0
- {binary_equalab-3.0.0b1.dist-info → binary_equalab-3.0.0b3.dist-info}/entry_points.txt +0 -0
binary_equalab/cli.py
CHANGED
|
@@ -21,7 +21,7 @@ console = Console()
|
|
|
21
21
|
|
|
22
22
|
BANNER = """
|
|
23
23
|
[bold orange1]╔══════════════════════════════════════════════════════════╗
|
|
24
|
-
║ [white]Binary EquaLab CLI[/white] [dim]Aurora
|
|
24
|
+
║ [white]Binary EquaLab CLI[/white] [dim]Aurora v3.0 (Beta)[/dim] ║
|
|
25
25
|
║ [dim italic]"Las matemáticas también sienten,[/dim italic] ║
|
|
26
26
|
║ [dim italic] pero estas no se equivocan."[/dim italic] ║
|
|
27
27
|
╚══════════════════════════════════════════════════════════╝[/bold orange1]
|
|
@@ -80,8 +80,19 @@ HELP_TEXT = """
|
|
|
80
80
|
| `tir(flujo0, flujo1, ...)` | `tir(-1000, 300, 400, 500)` |
|
|
81
81
|
| `depreciar(costo, residual, años)` | `depreciar(10000, 1000, 5)` |
|
|
82
82
|
| `interes_simple(capital, tasa, tiempo)` | `interes_simple(1000, 0.05, 3)` |
|
|
83
|
+
| `interes_simple(capital, tasa, tiempo)` | `interes_simple(1000, 0.05, 3)` |
|
|
83
84
|
| `interes_compuesto(capital, tasa, n, tiempo)` | `interes_compuesto(1000, 0.05, 12, 3)` |
|
|
84
85
|
|
|
86
|
+
### Sistemas Numéricos (NUEVO)
|
|
87
|
+
| Función | Ejemplo |
|
|
88
|
+
|---------|---------|
|
|
89
|
+
| `bin(n)` | `bin(10)` → `0b1010` |
|
|
90
|
+
| `oct(n)` | `oct(10)` → `0o12` |
|
|
91
|
+
| `hex(n)` | `hex(255)` → `0xff` |
|
|
92
|
+
| `base(n, b)` | `base(10, 2)` → `1010` |
|
|
93
|
+
|
|
94
|
+
### AI Assistant
|
|
95
|
+
|
|
85
96
|
### Aliases y Accesos Directos
|
|
86
97
|
- **Shell**: Puedes ejecutar el programa como `binary-equalab`, `bneqls`, `beq` o `binary-math`.
|
|
87
98
|
- **Trigonometría**: `seno`=`sin`, `coseno`=`cos`, `tangente`=`tan`.
|
|
@@ -99,7 +110,7 @@ def get_prompt_style():
|
|
|
99
110
|
def print_banner():
|
|
100
111
|
"""Print the CLI banner using Rich panels."""
|
|
101
112
|
title = Text("Binary EquaLab CLI", style="bold white")
|
|
102
|
-
version = Text("Aurora
|
|
113
|
+
version = Text("Aurora v3.0 (Beta)", style="dim")
|
|
103
114
|
slogan = Text('"Las matemáticas también sienten,\npero estas no se equivocan."', style="dim italic")
|
|
104
115
|
|
|
105
116
|
content = Text.assemble(title, " ", version, "\n\n", slogan, justify="center")
|
|
@@ -219,9 +230,13 @@ def main():
|
|
|
219
230
|
if len(sys.argv) > 1 and sys.argv[1] == 'setup-shell':
|
|
220
231
|
from .shell_setup import run_setup
|
|
221
232
|
run_setup()
|
|
233
|
+
elif len(sys.argv) > 1 and sys.argv[1] == 'tui':
|
|
234
|
+
from .tui import BinaryTUI
|
|
235
|
+
app = BinaryTUI()
|
|
236
|
+
app.run()
|
|
222
237
|
elif len(sys.argv) > 1 and sys.argv[1] == 'ai':
|
|
223
|
-
# AI Commands Mode
|
|
224
|
-
from .kimi_service import kimi_service
|
|
238
|
+
# AI Commands Mode (Kimi K2)
|
|
239
|
+
from .services.kimi_service import kimi_service
|
|
225
240
|
|
|
226
241
|
if len(sys.argv) < 3:
|
|
227
242
|
console.print("[bold red]Uso:[/bold red] binary ai [solve|explain|exercises] \"consulta\"")
|
|
@@ -230,7 +245,6 @@ def main():
|
|
|
230
245
|
subcmd = sys.argv[2]
|
|
231
246
|
query = " ".join(sys.argv[3:])
|
|
232
247
|
|
|
233
|
-
# 'exercises' command doesn't necessarily need a query if defaults are used, but we'll use query as topic
|
|
234
248
|
if subcmd != 'exercises' and not query:
|
|
235
249
|
console.print("[bold red]Error:[/bold red] Falta la consulta.")
|
|
236
250
|
sys.exit(1)
|
|
@@ -258,12 +272,15 @@ def main():
|
|
|
258
272
|
console.print(Panel(Markdown(response), title=f"Kimi AI: Explicación", border_style="blue"))
|
|
259
273
|
|
|
260
274
|
elif subcmd == "exercises":
|
|
261
|
-
|
|
262
|
-
#
|
|
263
|
-
exercises = kimi_service.generate_exercises(query if query else "Matemáticas
|
|
275
|
+
count = 3 # default
|
|
276
|
+
# Simple parsing for count if present in args? For now keeping simple.
|
|
277
|
+
exercises = kimi_service.generate_exercises(query if query else "Matemáticas Generales", count)
|
|
264
278
|
|
|
265
279
|
console.print(f"[bold u]Generando ejercicios para:[/bold u] {query}\n")
|
|
266
280
|
|
|
281
|
+
if not exercises:
|
|
282
|
+
console.print("No se pudieron generar ejercicios. Verifica tu API Key o intenta de nuevo.")
|
|
283
|
+
|
|
267
284
|
for i, ex in enumerate(exercises, 1):
|
|
268
285
|
console.print(Panel(
|
|
269
286
|
f"[bold]Pregunta:[/bold]\n{ex.get('problem')}\n\n"
|
|
@@ -271,10 +288,7 @@ def main():
|
|
|
271
288
|
title=f"Ejercicio {i}", border_style="magenta"
|
|
272
289
|
))
|
|
273
290
|
if ex.get('steps'):
|
|
274
|
-
|
|
275
|
-
# Hack para ocultar pasos inicialmente si se quisiera, pero aquí los mostramos
|
|
276
|
-
pass
|
|
277
|
-
console.print(f"[dim]Pasos: {', '.join(ex.get('steps', []))}[/dim]\n")
|
|
291
|
+
console.print(f"[dim]Pasos: {', '.join(ex.get('steps', []))}[/dim]\n")
|
|
278
292
|
else:
|
|
279
293
|
console.print(f"[bold red]Comando desconocido:[/bold red] {subcmd}")
|
|
280
294
|
sys.exit(1)
|
binary_equalab/engine.py
CHANGED
|
@@ -74,6 +74,12 @@ class MathEngine:
|
|
|
74
74
|
'recta': self._recta,
|
|
75
75
|
'circulo': self._circulo,
|
|
76
76
|
|
|
77
|
+
# Numeral Systems
|
|
78
|
+
'bin': self._binario,
|
|
79
|
+
'oct': self._octal,
|
|
80
|
+
'hex': self._hexadecimal,
|
|
81
|
+
'base': self._base_n,
|
|
82
|
+
|
|
77
83
|
# Trigonometry aliases
|
|
78
84
|
'seno': sin,
|
|
79
85
|
'coseno': cos,
|
|
@@ -394,6 +400,41 @@ class MathEngine:
|
|
|
394
400
|
engine = AudioEngine()
|
|
395
401
|
return engine.generate(str(expr), float(duration), str(filename))
|
|
396
402
|
|
|
403
|
+
# ============ NUMERAL SYSTEMS ============
|
|
404
|
+
|
|
405
|
+
def _binario(self, number):
|
|
406
|
+
"""Convert to binary: bin(10) → '0b1010'"""
|
|
407
|
+
try:
|
|
408
|
+
return bin(int(number))
|
|
409
|
+
except:
|
|
410
|
+
return bin(int(float(number)))
|
|
411
|
+
|
|
412
|
+
def _octal(self, number):
|
|
413
|
+
"""Convert to octal: oct(10) → '0o12'"""
|
|
414
|
+
try:
|
|
415
|
+
return oct(int(number))
|
|
416
|
+
except:
|
|
417
|
+
return oct(int(float(number)))
|
|
418
|
+
|
|
419
|
+
def _hexadecimal(self, number):
|
|
420
|
+
"""Convert to hex: hex(255) → '0xff'"""
|
|
421
|
+
try:
|
|
422
|
+
return hex(int(number))
|
|
423
|
+
except:
|
|
424
|
+
return hex(int(float(number)))
|
|
425
|
+
|
|
426
|
+
def _base_n(self, number, base):
|
|
427
|
+
"""Convert to arbitrary base: base(10, 2) → '1010'"""
|
|
428
|
+
n = int(number)
|
|
429
|
+
b = int(base)
|
|
430
|
+
if n == 0:
|
|
431
|
+
return "0"
|
|
432
|
+
digits = []
|
|
433
|
+
while n:
|
|
434
|
+
digits.append(int(n % b))
|
|
435
|
+
n //= b
|
|
436
|
+
return "".join(str(d) for d in digits[::-1])
|
|
437
|
+
|
|
397
438
|
# Convenience functions for direct import
|
|
398
439
|
def derivar(expr, var=None, n=1):
|
|
399
440
|
engine = MathEngine()
|
|
@@ -490,3 +531,19 @@ def circulo(centro, radio):
|
|
|
490
531
|
def sonify(expr, duration=3.0, filename="output.wav"):
|
|
491
532
|
engine = MathEngine()
|
|
492
533
|
return engine._sonify(expr, duration, filename)
|
|
534
|
+
|
|
535
|
+
def binario(number):
|
|
536
|
+
engine = MathEngine()
|
|
537
|
+
return engine._binario(number)
|
|
538
|
+
|
|
539
|
+
def octal(number):
|
|
540
|
+
engine = MathEngine()
|
|
541
|
+
return engine._octal(number)
|
|
542
|
+
|
|
543
|
+
def hexadecimal(number):
|
|
544
|
+
engine = MathEngine()
|
|
545
|
+
return engine._hexadecimal(number)
|
|
546
|
+
|
|
547
|
+
def base(number, n):
|
|
548
|
+
engine = MathEngine()
|
|
549
|
+
return engine._base_n(number, n)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
|
|
2
|
+
# binary-cli/binary_equalab/services/kimi_service.py
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import json
|
|
6
|
+
import requests
|
|
7
|
+
from typing import List, Dict, Optional, Any
|
|
8
|
+
|
|
9
|
+
class KimiService:
|
|
10
|
+
"""Service to interact with Kimi K2 (Moonshot AI) from CLI/Desktop"""
|
|
11
|
+
|
|
12
|
+
def __init__(self):
|
|
13
|
+
# Allow checking multiple env locations or just env
|
|
14
|
+
self.api_key = os.getenv('KIMI_API_KEY', '')
|
|
15
|
+
self.base_url = 'https://api.moonshot.cn/v1'
|
|
16
|
+
self.model = 'moonshot-v1-128k'
|
|
17
|
+
|
|
18
|
+
if not self.api_key:
|
|
19
|
+
# Try to read from .env if not loaded (though click/main usually loads it)
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
def chat(
|
|
23
|
+
self,
|
|
24
|
+
messages: List[Dict[str, str]],
|
|
25
|
+
temperature: float = 0.7,
|
|
26
|
+
max_tokens: int = 4096,
|
|
27
|
+
stream: bool = False
|
|
28
|
+
) -> Any: # Returns str or iterator
|
|
29
|
+
"""Send message to Kimi K2"""
|
|
30
|
+
|
|
31
|
+
if not self.api_key:
|
|
32
|
+
raise ValueError("KIMI_API_KEY not configured. Set it in your environment.")
|
|
33
|
+
|
|
34
|
+
headers = {
|
|
35
|
+
'Content-Type': 'application/json',
|
|
36
|
+
'Authorization': f'Bearer {self.api_key}'
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
payload = {
|
|
40
|
+
'model': self.model,
|
|
41
|
+
'messages': messages,
|
|
42
|
+
'temperature': temperature,
|
|
43
|
+
'max_tokens': max_tokens,
|
|
44
|
+
'stream': stream
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
response = requests.post(
|
|
48
|
+
f'{self.base_url}/chat/completions',
|
|
49
|
+
headers=headers,
|
|
50
|
+
json=payload,
|
|
51
|
+
stream=stream
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
if not response.ok:
|
|
55
|
+
raise Exception(f"Kimi API Error: {response.status_code} - {response.text}")
|
|
56
|
+
|
|
57
|
+
if stream:
|
|
58
|
+
return self._stream_response(response)
|
|
59
|
+
|
|
60
|
+
data = response.json()
|
|
61
|
+
return data['choices'][0]['message']['content']
|
|
62
|
+
|
|
63
|
+
def _stream_response(self, response):
|
|
64
|
+
for line in response.iter_lines():
|
|
65
|
+
if line:
|
|
66
|
+
line = line.decode('utf-8')
|
|
67
|
+
if line.startswith('data: '):
|
|
68
|
+
data = line[6:]
|
|
69
|
+
if data == '[DONE]':
|
|
70
|
+
break
|
|
71
|
+
try:
|
|
72
|
+
parsed = json.loads(data)
|
|
73
|
+
content = parsed['choices'][0]['delta'].get('content')
|
|
74
|
+
if content:
|
|
75
|
+
yield content
|
|
76
|
+
except:
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
def solve_math_problem(self, problem: str, show_steps: bool = True) -> Dict[str, Any]:
|
|
80
|
+
"""Solve with steps"""
|
|
81
|
+
system_prompt = f"""Eres un asistente matemático experto.
|
|
82
|
+
Resuelve el problema {'mostrando pasos' if show_steps else 'directamente'}.
|
|
83
|
+
Responde en JSON:
|
|
84
|
+
{{
|
|
85
|
+
"solution": "Respuesta LaTeX",
|
|
86
|
+
"steps": ["Paso 1"],
|
|
87
|
+
"reasoning": "...",
|
|
88
|
+
"concepts": []
|
|
89
|
+
}}"""
|
|
90
|
+
|
|
91
|
+
messages = [
|
|
92
|
+
{'role': 'system', 'content': system_prompt},
|
|
93
|
+
{'role': 'user', 'content': problem}
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
content = self.chat(messages, temperature=0.3)
|
|
97
|
+
try:
|
|
98
|
+
if content.startswith("```json"):
|
|
99
|
+
content = content.replace("```json", "").replace("```", "")
|
|
100
|
+
return json.loads(content)
|
|
101
|
+
except:
|
|
102
|
+
return {"solution": content, "steps": [], "reasoning": "Raw output", "concepts": []}
|
|
103
|
+
|
|
104
|
+
def explain_concept(self, concept: str, level: str = 'intermediate') -> str:
|
|
105
|
+
"""Explain concept"""
|
|
106
|
+
system_prompt = f"""Explica {concept} para nivel {level}. Usa Markdown."""
|
|
107
|
+
messages = [
|
|
108
|
+
{'role': 'system', 'content': system_prompt},
|
|
109
|
+
{'role': 'user', 'content': 'Explícalo'}
|
|
110
|
+
]
|
|
111
|
+
return self.chat(messages, temperature=0.5)
|
|
112
|
+
|
|
113
|
+
def generate_exercises(self, topic: str, count: int = 5, difficulty: str = 'medium') -> List[Dict[str, Any]]:
|
|
114
|
+
"""Generate exercises"""
|
|
115
|
+
system_prompt = f"""Genera {count} ejercicios de {topic} ({difficulty}).
|
|
116
|
+
Responde en JSON: [{{ "problem": "...", "solution": "...", "steps": [] }}]"""
|
|
117
|
+
|
|
118
|
+
messages = [{'role': 'system', 'content': system_prompt}, {'role': 'user', 'content': 'Generar'}]
|
|
119
|
+
content = self.chat(messages)
|
|
120
|
+
try:
|
|
121
|
+
if content.startswith("```json"): content = content.replace("```json", "").replace("```","")
|
|
122
|
+
return json.loads(content)
|
|
123
|
+
except:
|
|
124
|
+
return []
|
|
125
|
+
|
|
126
|
+
# Singleton
|
|
127
|
+
kimi_service = KimiService()
|
binary_equalab/sonify.py
CHANGED
|
@@ -3,6 +3,7 @@ import numpy as np
|
|
|
3
3
|
import wave
|
|
4
4
|
import struct
|
|
5
5
|
import os
|
|
6
|
+
import re
|
|
6
7
|
from typing import Union, List
|
|
7
8
|
import sympy as sp
|
|
8
9
|
from .parser_enhanced import EnhancedParser
|
|
@@ -27,9 +28,23 @@ class AudioEngine:
|
|
|
27
28
|
|
|
28
29
|
# Preprocess using our EnhancedParser (supports 2t, sin^2(t))
|
|
29
30
|
clean_expr = EnhancedParser.preprocess(expr_str)
|
|
31
|
+
# Custom replacements for Sonify (since it doesn't use Engine's full pipeline)
|
|
32
|
+
replacements = {
|
|
33
|
+
'seno': 'sin', 'sen': 'sin',
|
|
34
|
+
'coseno': 'cos', 'tangente': 'tan',
|
|
35
|
+
'raiz': 'sqrt'
|
|
36
|
+
}
|
|
37
|
+
for es, en in replacements.items():
|
|
38
|
+
clean_expr = re.sub(rf'\b{es}\b', en, clean_expr, flags=re.IGNORECASE)
|
|
39
|
+
|
|
30
40
|
# Also standardize python power
|
|
31
41
|
clean_expr = clean_expr.replace('^', '**')
|
|
32
42
|
|
|
43
|
+
# Smart variable replacement: if users type 'sin(x)', they mean 'sin(t)' for audio
|
|
44
|
+
if 'x' in clean_expr and 't' not in clean_expr:
|
|
45
|
+
print("ℹ️ Auto-detecting 'x' as time variable...")
|
|
46
|
+
clean_expr = clean_expr.replace('x', 't')
|
|
47
|
+
|
|
33
48
|
try:
|
|
34
49
|
# Parse with SymPy
|
|
35
50
|
sym_expr = sp.sympify(clean_expr)
|
binary_equalab/tui.py
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
from textual.app import App, ComposeResult
|
|
2
|
+
from textual.widgets import Header, Footer, Input, RichLog, Static
|
|
3
|
+
from textual.containers import Container
|
|
4
|
+
from textual import on, events
|
|
5
|
+
from textual.suggester import Suggester
|
|
6
|
+
from sympy import pretty
|
|
7
|
+
from .engine import MathEngine
|
|
8
|
+
|
|
9
|
+
class MathSuggester(Suggester):
|
|
10
|
+
"""Basic auto-complete for math commands."""
|
|
11
|
+
def __init__(self):
|
|
12
|
+
super().__init__(use_cache=False)
|
|
13
|
+
self.suggestions = [
|
|
14
|
+
"derivar(", "integrar(", "limite(", "sumatoria(",
|
|
15
|
+
"simplificar(", "expandir(", "factorizar(", "resolver(",
|
|
16
|
+
"sonify(", "distancia(", "recta(", "circulo(",
|
|
17
|
+
"sin(", "cos(", "tan(", "sqrt(", "log(",
|
|
18
|
+
"matrix(", "help", "clear", "exit", "quit"
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
async def get_suggestion(self, value: str) -> str | None:
|
|
22
|
+
if not value:
|
|
23
|
+
return None
|
|
24
|
+
# Simple prefix match
|
|
25
|
+
word = value.split(" ")[-1] # last word
|
|
26
|
+
if not word: return None
|
|
27
|
+
|
|
28
|
+
for s in self.suggestions:
|
|
29
|
+
if s.startswith(word) and s != word:
|
|
30
|
+
# Return the ending part to complete the word
|
|
31
|
+
return s[len(word):]
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
class BinaryTUI(App):
|
|
35
|
+
"""A Textual App for Binary EquaLab (Jupyter-Lite Style)."""
|
|
36
|
+
|
|
37
|
+
CSS = """
|
|
38
|
+
Screen {
|
|
39
|
+
layout: vertical;
|
|
40
|
+
background: #0f172a; /* Slate 900 */
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
RichLog {
|
|
44
|
+
background: #1e293b; /* Slate 800 */
|
|
45
|
+
color: #e2e8f0; /* Slate 200 */
|
|
46
|
+
border: none;
|
|
47
|
+
height: 1fr;
|
|
48
|
+
padding: 1;
|
|
49
|
+
scrollbar-background: #0f172a;
|
|
50
|
+
scrollbar-color: #3b82f6;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
Input {
|
|
54
|
+
dock: bottom;
|
|
55
|
+
margin: 0;
|
|
56
|
+
border: wide #3b82f6; /* Blue 500 */
|
|
57
|
+
background: #0f172a;
|
|
58
|
+
color: #fb923c; /* Orange 400 */
|
|
59
|
+
height: 3;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
#welcome {
|
|
63
|
+
color: #fb923c;
|
|
64
|
+
text-style: bold;
|
|
65
|
+
text-align: center;
|
|
66
|
+
background: #0f172a;
|
|
67
|
+
padding: 1;
|
|
68
|
+
border-bottom: solid #3b82f6;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.in-prompt {
|
|
72
|
+
color: #38bdf8; /* Sky 400 */
|
|
73
|
+
text-style: bold;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.out-prompt {
|
|
77
|
+
color: #fb923c; /* Orange 400 */
|
|
78
|
+
text-style: bold;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
#help-bar {
|
|
82
|
+
background: #0f172a;
|
|
83
|
+
color: #94a3b8; /* Slate 400 */
|
|
84
|
+
padding-left: 1;
|
|
85
|
+
height: 1;
|
|
86
|
+
dock: bottom; /* Sit right above the input */
|
|
87
|
+
}
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
BINDINGS = [
|
|
91
|
+
("ctrl+q", "quit", "Quit"),
|
|
92
|
+
("ctrl+c", "copy_or_ignore", "Copy/Ignore"), # Prevent accidental exit
|
|
93
|
+
("ctrl+l", "clear_screen", "Clear"),
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
def action_copy_or_ignore(self) -> None:
|
|
97
|
+
"""Handle Ctrl+C safely."""
|
|
98
|
+
self.notify("Usa Ctrl+Q o escribe 'exit' para salir.", title="No te vayas aún...", severity="warning")
|
|
99
|
+
|
|
100
|
+
def __init__(self):
|
|
101
|
+
super().__init__()
|
|
102
|
+
self.engine = MathEngine()
|
|
103
|
+
self.history = []
|
|
104
|
+
self.history_index = -1
|
|
105
|
+
self.execution_count = 1
|
|
106
|
+
|
|
107
|
+
# Help Dictionary for Contextual Hints
|
|
108
|
+
HELP_DOCS = {
|
|
109
|
+
"derivar": "💡 derivar(expr, var) - Calcula la derivada. Ej: derivar(x^2, x)",
|
|
110
|
+
"integrar": "💡 integrar(expr, var, [a, b]) - Integral indefinida o definida. Ej: integrar(sin(x), x)",
|
|
111
|
+
"limite": "💡 limite(expr, var, punto) - Calcula el límite. Ej: limite(sin(x)/x, x, 0)",
|
|
112
|
+
"sumatoria": "💡 sumatoria(expr, var, a, b) - Suma de a hasta b. Ej: sumatoria(n^2, n, 1, 10)",
|
|
113
|
+
"simplificar": "💡 simplificar(expr) - Reduce la expresión. Ej: simplificar((x^2-1)/(x-1))",
|
|
114
|
+
"expandir": "💡 expandir(expr) - Expande polinomios. Ej: expandir((x+1)^2)",
|
|
115
|
+
"factorizar": "💡 factorizar(expr) - Factoriza polinomios. Ej: factorizar(x^2 - 1)",
|
|
116
|
+
"resolver": "💡 resolver(expr, var) - Encuentra las raíces. Ej: resolver(x^2 - 4, x)",
|
|
117
|
+
"sonify": "💡 sonify(expr) - Genera y reproduce audio. Ej: sonify(sin(440*2*pi*t))",
|
|
118
|
+
"distancia": "💡 distancia(p1, p2) - Distancia Euclidiana. Ej: distancia((0,0), (3,4))",
|
|
119
|
+
"matrix": "💡 matrix([[a,b],[c,d]]) - Crea una matriz. Ej: matrix([[1,2],[3,4]])",
|
|
120
|
+
"help": "💡 presiona Enter para ver la ayuda completa.",
|
|
121
|
+
"clear": "💡 Limpia la pantalla.",
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
def compose(self) -> ComposeResult:
|
|
125
|
+
"""Compose the UI."""
|
|
126
|
+
yield Header(show_clock=True)
|
|
127
|
+
yield Static("Binary EquaLab PRO terminal", id="welcome")
|
|
128
|
+
yield RichLog(id="output", markup=True, wrap=True)
|
|
129
|
+
yield Static("", id="help-bar") # New Help Bar
|
|
130
|
+
yield Input(
|
|
131
|
+
placeholder=">>> Escribe una expresión (ej. integrate(x^2))...",
|
|
132
|
+
id="input",
|
|
133
|
+
suggester=MathSuggester()
|
|
134
|
+
)
|
|
135
|
+
yield Footer()
|
|
136
|
+
|
|
137
|
+
def on_mount(self) -> None:
|
|
138
|
+
"""Called when app starts."""
|
|
139
|
+
self.title = "Binary EquaLab TUI"
|
|
140
|
+
log = self.query_one(RichLog)
|
|
141
|
+
log.write("[bold green]System Ready.[/] Type 'help' for commands.")
|
|
142
|
+
log.write("[dim italic]Consejo: Usa 'sonify(sin(440*t))' para escuchar ecuaciones.[/dim italic]")
|
|
143
|
+
|
|
144
|
+
@on(Input.Changed)
|
|
145
|
+
def handle_input_change(self, event: Input.Changed) -> None:
|
|
146
|
+
"""Update help bar based on input."""
|
|
147
|
+
val = event.value.strip().lower()
|
|
148
|
+
help_bar = self.query_one("#help-bar", Static)
|
|
149
|
+
|
|
150
|
+
# Simple prefix check
|
|
151
|
+
found = False
|
|
152
|
+
for cmd, doc in self.HELP_DOCS.items():
|
|
153
|
+
if val.startswith(cmd):
|
|
154
|
+
help_bar.update(doc)
|
|
155
|
+
help_bar.styles.color = "#38bdf8" # Sky blue
|
|
156
|
+
found = True
|
|
157
|
+
break
|
|
158
|
+
|
|
159
|
+
if not found:
|
|
160
|
+
help_bar.update("")
|
|
161
|
+
|
|
162
|
+
@on(Input.Submitted)
|
|
163
|
+
def handle_input(self, event: Input.Submitted) -> None:
|
|
164
|
+
"""Handle input submission."""
|
|
165
|
+
command = event.value.strip()
|
|
166
|
+
if not command:
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
log = self.query_one(RichLog)
|
|
170
|
+
input_widget = self.query_one(Input)
|
|
171
|
+
help_bar = self.query_one("#help-bar", Static)
|
|
172
|
+
|
|
173
|
+
# Clear help
|
|
174
|
+
help_bar.update("")
|
|
175
|
+
|
|
176
|
+
# Add to local history
|
|
177
|
+
self.history.append(command)
|
|
178
|
+
self.history_index = len(self.history)
|
|
179
|
+
|
|
180
|
+
# Echo Input (Jupyter style)
|
|
181
|
+
count = self.execution_count
|
|
182
|
+
log.write(f"\n[bold blue]In [{count}]:[/] [white]{command}[/]")
|
|
183
|
+
|
|
184
|
+
# Clear input
|
|
185
|
+
input_widget.value = ""
|
|
186
|
+
|
|
187
|
+
if command.lower() in ('exit', 'quit'):
|
|
188
|
+
self.exit()
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
if command.lower() == 'clear':
|
|
192
|
+
log.clear()
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
# Process logic
|
|
196
|
+
try:
|
|
197
|
+
result = self.engine.evaluate(command)
|
|
198
|
+
|
|
199
|
+
if result is not None:
|
|
200
|
+
# Pretty print result (ASCII/Unicode Art)
|
|
201
|
+
pretty_result = pretty(result, use_unicode=True)
|
|
202
|
+
|
|
203
|
+
# Check for audio file output (Sonify)
|
|
204
|
+
if isinstance(result, str) and result.endswith('.wav'):
|
|
205
|
+
import os
|
|
206
|
+
import platform
|
|
207
|
+
if platform.system() == "Windows":
|
|
208
|
+
# Auto-play on Windows
|
|
209
|
+
try:
|
|
210
|
+
os.startfile(result)
|
|
211
|
+
log.write("[bold green]🎵 Reproduciendo audio...[/]")
|
|
212
|
+
except Exception as e:
|
|
213
|
+
log.write(f"[bold red]Error reproduciendo audio: {e}[/]")
|
|
214
|
+
|
|
215
|
+
# Format output
|
|
216
|
+
log.write(f"[bold orange1]Out[{count}]:[/]")
|
|
217
|
+
log.write(pretty_result)
|
|
218
|
+
|
|
219
|
+
except Exception as e:
|
|
220
|
+
log.write(f"[bold red]Error:[/bold red] {e}")
|
|
221
|
+
|
|
222
|
+
self.execution_count += 1
|
|
223
|
+
|
|
224
|
+
def on_key(self, event: events.Key) -> None:
|
|
225
|
+
"""Handle global key events for history navigation."""
|
|
226
|
+
input_widget = self.query_one(Input)
|
|
227
|
+
|
|
228
|
+
if not self.history:
|
|
229
|
+
return
|
|
230
|
+
|
|
231
|
+
if event.key == "up":
|
|
232
|
+
self.history_index = max(0, self.history_index - 1)
|
|
233
|
+
if self.history_index < len(self.history):
|
|
234
|
+
input_widget.value = self.history[self.history_index]
|
|
235
|
+
input_widget.cursor_position = len(input_widget.value)
|
|
236
|
+
event.stop() # Prevent default behavior
|
|
237
|
+
|
|
238
|
+
elif event.key == "down":
|
|
239
|
+
self.history_index = min(len(self.history), self.history_index + 1)
|
|
240
|
+
if self.history_index < len(self.history):
|
|
241
|
+
input_widget.value = self.history[self.history_index]
|
|
242
|
+
else:
|
|
243
|
+
input_widget.value = ""
|
|
244
|
+
input_widget.cursor_position = len(input_widget.value)
|
|
245
|
+
event.stop()
|
|
246
|
+
|
|
247
|
+
def action_clear_screen(self) -> None:
|
|
248
|
+
self.query_one(RichLog).clear()
|
|
249
|
+
|
|
250
|
+
if __name__ == "__main__":
|
|
251
|
+
app = BinaryTUI()
|
|
252
|
+
app.run()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: binary-equalab
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.0b3
|
|
4
4
|
Summary: Binary Equalab is a symbolic mathematics toolkit focused on clarity, performance and educational value. Designed for students, engineers and curious minds who want math to feel alive.
|
|
5
5
|
Project-URL: Homepage, https://github.com/AldrasTeam/BinaryEquaLab
|
|
6
6
|
Project-URL: Repository, https://github.com/AldrasTeam/BinaryEquaLab
|
|
@@ -26,6 +26,7 @@ Requires-Dist: requests>=2.31.0
|
|
|
26
26
|
Requires-Dist: rich>=13.0.0
|
|
27
27
|
Requires-Dist: scipy>=1.10.0
|
|
28
28
|
Requires-Dist: sympy>=1.12
|
|
29
|
+
Requires-Dist: textual>=0.40.0
|
|
29
30
|
Requires-Dist: typer>=0.9.0
|
|
30
31
|
Provides-Extra: dev
|
|
31
32
|
Requires-Dist: black>=23.0; extra == 'dev'
|
|
@@ -91,7 +92,21 @@ Soporta:
|
|
|
91
92
|
|
|
92
93
|
## 🚀 Uso del CLI
|
|
93
94
|
|
|
94
|
-
###
|
|
95
|
+
### 🎮 Interactive TUI (Nuevo v3.0)
|
|
96
|
+
La nueva interfaz inmersiva tipo "Jupyter-Lite".
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
binary-math tui
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Features:**
|
|
103
|
+
- ✨ **Autocompletado & Hints:** Escribe y recibe ayuda contextual.
|
|
104
|
+
- 🎹 **Sonificación:** `sonify(sin(440*t))` reproduce el audio automáticamente.
|
|
105
|
+
- 📜 **Historial:** Navega con ↑ / ↓.
|
|
106
|
+
- 🖥️ **Pretty Print:** Ecuaciones renderizadas con Unicode.
|
|
107
|
+
|
|
108
|
+
### Rápido REPL Mode
|
|
109
|
+
Para consultas rápidas sin interfaz gráfica:
|
|
95
110
|
```bash
|
|
96
111
|
binary-math
|
|
97
112
|
```
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
binary_equalab/__init__.py,sha256=WFF_aioc3t6xkz0qU_RI4FeqY4UT92c8wE5cDDJWenY,675
|
|
2
|
+
binary_equalab/cli.py,sha256=XQ-PDw8KfQfkaUr7kjG_fNXObMp9IKk9CAeauS6kQG8,12510
|
|
3
|
+
binary_equalab/engine.py,sha256=0ZC6QcTmEKuKl0GEFioj5BR1wK0e7P8_F1W20F0zk9s,17474
|
|
4
|
+
binary_equalab/functions.py,sha256=F6GPCXoScCHVZ4CDEE2Rns5i3x89DAcKRVBte88j9oE,569
|
|
5
|
+
binary_equalab/geometry.py,sha256=YPn59oQtTY_hb4LZyIEJUvN1pWJz_y5uk8gVBio1Gwo,2217
|
|
6
|
+
binary_equalab/giac_poc.py,sha256=_XdxE2lrvFH7M6qDfzSQHRdplxPzKhKfIWAcsOQq8Ho,2226
|
|
7
|
+
binary_equalab/kimi_service.py,sha256=wofChDT1tB9lN72jwC6DcMdd8Xg2VfbJj2Mt5seQ-R0,5491
|
|
8
|
+
binary_equalab/parser_enhanced.py,sha256=P7oMnGHWn3xoqVChkPJ6B_i_IR9TeJMJXBjKoCO13ts,2735
|
|
9
|
+
binary_equalab/shell_setup.py,sha256=9Ax_QcHEAopYIjShuBaHglg4HPysXJKbNL3D_FjexrU,4518
|
|
10
|
+
binary_equalab/sonify.py,sha256=6SUVzfVB4A78Elr-6tz9KxGLp1C0KbslAaTOkkavUNE,3954
|
|
11
|
+
binary_equalab/tui.py,sha256=ZID98jgy4upEUxLjeRAPmP_VUbcmspMkII_hawTHZcc,9140
|
|
12
|
+
binary_equalab/services/kimi_service.py,sha256=rhEWWbw2NG-bBQR5STZi7MQuJzRNW4qmMBEc-FLic-k,4594
|
|
13
|
+
binary_equalab-3.0.0b3.dist-info/METADATA,sha256=qIZnHv4uJXFHBk18N5yxuqeBzLiuLG-69mSoApX9514,5845
|
|
14
|
+
binary_equalab-3.0.0b3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
15
|
+
binary_equalab-3.0.0b3.dist-info/entry_points.txt,sha256=2Io1Wi069dvgPHmGXmAT1a6rNA-DIq7tChai69jtbF0,119
|
|
16
|
+
binary_equalab-3.0.0b3.dist-info/RECORD,,
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
binary_equalab/__init__.py,sha256=WFF_aioc3t6xkz0qU_RI4FeqY4UT92c8wE5cDDJWenY,675
|
|
2
|
-
binary_equalab/cli.py,sha256=kEA7Vw3gQZrRJJRFxDppqUo253jem424ZuczIwEjz60,12219
|
|
3
|
-
binary_equalab/engine.py,sha256=WhkKpdDCg2zqH9cMXDPMQ7Ut7Awyn4WzkVrRVP7_dtE,15931
|
|
4
|
-
binary_equalab/functions.py,sha256=F6GPCXoScCHVZ4CDEE2Rns5i3x89DAcKRVBte88j9oE,569
|
|
5
|
-
binary_equalab/geometry.py,sha256=YPn59oQtTY_hb4LZyIEJUvN1pWJz_y5uk8gVBio1Gwo,2217
|
|
6
|
-
binary_equalab/giac_poc.py,sha256=_XdxE2lrvFH7M6qDfzSQHRdplxPzKhKfIWAcsOQq8Ho,2226
|
|
7
|
-
binary_equalab/kimi_service.py,sha256=wofChDT1tB9lN72jwC6DcMdd8Xg2VfbJj2Mt5seQ-R0,5491
|
|
8
|
-
binary_equalab/parser_enhanced.py,sha256=P7oMnGHWn3xoqVChkPJ6B_i_IR9TeJMJXBjKoCO13ts,2735
|
|
9
|
-
binary_equalab/shell_setup.py,sha256=9Ax_QcHEAopYIjShuBaHglg4HPysXJKbNL3D_FjexrU,4518
|
|
10
|
-
binary_equalab/sonify.py,sha256=HfAYPtXLRyb3miLBsd8liqk4D5hvXVBHEEyV6fj9Za8,3274
|
|
11
|
-
binary_equalab-3.0.0b1.dist-info/METADATA,sha256=is9DrY6iTKa1t6Z38P2SvgFbGfvzND3BhCWWGyP7_Jw,5357
|
|
12
|
-
binary_equalab-3.0.0b1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
13
|
-
binary_equalab-3.0.0b1.dist-info/entry_points.txt,sha256=2Io1Wi069dvgPHmGXmAT1a6rNA-DIq7tChai69jtbF0,119
|
|
14
|
-
binary_equalab-3.0.0b1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|