misata 0.3.0b0__py3-none-any.whl → 0.5.0__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.
- misata/__init__.py +1 -1
- misata/agents/__init__.py +23 -0
- misata/agents/pipeline.py +286 -0
- misata/causal/__init__.py +5 -0
- misata/causal/graph.py +109 -0
- misata/causal/solver.py +115 -0
- misata/cli.py +31 -0
- misata/generators/__init__.py +19 -0
- misata/generators/copula.py +198 -0
- misata/llm_parser.py +180 -137
- misata/quality.py +78 -33
- misata/reference_data.py +221 -0
- misata/research/__init__.py +3 -0
- misata/research/agent.py +70 -0
- misata/schema.py +25 -0
- misata/simulator.py +264 -12
- misata/smart_values.py +144 -6
- misata/studio/__init__.py +55 -0
- misata/studio/app.py +49 -0
- misata/studio/components/inspector.py +81 -0
- misata/studio/components/sidebar.py +35 -0
- misata/studio/constraint_generator.py +781 -0
- misata/studio/inference.py +319 -0
- misata/studio/outcome_curve.py +284 -0
- misata/studio/state/store.py +55 -0
- misata/studio/tabs/configure.py +50 -0
- misata/studio/tabs/generate.py +117 -0
- misata/studio/tabs/outcome_curve.py +149 -0
- misata/studio/tabs/schema_designer.py +217 -0
- misata/studio/utils/styles.py +143 -0
- misata/studio_constraints/__init__.py +29 -0
- misata/studio_constraints/z3_solver.py +259 -0
- {misata-0.3.0b0.dist-info → misata-0.5.0.dist-info}/METADATA +13 -2
- misata-0.5.0.dist-info/RECORD +61 -0
- {misata-0.3.0b0.dist-info → misata-0.5.0.dist-info}/WHEEL +1 -1
- {misata-0.3.0b0.dist-info → misata-0.5.0.dist-info}/entry_points.txt +1 -0
- misata-0.3.0b0.dist-info/RECORD +0 -37
- /misata/{generators.py → generators_legacy.py} +0 -0
- {misata-0.3.0b0.dist-info → misata-0.5.0.dist-info}/licenses/LICENSE +0 -0
- {misata-0.3.0b0.dist-info → misata-0.5.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import streamlit as st
|
|
2
|
+
from misata.studio.state.store import StudioStore
|
|
3
|
+
|
|
4
|
+
def render_configure_tab():
|
|
5
|
+
"""Render the configuration tab."""
|
|
6
|
+
st.markdown("#### Generation Configuration")
|
|
7
|
+
|
|
8
|
+
config = StudioStore.get('warehouse_config', {})
|
|
9
|
+
|
|
10
|
+
col1, col2 = st.columns(2)
|
|
11
|
+
|
|
12
|
+
with col1:
|
|
13
|
+
avg_txn = st.number_input(
|
|
14
|
+
"Avg Transaction Value ($)",
|
|
15
|
+
min_value=1.0,
|
|
16
|
+
value=config.get('avg_transaction', 50.0),
|
|
17
|
+
help="Average value of a single invoice/transaction"
|
|
18
|
+
)
|
|
19
|
+
config['avg_transaction'] = avg_txn
|
|
20
|
+
|
|
21
|
+
with col2:
|
|
22
|
+
seed = st.number_input(
|
|
23
|
+
"Random Seed",
|
|
24
|
+
min_value=1,
|
|
25
|
+
value=config.get('seed', 42),
|
|
26
|
+
help="Fixed seed for reproducibility"
|
|
27
|
+
)
|
|
28
|
+
config['seed'] = seed
|
|
29
|
+
|
|
30
|
+
st.markdown("#### Tier Distribution")
|
|
31
|
+
st.caption("Distribution of Customer Segments")
|
|
32
|
+
|
|
33
|
+
c1, c2, c3 = st.columns(3)
|
|
34
|
+
defaults = config.get('tier_distribution', [0.5, 0.3, 0.2])
|
|
35
|
+
|
|
36
|
+
with c1:
|
|
37
|
+
tier_basic = st.slider("Basic %", 0, 100, int(defaults[0]*100))
|
|
38
|
+
with c2:
|
|
39
|
+
tier_pro = st.slider("Pro %", 0, 100, int(defaults[1]*100))
|
|
40
|
+
with c3:
|
|
41
|
+
tier_enterprise = st.slider("Enterprise %", 0, 100, int(defaults[2]*100))
|
|
42
|
+
|
|
43
|
+
config['tier_distribution'] = [tier_basic/100, tier_pro/100, tier_enterprise/100]
|
|
44
|
+
StudioStore.set('warehouse_config', config)
|
|
45
|
+
|
|
46
|
+
# NEXT BUTTON (Bottom of Tab)
|
|
47
|
+
st.markdown("<br><br>", unsafe_allow_html=True)
|
|
48
|
+
if st.button("Next: Generate Data →", type="primary", use_container_width=True):
|
|
49
|
+
StudioStore.set('active_tab', "Generate")
|
|
50
|
+
st.rerun()
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import streamlit as st
|
|
2
|
+
from datetime import datetime, timedelta
|
|
3
|
+
from misata.studio.state.store import StudioStore
|
|
4
|
+
from misata.studio.outcome_curve import OutcomeCurve, CurvePoint
|
|
5
|
+
from misata.studio.constraint_generator import (
|
|
6
|
+
create_service_company_schema, generate_constrained_warehouse, convert_schema_config_to_spec
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
def render_generate_tab():
|
|
10
|
+
"""Render the generation and export tab."""
|
|
11
|
+
st.markdown("#### Generate Complete Data Warehouse")
|
|
12
|
+
|
|
13
|
+
# Summary
|
|
14
|
+
schema = StudioStore.get('warehouse_schema', {})
|
|
15
|
+
curve = StudioStore.get('warehouse_curve', [])
|
|
16
|
+
config = StudioStore.get('warehouse_config', {})
|
|
17
|
+
schema_source = StudioStore.get('schema_source', 'Template')
|
|
18
|
+
schema_config = StudioStore.get('schema_config')
|
|
19
|
+
|
|
20
|
+
schema_name = "Service Company"
|
|
21
|
+
if schema_source == "AI" and schema_config:
|
|
22
|
+
schema_name = schema_config.name
|
|
23
|
+
|
|
24
|
+
st.markdown(f"""
|
|
25
|
+
**Schema:** {schema_name}
|
|
26
|
+
**Revenue Periods:** {len(curve)}
|
|
27
|
+
**Total Target Revenue:** ${sum(curve):,.0f}
|
|
28
|
+
""")
|
|
29
|
+
|
|
30
|
+
if st.button("Generate Warehouse ▸", type="primary", use_container_width=True):
|
|
31
|
+
with st.spinner("Generating complete data warehouse..."):
|
|
32
|
+
# Build revenue curve
|
|
33
|
+
start_date = datetime.combine(
|
|
34
|
+
StudioStore.get('start_date_input', datetime.now().date()),
|
|
35
|
+
datetime.min.time()
|
|
36
|
+
)
|
|
37
|
+
time_unit_val = "month" # Simplify for now
|
|
38
|
+
|
|
39
|
+
revenue_curve = OutcomeCurve(
|
|
40
|
+
metric_name='revenue',
|
|
41
|
+
time_unit=time_unit_val,
|
|
42
|
+
points=[
|
|
43
|
+
CurvePoint(timestamp=start_date + timedelta(days=30*i), value=v)
|
|
44
|
+
for i, v in enumerate(curve)
|
|
45
|
+
],
|
|
46
|
+
avg_transaction_value=config.get('avg_transaction', 50.0)
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Create warehouse spec
|
|
50
|
+
spec = None
|
|
51
|
+
if schema_source == "AI" and schema_config:
|
|
52
|
+
spec = convert_schema_config_to_spec(
|
|
53
|
+
schema_config,
|
|
54
|
+
revenue_curve=revenue_curve
|
|
55
|
+
)
|
|
56
|
+
else:
|
|
57
|
+
spec = create_service_company_schema(
|
|
58
|
+
customer_count=schema.get('customer_count', 500),
|
|
59
|
+
project_count=schema.get('project_count', 2000),
|
|
60
|
+
revenue_curve=revenue_curve
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Generate
|
|
64
|
+
result = generate_constrained_warehouse(spec, seed=config.get('seed', 42))
|
|
65
|
+
|
|
66
|
+
# Store result
|
|
67
|
+
StudioStore.set('generated_warehouse', result)
|
|
68
|
+
StudioStore.set('warehouse_generated', True)
|
|
69
|
+
|
|
70
|
+
st.success("◆ Data warehouse generated successfully!")
|
|
71
|
+
|
|
72
|
+
# Show results
|
|
73
|
+
result = StudioStore.get('generated_warehouse')
|
|
74
|
+
if result:
|
|
75
|
+
# Show results
|
|
76
|
+
for table_name, df in result.items():
|
|
77
|
+
st.write(f"**{table_name}** ({len(df):,} rows)")
|
|
78
|
+
st.dataframe(df.head(100), use_container_width=True)
|
|
79
|
+
|
|
80
|
+
# Dynamic Verification
|
|
81
|
+
# (Simplified for the modular view, but should use spec.constraints)
|
|
82
|
+
# Using heuristic check for now if available
|
|
83
|
+
if "invoices" in result:
|
|
84
|
+
invoices = result['invoices']
|
|
85
|
+
if 'amount' in invoices.columns:
|
|
86
|
+
total_actual = invoices['amount'].sum()
|
|
87
|
+
total_expected = sum(curve)
|
|
88
|
+
match_pct = 100 * total_actual / total_expected if total_expected > 0 else 0
|
|
89
|
+
|
|
90
|
+
st.markdown("#### Verification")
|
|
91
|
+
col1, col2, col3 = st.columns(3)
|
|
92
|
+
with col1:
|
|
93
|
+
st.metric("Target Revenue", f"${total_expected:,.0f}")
|
|
94
|
+
with col2:
|
|
95
|
+
st.metric("Actual Revenue", f"${total_actual:,.0f}")
|
|
96
|
+
with col3:
|
|
97
|
+
st.metric("Match", f"{match_pct:.2f}%")
|
|
98
|
+
|
|
99
|
+
# Export
|
|
100
|
+
st.markdown("#### Export")
|
|
101
|
+
|
|
102
|
+
if st.button("Download All Tables ▼"):
|
|
103
|
+
import io
|
|
104
|
+
import zipfile
|
|
105
|
+
|
|
106
|
+
zip_buffer = io.BytesIO()
|
|
107
|
+
with zipfile.ZipFile(zip_buffer, "w") as zf:
|
|
108
|
+
for name, df in result.items():
|
|
109
|
+
csv_data = df.to_csv(index=False)
|
|
110
|
+
zf.writestr(f"{name}.csv", csv_data)
|
|
111
|
+
|
|
112
|
+
st.download_button(
|
|
113
|
+
"Download ZIP",
|
|
114
|
+
zip_buffer.getvalue(),
|
|
115
|
+
file_name="misata_warehouse.zip",
|
|
116
|
+
mime="application/zip"
|
|
117
|
+
)
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import streamlit as st
|
|
2
|
+
import plotly.graph_objects as go
|
|
3
|
+
from misata.studio.state.store import StudioStore
|
|
4
|
+
from misata.studio.outcome_curve import get_curve_presets, CurvePoint, OutcomeCurve
|
|
5
|
+
|
|
6
|
+
def render_outcome_tab():
|
|
7
|
+
"""Render the Outcome Constraint configuration tab."""
|
|
8
|
+
st.markdown("#### Define Outcome Constraints")
|
|
9
|
+
|
|
10
|
+
# Check if a constraint is selected
|
|
11
|
+
selected = StudioStore.get('selected_constraint')
|
|
12
|
+
|
|
13
|
+
if not selected:
|
|
14
|
+
# In AI mode, let them pick any float column
|
|
15
|
+
schema_source = StudioStore.get('schema_source')
|
|
16
|
+
schema_config = StudioStore.get('schema_config')
|
|
17
|
+
|
|
18
|
+
if schema_source == "AI" and schema_config:
|
|
19
|
+
st.info("Pick a column to constrain:")
|
|
20
|
+
# List candidate columns
|
|
21
|
+
candidates = []
|
|
22
|
+
for t_name, cols in schema_config.columns.items():
|
|
23
|
+
for c in cols:
|
|
24
|
+
if c.type in ['float', 'int'] and c.name in ['amount', 'price', 'total', 'revenue', 'value', 'cost']:
|
|
25
|
+
candidates.append(f"{t_name}.{c.name}")
|
|
26
|
+
|
|
27
|
+
if candidates:
|
|
28
|
+
selected_col = st.selectbox("Column", candidates)
|
|
29
|
+
if st.button("Add Constraint"):
|
|
30
|
+
StudioStore.set('selected_constraint', selected_col)
|
|
31
|
+
st.rerun()
|
|
32
|
+
else:
|
|
33
|
+
st.warning("No suitable columns found for outcome constraints (need float/int like 'amount').")
|
|
34
|
+
|
|
35
|
+
else:
|
|
36
|
+
st.info("👈 Select a numerical column (like **invoices.amount**) from the Schema tab to add a constraint.")
|
|
37
|
+
st.markdown("""
|
|
38
|
+
**What is an Outcome Constraint?**
|
|
39
|
+
It forces the generated data to match a specific aggregated shape over time.
|
|
40
|
+
|
|
41
|
+
*Example: Draw a revenue curve, and Misata generates millions of transactions that sum up exactly to that curve.*
|
|
42
|
+
""")
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
st.markdown(f"""
|
|
46
|
+
<div style="background:rgba(52, 78, 65, 0.05); border-left: 3px solid var(--sage-hunter); padding:1rem; border-radius:4px; margin-bottom:2rem;">
|
|
47
|
+
<span style="font-size:0.8rem; text-transform:uppercase; color:var(--text-tertiary);">Active Constraint</span>
|
|
48
|
+
<div style="font-size:1.2rem; font-weight:600; font-family:var(--font-mono); color:var(--sage-hunter);">
|
|
49
|
+
{selected}
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
""", unsafe_allow_html=True)
|
|
53
|
+
|
|
54
|
+
col1, col2 = st.columns([1, 2])
|
|
55
|
+
|
|
56
|
+
with col1:
|
|
57
|
+
st.markdown("#### Presets")
|
|
58
|
+
presets = get_curve_presets()
|
|
59
|
+
|
|
60
|
+
# Current curve state
|
|
61
|
+
current_curve = StudioStore.get('warehouse_curve', [100000] * 12)
|
|
62
|
+
|
|
63
|
+
selected_preset = st.radio(
|
|
64
|
+
"Shape",
|
|
65
|
+
list(presets.keys()),
|
|
66
|
+
help="Choose a starting shape for your curve"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Scale
|
|
70
|
+
total_target = st.number_input(
|
|
71
|
+
"Total Target",
|
|
72
|
+
min_value=1000,
|
|
73
|
+
value=int(sum(current_curve)),
|
|
74
|
+
step=10000,
|
|
75
|
+
format="%d"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
if st.button("Apply Preset", type="primary", use_container_width=True):
|
|
79
|
+
base_values = presets[selected_preset]
|
|
80
|
+
# Rescale to match target
|
|
81
|
+
current_sum = sum(base_values)
|
|
82
|
+
scale = total_target / current_sum if current_sum > 0 else 1
|
|
83
|
+
|
|
84
|
+
if selected_preset == "Random Volatility":
|
|
85
|
+
import random
|
|
86
|
+
values = [v * scale * random.uniform(0.8, 1.2) for v in base_values]
|
|
87
|
+
else:
|
|
88
|
+
values = [v * scale for v in base_values]
|
|
89
|
+
StudioStore.set('warehouse_curve', values)
|
|
90
|
+
|
|
91
|
+
# NEXT BUTTON (Bottom of Tab)
|
|
92
|
+
st.markdown("<br><br>", unsafe_allow_html=True)
|
|
93
|
+
if st.button("Next: Configure Generation →", type="primary", use_container_width=True):
|
|
94
|
+
StudioStore.set('active_tab', "Configure")
|
|
95
|
+
st.rerun()
|
|
96
|
+
|
|
97
|
+
with col2:
|
|
98
|
+
st.markdown("#### Shape the Curve")
|
|
99
|
+
|
|
100
|
+
# Sliders for each month
|
|
101
|
+
curve_points = StudioStore.get('warehouse_curve', [100000] * 12)
|
|
102
|
+
months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
|
|
103
|
+
|
|
104
|
+
# Interactive chart
|
|
105
|
+
fig = go.Figure()
|
|
106
|
+
fig.add_trace(go.Bar(
|
|
107
|
+
x=months,
|
|
108
|
+
y=curve_points,
|
|
109
|
+
marker_color='#588157',
|
|
110
|
+
opacity=0.8
|
|
111
|
+
))
|
|
112
|
+
|
|
113
|
+
fig.add_trace(go.Scatter(
|
|
114
|
+
x=months,
|
|
115
|
+
y=curve_points,
|
|
116
|
+
mode='lines+markers',
|
|
117
|
+
line=dict(color='#344E41', width=3, shape='spline'),
|
|
118
|
+
marker=dict(size=8, color='#344E41')
|
|
119
|
+
))
|
|
120
|
+
|
|
121
|
+
fig.update_layout(
|
|
122
|
+
paper_bgcolor='rgba(0,0,0,0)',
|
|
123
|
+
plot_bgcolor='rgba(0,0,0,0)',
|
|
124
|
+
margin=dict(l=20, r=20, t=20, b=20),
|
|
125
|
+
height=300,
|
|
126
|
+
showlegend=False
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
st.plotly_chart(fig, use_container_width=True)
|
|
130
|
+
|
|
131
|
+
st.markdown("---")
|
|
132
|
+
st.caption("Adjust Monthly Targets")
|
|
133
|
+
|
|
134
|
+
# 3 columns of 4 sliders
|
|
135
|
+
sc1, sc2, sc3 = st.columns(3)
|
|
136
|
+
cols = [sc1, sc2, sc3]
|
|
137
|
+
|
|
138
|
+
new_curve = list(curve_points)
|
|
139
|
+
for i, month in enumerate(months):
|
|
140
|
+
with cols[i // 4]:
|
|
141
|
+
new_curve[i] = st.slider(
|
|
142
|
+
f"{month}",
|
|
143
|
+
0,
|
|
144
|
+
int(max(curve_points) * 2),
|
|
145
|
+
int(new_curve[i]),
|
|
146
|
+
key=f"slider_{i}"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
StudioStore.set('warehouse_curve', new_curve)
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import streamlit as st
|
|
2
|
+
from misata.studio.state.store import StudioStore
|
|
3
|
+
from misata.llm_parser import generate_schema
|
|
4
|
+
from misata.studio.components.inspector import render_table_inspector
|
|
5
|
+
|
|
6
|
+
def render_schema_tab():
|
|
7
|
+
"""Render the Schema Designer tab."""
|
|
8
|
+
# Check if we are in Inspector Mode
|
|
9
|
+
if StudioStore.get("selected_table"):
|
|
10
|
+
render_table_inspector()
|
|
11
|
+
# Initial view is the inspector, but we also want to see the schema context?
|
|
12
|
+
# For now, let's keep it simple: Modal-like experience.
|
|
13
|
+
# Or render inspector at top, schema below.
|
|
14
|
+
st.markdown("---")
|
|
15
|
+
|
|
16
|
+
st.markdown("#### Schema Designer")
|
|
17
|
+
|
|
18
|
+
col_controls, col_viz = st.columns([1, 1])
|
|
19
|
+
|
|
20
|
+
with col_controls:
|
|
21
|
+
# Schema Source Selection
|
|
22
|
+
source_mode = st.radio(
|
|
23
|
+
"Design Mode",
|
|
24
|
+
["Template", "AI Story 🧠"],
|
|
25
|
+
horizontal=True,
|
|
26
|
+
label_visibility="collapsed"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
if source_mode == "Template":
|
|
30
|
+
schema_type = st.selectbox(
|
|
31
|
+
"Select Template",
|
|
32
|
+
["Service Company", "E-commerce Store", "SaaS Platform", "Custom"],
|
|
33
|
+
label_visibility="collapsed"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
if schema_type == "Service Company":
|
|
37
|
+
# Initialize default if not present
|
|
38
|
+
warehouse_schema = StudioStore.get('warehouse_schema') or {}
|
|
39
|
+
if not warehouse_schema or warehouse_schema.get('type') != 'service_company':
|
|
40
|
+
StudioStore.set('warehouse_schema', {
|
|
41
|
+
"type": "service_company",
|
|
42
|
+
"customer_count": 500,
|
|
43
|
+
"project_count": 2000
|
|
44
|
+
})
|
|
45
|
+
else:
|
|
46
|
+
# AI Story Mode
|
|
47
|
+
schema_type = "AI"
|
|
48
|
+
story = st.text_area(
|
|
49
|
+
"Describe your data needs:",
|
|
50
|
+
placeholder="e.g., A healthcare system with patients, doctors, and appointments...",
|
|
51
|
+
height=100
|
|
52
|
+
)
|
|
53
|
+
if st.button("Generate Schema ✨", type="primary"):
|
|
54
|
+
with st.spinner("consulting the architect..."):
|
|
55
|
+
try:
|
|
56
|
+
# Call LLM
|
|
57
|
+
config = generate_schema(story)
|
|
58
|
+
StudioStore.set('schema_config', config)
|
|
59
|
+
StudioStore.set('schema_source', "AI")
|
|
60
|
+
StudioStore.set('selected_table', None) # Reset selection
|
|
61
|
+
st.success("Schema generated!")
|
|
62
|
+
st.rerun()
|
|
63
|
+
except Exception as e:
|
|
64
|
+
st.error(f"Generation failed: {str(e)}")
|
|
65
|
+
|
|
66
|
+
with col_viz:
|
|
67
|
+
st.markdown("""
|
|
68
|
+
<div style="text-align: center; color: var(--text-secondary); padding: 2rem;">
|
|
69
|
+
<div style="font-size: 0.9rem;">Visual Relationship Mapper</div>
|
|
70
|
+
</div>
|
|
71
|
+
""", unsafe_allow_html=True)
|
|
72
|
+
|
|
73
|
+
st.markdown("---")
|
|
74
|
+
|
|
75
|
+
# RENDER AREA
|
|
76
|
+
schema_source = StudioStore.get("schema_source")
|
|
77
|
+
schema_config = StudioStore.get("schema_config")
|
|
78
|
+
|
|
79
|
+
# 1. AI GENERATED SCHEMA
|
|
80
|
+
if schema_source == "AI" and schema_config:
|
|
81
|
+
st.markdown(f"#### {schema_config.name}")
|
|
82
|
+
st.markdown(f"_{schema_config.description or 'Custom generated schema'}_")
|
|
83
|
+
|
|
84
|
+
# Grid layout for tables
|
|
85
|
+
cols = st.columns(3)
|
|
86
|
+
for i, table in enumerate(schema_config.tables):
|
|
87
|
+
col = cols[i % 3]
|
|
88
|
+
with col:
|
|
89
|
+
# Badge logic
|
|
90
|
+
badge_color = "var(--sage-hunter)"
|
|
91
|
+
badge_text = "Table"
|
|
92
|
+
bg_style = ""
|
|
93
|
+
|
|
94
|
+
if table.is_reference:
|
|
95
|
+
badge_color = "#D4A574" # Gold
|
|
96
|
+
badge_text = "Reference"
|
|
97
|
+
elif hasattr(table, 'row_count') and table.row_count > 10000:
|
|
98
|
+
badge_text = "Transactional"
|
|
99
|
+
bg_style = "border-left-color: var(--sage-hunter); background: rgba(88, 129, 87, 0.05);"
|
|
100
|
+
|
|
101
|
+
# Check if this table is being edited
|
|
102
|
+
is_selected = (StudioStore.get("selected_table") == table.name)
|
|
103
|
+
if is_selected:
|
|
104
|
+
bg_style += "border: 2px solid var(--sage-mint); box-shadow: 0 0 10px rgba(88,129,87,0.2);"
|
|
105
|
+
|
|
106
|
+
st.markdown(f"""
|
|
107
|
+
<div class="schema-node" style="{bg_style}">
|
|
108
|
+
<div class="node-header">
|
|
109
|
+
<span class="node-title">{table.name}</span>
|
|
110
|
+
<span class="node-badge" style="background:{badge_color}; color:white;">{badge_text}</span>
|
|
111
|
+
</div>
|
|
112
|
+
""", unsafe_allow_html=True)
|
|
113
|
+
|
|
114
|
+
# Header Actions: Edit Button
|
|
115
|
+
c_row, c_edit = st.columns([2, 1])
|
|
116
|
+
with c_row:
|
|
117
|
+
if not table.is_reference:
|
|
118
|
+
st.caption(f"{table.row_count:,} rows")
|
|
119
|
+
else:
|
|
120
|
+
st.caption("Ref Table")
|
|
121
|
+
with c_edit:
|
|
122
|
+
if st.button("Edit ✎", key=f"edit_{table.name}"):
|
|
123
|
+
StudioStore.set("selected_table", table.name)
|
|
124
|
+
st.rerun()
|
|
125
|
+
|
|
126
|
+
# Columns preview
|
|
127
|
+
if table.name in schema_config.columns:
|
|
128
|
+
col_html = ""
|
|
129
|
+
for col_def in schema_config.columns[table.name][:5]:
|
|
130
|
+
col_html += f"""
|
|
131
|
+
<div style="display:flex; justify-content:space-between; margin-bottom:4px; font-size:0.8rem;">
|
|
132
|
+
<span>{col_def.name}</span><span style="color:var(--text-tertiary);">{col_def.type}</span>
|
|
133
|
+
</div>
|
|
134
|
+
"""
|
|
135
|
+
if len(schema_config.columns[table.name]) > 5:
|
|
136
|
+
col_html += f"<div style='font-size:0.7rem; color:var(--text-tertiary);'>+ {len(schema_config.columns[table.name])-5} more...</div>"
|
|
137
|
+
|
|
138
|
+
st.markdown(f"""
|
|
139
|
+
<div style="margin-top:0.5rem; padding:0.5rem; background:rgba(255,255,255,0.5); border-radius:4px;">
|
|
140
|
+
{col_html}
|
|
141
|
+
</div>
|
|
142
|
+
""", unsafe_allow_html=True)
|
|
143
|
+
|
|
144
|
+
st.markdown("</div>", unsafe_allow_html=True)
|
|
145
|
+
|
|
146
|
+
# 2. LEGACY TEMPLATE (Service Company)
|
|
147
|
+
elif schema_source == "Template":
|
|
148
|
+
warehouse_schema = StudioStore.get('warehouse_schema')
|
|
149
|
+
col_t1, col_t2, col_t3 = st.columns(3)
|
|
150
|
+
|
|
151
|
+
with col_t1:
|
|
152
|
+
st.markdown("""
|
|
153
|
+
<div class="schema-node">
|
|
154
|
+
<div class="node-header">
|
|
155
|
+
<span class="node-title">customers</span>
|
|
156
|
+
<span class="node-badge">Dimension</span>
|
|
157
|
+
</div>
|
|
158
|
+
""", unsafe_allow_html=True)
|
|
159
|
+
|
|
160
|
+
customer_count = st.number_input("Rows", 100, 100000,
|
|
161
|
+
warehouse_schema.get('customer_count', 500),
|
|
162
|
+
step=100, key="n_cust")
|
|
163
|
+
st.markdown("<div style='font-size:0.8rem; color:var(--text-secondary); margin-top:0.5rem;'>Attributes: id, name, email, tier, created_at</div>", unsafe_allow_html=True)
|
|
164
|
+
st.markdown("</div>", unsafe_allow_html=True)
|
|
165
|
+
|
|
166
|
+
with col_t2:
|
|
167
|
+
st.markdown("""
|
|
168
|
+
<div class="schema-node">
|
|
169
|
+
<div class="node-header">
|
|
170
|
+
<span class="node-title">projects</span>
|
|
171
|
+
<span class="node-badge">Dimension</span>
|
|
172
|
+
</div>
|
|
173
|
+
""", unsafe_allow_html=True)
|
|
174
|
+
|
|
175
|
+
project_count = st.number_input("Rows", 500, 500000,
|
|
176
|
+
warehouse_schema.get('project_count', 2000),
|
|
177
|
+
step=500, key="n_proj")
|
|
178
|
+
st.markdown("<div style='font-size:0.8rem; color:var(--text-secondary); margin-top:0.5rem;'>Attributes: id, customer_id, name, status</div>", unsafe_allow_html=True)
|
|
179
|
+
st.markdown("</div>", unsafe_allow_html=True)
|
|
180
|
+
|
|
181
|
+
with col_t3:
|
|
182
|
+
st.markdown("""
|
|
183
|
+
<div class="schema-node" style="border-left-color: var(--sage-hunter); background: rgba(88, 129, 87, 0.05);">
|
|
184
|
+
<div class="node-header">
|
|
185
|
+
<span class="node-title">invoices</span>
|
|
186
|
+
<span class="node-badge" style="background:var(--sage-hunter); color:white;">Fact Table</span>
|
|
187
|
+
</div>
|
|
188
|
+
""", unsafe_allow_html=True)
|
|
189
|
+
|
|
190
|
+
st.markdown("""
|
|
191
|
+
<div style="font-size:0.85rem; font-family:'JetBrains Mono'; margin-bottom:0.5rem;">
|
|
192
|
+
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:4px; background:rgba(255,255,255,0.5); padding:2px 4px; border-radius:4px;">
|
|
193
|
+
<span>amount</span>
|
|
194
|
+
<div style="display:flex; align-items:center; gap:4px;">
|
|
195
|
+
<span style="color:var(--text-tertiary); font-size:0.8rem;">float</span>
|
|
196
|
+
<span style="font-size:0.8rem; color:var(--sage-hunter);" title="Has Outcome Constraint">◭</span>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
""", unsafe_allow_html=True)
|
|
201
|
+
|
|
202
|
+
if st.button("Configure Constraint ◭", key="btn_const_inv", use_container_width=True):
|
|
203
|
+
StudioStore.set('active_tab', "Outcome Curve")
|
|
204
|
+
StudioStore.set('selected_constraint', "invoices.amount")
|
|
205
|
+
st.rerun()
|
|
206
|
+
|
|
207
|
+
st.markdown("</div>", unsafe_allow_html=True)
|
|
208
|
+
|
|
209
|
+
# Update state
|
|
210
|
+
warehouse_schema['customer_count'] = customer_count
|
|
211
|
+
warehouse_schema['project_count'] = project_count
|
|
212
|
+
|
|
213
|
+
# NEXT BUTTON
|
|
214
|
+
st.markdown("<br>", unsafe_allow_html=True)
|
|
215
|
+
if st.button("Next: Define Outcome Curve →", type="primary", use_container_width=True):
|
|
216
|
+
StudioStore.set('active_tab', "Outcome Curve")
|
|
217
|
+
st.rerun()
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import streamlit as st
|
|
2
|
+
|
|
3
|
+
def apply_custom_styles():
|
|
4
|
+
"""Apply the premium 'Botanical Sage' theme and architectural overrides."""
|
|
5
|
+
st.markdown("""
|
|
6
|
+
<style>
|
|
7
|
+
/* ============ VARIABLES ============ */
|
|
8
|
+
:root {
|
|
9
|
+
--sage-bg: #F4F7F5;
|
|
10
|
+
--sage-dark: #2A3B30;
|
|
11
|
+
--sage-hunter: #344E41;
|
|
12
|
+
--sage-fern: #588157;
|
|
13
|
+
--sage-mint: #A3B18A;
|
|
14
|
+
--sage-pale: #DAD7CD;
|
|
15
|
+
|
|
16
|
+
--text-primary: #1A1C1A;
|
|
17
|
+
--text-secondary: #4A524A;
|
|
18
|
+
--text-tertiary: #849685;
|
|
19
|
+
|
|
20
|
+
--card-bg: #FFFFFF;
|
|
21
|
+
--border-default: #E2E8E2;
|
|
22
|
+
--shadow-sm: 0 1px 2px rgba(42, 59, 48, 0.05);
|
|
23
|
+
--shadow-md: 0 4px 6px -1px rgba(42, 59, 48, 0.08);
|
|
24
|
+
--font-display: 'Instrument Serif', serif;
|
|
25
|
+
--font-body: 'Inter', sans-serif;
|
|
26
|
+
--font-mono: 'JetBrains Mono', monospace;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* ============ GLOBAL RESET ============ */
|
|
30
|
+
.stApp {
|
|
31
|
+
background-color: var(--sage-bg);
|
|
32
|
+
font-family: var(--font-body);
|
|
33
|
+
color: var(--text-primary);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* Override Sidebar */
|
|
37
|
+
[data-testid="stSidebar"] {
|
|
38
|
+
background-color: var(--sage-hunter);
|
|
39
|
+
border-right: 1px solid var(--sage-dark);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* Force White Text in Sidebar */
|
|
43
|
+
section[data-testid="stSidebar"] p,
|
|
44
|
+
section[data-testid="stSidebar"] span,
|
|
45
|
+
section[data-testid="stSidebar"] div,
|
|
46
|
+
section[data-testid="stSidebar"] label {
|
|
47
|
+
color: #FFFFFF !important;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* Restore header but transparent */
|
|
51
|
+
header {
|
|
52
|
+
visibility: visible !important;
|
|
53
|
+
background: transparent !important;
|
|
54
|
+
}
|
|
55
|
+
.header-decoration { visibility: hidden; }
|
|
56
|
+
|
|
57
|
+
/* ===== SIDEBAR TOGGLE (Locked Open) ===== */
|
|
58
|
+
[data-testid="collapsedControl"], [data-testid="stSidebarCollapsedControl"] {
|
|
59
|
+
display: none !important;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* ============ TYPOGRAPHY ============ */
|
|
63
|
+
@import url('https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Inter:wght@300;400;500;600&family=JetBrains+Mono:wght@400;500&family=Pinyon+Script&display=swap');
|
|
64
|
+
|
|
65
|
+
h1, h2, h3 { font-family: var(--font-body); letter-spacing: -0.02em; }
|
|
66
|
+
h3 {
|
|
67
|
+
font-family: 'Cormorant Garamond', serif !important;
|
|
68
|
+
color: var(--text-primary) !important;
|
|
69
|
+
font-weight: 500 !important;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* ============ COMPONENT: LOGO ============ */
|
|
73
|
+
.brand-logo {
|
|
74
|
+
font-family: 'Pinyon Script', cursive;
|
|
75
|
+
font-size: 2.8rem;
|
|
76
|
+
color: #FFFFFF !important;
|
|
77
|
+
text-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
78
|
+
margin-bottom: 0.5rem;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* ============ COMPONENT: SCHEMA NODE ============ */
|
|
82
|
+
.schema-node {
|
|
83
|
+
background: white;
|
|
84
|
+
border: 1px solid var(--border-default);
|
|
85
|
+
border-radius: 8px;
|
|
86
|
+
padding: 1rem;
|
|
87
|
+
box-shadow: var(--shadow-sm);
|
|
88
|
+
margin-bottom: 1rem;
|
|
89
|
+
transition: all 0.2s ease;
|
|
90
|
+
border-left: 3px solid var(--sage-mint);
|
|
91
|
+
}
|
|
92
|
+
.schema-node:hover {
|
|
93
|
+
transform: translateY(-2px);
|
|
94
|
+
box-shadow: var(--shadow-md);
|
|
95
|
+
border-color: var(--sage-mint);
|
|
96
|
+
}
|
|
97
|
+
.node-header {
|
|
98
|
+
display: flex;
|
|
99
|
+
justify-content: space-between;
|
|
100
|
+
align-items: center;
|
|
101
|
+
margin-bottom: 0.75rem;
|
|
102
|
+
padding-bottom: 0.5rem;
|
|
103
|
+
border-bottom: 1px solid var(--border-default);
|
|
104
|
+
}
|
|
105
|
+
.node-title {
|
|
106
|
+
font-family: var(--font-mono);
|
|
107
|
+
color: var(--sage-hunter);
|
|
108
|
+
font-weight: 600;
|
|
109
|
+
font-size: 0.95rem;
|
|
110
|
+
background: rgba(52, 78, 65, 0.05);
|
|
111
|
+
padding: 2px 6px;
|
|
112
|
+
border-radius: 4px;
|
|
113
|
+
}
|
|
114
|
+
.node-badge {
|
|
115
|
+
font-size: 0.7rem;
|
|
116
|
+
text-transform: uppercase;
|
|
117
|
+
letter-spacing: 0.05em;
|
|
118
|
+
color: var(--sage-fern);
|
|
119
|
+
background: rgba(88, 129, 87, 0.1);
|
|
120
|
+
padding: 2px 6px;
|
|
121
|
+
border-radius: 99px;
|
|
122
|
+
font-weight: 600;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* ============ COMPONENT: BUTTONS ============ */
|
|
126
|
+
.stButton button {
|
|
127
|
+
border-radius: 6px;
|
|
128
|
+
font-weight: 500;
|
|
129
|
+
transition: all 0.2s;
|
|
130
|
+
}
|
|
131
|
+
/* Primary Button override */
|
|
132
|
+
.stButton button[kind="primary"] {
|
|
133
|
+
background: linear-gradient(135deg, var(--sage-hunter) 0%, #2A3B30 100%);
|
|
134
|
+
border: none;
|
|
135
|
+
box-shadow: 0 2px 8px rgba(52, 78, 65, 0.2);
|
|
136
|
+
}
|
|
137
|
+
.stButton button[kind="primary"]:hover {
|
|
138
|
+
transform: translateY(-1px);
|
|
139
|
+
box-shadow: 0 4px 12px rgba(52, 78, 65, 0.3);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
</style>
|
|
143
|
+
""", unsafe_allow_html=True)
|