Skip to content

Commit c8aadf0

Browse files
committed
Add Streamlit and Textual UIs, update CLI and deps
Introduces a Streamlit-based web UI (app.py) and a Textual TUI (patternlab/tui.py). Updates CLI to support launching both UIs, replacing uvicorn for the web UI. Adds 'ui' dependencies group with streamlit and textual, and downgrades oletools version for compatibility.
1 parent d735252 commit c8aadf0

4 files changed

Lines changed: 518 additions & 16 deletions

File tree

app.py

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import streamlit as st
2+
import threading
3+
from patternlab.engine import Engine
4+
import pandas as pd
5+
6+
st.set_page_config(page_title="PatternLab Analizi", layout="wide")
7+
8+
engine = Engine()
9+
10+
def run_analysis(config):
11+
result = engine.analyze(config)
12+
st.session_state['analysis_result'] = result
13+
14+
def main():
15+
st.title("Pattern Analyzer")
16+
st.write("Bu, temel Streamlit uygulamasıdır.")
17+
18+
# Sidebar control panel
19+
st.sidebar.header("Kontrol Paneli")
20+
21+
uploaded_file = st.sidebar.file_uploader(
22+
"Dosya Yükle", accept_multiple_files=False, type=['bin', 'txt']
23+
)
24+
25+
text_input = st.sidebar.text_area(
26+
"Veri (Base64 veya metin)",
27+
placeholder='Base64 encoded data or plain text',
28+
height=150,
29+
)
30+
31+
# Use engine to populate available tests/transforms; default select all
32+
available_tests = engine.get_available_tests()
33+
available_transforms = engine.get_available_transforms()
34+
35+
selected_tests = st.sidebar.multiselect(
36+
"Testler",
37+
options=available_tests,
38+
default=available_tests,
39+
)
40+
41+
selected_transforms = st.sidebar.multiselect(
42+
"Transformlar",
43+
options=available_transforms,
44+
default=available_transforms,
45+
)
46+
47+
analizi_baslat = st.sidebar.button("Analizi Başlat")
48+
49+
# Persist current UI values into session_state so we can build config from it
50+
if 'uploaded_file' not in st.session_state:
51+
st.session_state.uploaded_file = uploaded_file
52+
if 'text_input' not in st.session_state:
53+
st.session_state.text_input = text_input
54+
if 'selected_tests' not in st.session_state:
55+
st.session_state.selected_tests = selected_tests
56+
if 'selected_transforms' not in st.session_state:
57+
st.session_state.selected_transforms = selected_transforms
58+
59+
if analizi_baslat:
60+
# Update session_state with latest values
61+
st.session_state.uploaded_file = uploaded_file
62+
st.session_state.text_input = text_input
63+
st.session_state.selected_tests = selected_tests
64+
st.session_state.selected_transforms = selected_transforms
65+
66+
# Build config from session_state
67+
config = {
68+
'data': {
69+
'file': st.session_state.uploaded_file,
70+
'text': st.session_state.text_input,
71+
},
72+
'tests': st.session_state.selected_tests,
73+
'transforms': st.session_state.selected_transforms,
74+
}
75+
76+
with st.spinner('Analiz yapılıyor...'):
77+
threading.Thread(target=run_analysis, args=(config,)).start()
78+
79+
# Gösterim: Analiz sonucu session_state'te varsa ana ekranda göster
80+
if 'analysis_result' in st.session_state:
81+
st.header("Analiz Sonuçları")
82+
result = st.session_state['analysis_result']
83+
84+
# scorecard'ı st.metric ile göster
85+
scorecard = result.get('scorecard', {}) if isinstance(result, dict) else {}
86+
if scorecard:
87+
st.subheader("Scorecard")
88+
keys = list(scorecard.keys())
89+
if keys:
90+
cols = st.columns(len(keys))
91+
for i, k in enumerate(keys):
92+
try:
93+
val = scorecard[k]
94+
except Exception:
95+
val = str(scorecard.get(k))
96+
cols[i].metric(str(k), str(val))
97+
98+
# results listesini dataframe içinde göster, p-value'a göre renklendirme
99+
results = result.get('results', []) if isinstance(result, dict) else []
100+
st.subheader("Bulgular")
101+
if results:
102+
df = pd.DataFrame(results)
103+
if 'p_value' in df.columns:
104+
def _p_style(v):
105+
try:
106+
return 'background-color: red' if float(v) < 0.05 else ''
107+
except Exception:
108+
return ''
109+
styled = df.style.applymap(_p_style, subset=['p_value'])
110+
st.dataframe(styled)
111+
else:
112+
st.dataframe(df)
113+
114+
# Kullanıcının bir sonucu seçebilmesi için selectbox (tablodan tıklama benzeri)
115+
option_labels = []
116+
for idx, row in df.iterrows():
117+
name = row.get('name') if 'name' in row else None
118+
label = f"{idx}"
119+
if name:
120+
label = f"{idx} - {name}"
121+
option_labels.append(label)
122+
123+
selected_label = st.selectbox("Bir sonuç seçin", options=option_labels)
124+
if selected_label is not None and selected_label != "":
125+
selected_idx = int(str(selected_label).split(" - ")[0])
126+
selected_result = results[selected_idx]
127+
st.subheader("Seçilen Sonucun Detayları")
128+
st.json(selected_result)
129+
130+
# Eğer seçilen sonucun görselleri varsa göster (SVG/PNG)
131+
visuals = selected_result.get('visuals') if isinstance(selected_result, dict) else None
132+
if visuals:
133+
st.subheader("Görseller")
134+
for v in visuals:
135+
try:
136+
st.image(v, use_column_width=True)
137+
except Exception:
138+
# Eğer doğrudan gösterilemiyorsa base64 veya ham SVG olabilir; yine de dene
139+
st.image(v)
140+
else:
141+
st.info("Henüz analiz sonucu yok veya sonuç listesi boş.")
142+
143+
if __name__ == "__main__":
144+
main()

patternlab/cli.py

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -366,30 +366,45 @@ def _run_one(bts: bytes):
366366
@click.option('--port', 'port', default=8000, type=int, help='Port for the UI server')
367367
@click.option('--reload', 'reload', is_flag=True, default=False, help='Enable auto-reload (development only)')
368368
def serve_ui(host, port, reload):
369-
"""Serve the self-hosted frontend UI together with the API using uvicorn.
369+
"""Serve the self-hosted frontend UI using Streamlit.
370370
371-
Serves the FastAPI app which already mounts the static UI at /ui.
371+
This command runs: streamlit run app.py --server.address HOST --server.port PORT
372372
"""
373373
try:
374-
click.echo(f"Starting PatternLab UI at http://{host}:{port}/")
375-
# Lazy import uvicorn so CLI can be used without the optional dependency installed.
374+
click.echo(f"Starting PatternLab Streamlit UI at http://{host}:{port}/")
376375
try:
377-
import uvicorn # type: ignore
378-
except Exception:
379-
# Provide a clear actionable error via Click
380-
raise click.ClickException(
381-
"uvicorn is required to run 'serve-ui'.\n"
382-
"Install it with: pip install \"uvicorn[standard]\"\n"
383-
"Or run the server manually: python -m uvicorn patternlab.api:app --host {host} --port {port}".format(host=host, port=port)
384-
)
385-
# Use module path so uvicorn picks up patternlab.api:app
386-
uvicorn.run("patternlab.api:app", host=host, port=port, reload=reload)
376+
import shutil
377+
import subprocess
378+
# Ensure streamlit executable is available
379+
if shutil.which('streamlit') is None:
380+
raise click.ClickException(
381+
"streamlit is required to run 'serve-ui'.\n"
382+
"Install it with: pip install streamlit\n"
383+
"Or run the server manually: streamlit run app.py --server.address {host} --server.port {port}".format(host=host, port=port)
384+
)
385+
cmd = ['streamlit', 'run', 'app.py', '--server.address', host, '--server.port', str(port)]
386+
subprocess.run(cmd, check=True)
387+
except click.ClickException:
388+
raise
389+
except Exception as e:
390+
# Wrap subprocess/import errors into a ClickException for clearer output
391+
raise click.ClickException(f"Failed to start streamlit: {e}")
387392
except click.ClickException:
388-
# Let Click handle this exception type (it will print the message)
389393
raise
390394
except Exception as e:
391395
click.echo(f"serve-ui failed: {e}", err=True)
392396
raise click.Abort()
393397

398+
@cli.command()
399+
def tui():
400+
"""Start the PatternLab Textual TUI."""
401+
try:
402+
click.echo("Starting PatternLab Textual TUI...")
403+
from .tui import PatternLabTUI
404+
PatternLabTUI().run()
405+
except Exception as e:
406+
click.echo(f"Failed to start TUI: {e}", err=True)
407+
raise click.Abort()
408+
394409
if __name__ == '__main__':
395410
cli()

0 commit comments

Comments
 (0)