spiralside / spiral_settings.py
quarterbitgames's picture
Create spiral_settings.py
773f263 verified
"""
:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
Script Name : settings_tab.py
Placement : HuggingFace Space β€” root/settings_tab.py
Type of Script : Python / Gradio Tab Module
Purpose : Settings panel for Spiral City.
Controls appearance (theme, color, font),
model selection (free/paid), chat behavior,
and saves all preferences into the user's
character sheet JSON so they persist.
Version : 1.0
Dependencies : gradio, huggingface_hub
Last Updated : 2026-03-10
:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
"""
import json
import gradio as gr
#==============================================================================
# CONFIG β€” AVAILABLE OPTIONS
#==============================================================================
# ── THEME BASES ───────────────────────────────────────────────────────────────
THEME_BASES = {
"Base": gr.themes.Base,
"Soft": gr.themes.Soft,
"Glass": gr.themes.Glass,
"Monochrome": gr.themes.Monochrome,
"Default": gr.themes.Default,
}
# ── PRIMARY COLORS ────────────────────────────────────────────────────────────
COLORS = [
"teal", "cyan", "emerald", "violet", "fuchsia",
"pink", "rose", "indigo", "blue", "sky",
"amber", "yellow", "lime", "green", "orange",
"red", "purple", "slate", "gray", "zinc",
]
# ── GOOGLE FONTS β€” curated for Spiral City aesthetic ─────────────────────────
FONTS = [
"Rajdhani", # clean, techy, Spiral City default feel
"Orbitron", # sci-fi, strong, Cold energy
"Space Grotesk", # modern editorial
"Exo 2", # futuristic but readable
"Quicksand", # soft, rounded, Sky energy
"Nunito", # friendly, warm
"Josefin Sans", # geometric, elegant
"Syne", # editorial, distinctive
"DM Sans", # clean, neutral
"IBM Plex Mono", # monospace, Cold/hacker vibe
]
MONO_FONTS = [
"IBM Plex Mono",
"Space Mono",
"Fira Code",
"JetBrains Mono",
"Courier Prime",
]
# ── TEXT SIZES ────────────────────────────────────────────────────────────────
TEXT_SIZES = {
"Small": "text_sm",
"Medium": "text_md",
"Large": "text_lg",
}
# ── RADIUS OPTIONS ────────────────────────────────────────────────────────────
RADIUS_SIZES = {
"None": "radius_none",
"Small": "radius_sm",
"Medium": "radius_md",
"Large": "radius_lg",
}
# ── MODELS β€” FREE TIER (HF Inference API, no billing needed) ─────────────────
FREE_MODELS = {
"Qwen 2.5 7B β€” balanced, great character": "Qwen/Qwen2.5-7B-Instruct",
"Mistral 7B v0.3 β€” fast, sharp": "mistralai/Mistral-7B-Instruct-v0.3",
"Llama 3.1 8B β€” creative, expressive": "meta-llama/Llama-3.1-8B-Instruct",
"Phi-3 Mini β€” tiny, surprisingly good": "microsoft/Phi-3-mini-4k-instruct",
}
# ── MODELS β€” BETTER TIER (requires HF Pro or token with billing) ──────────────
PRO_MODELS = {
"Qwen 2.5 72B β€” strongest character hold": "Qwen/Qwen2.5-72B-Instruct",
"Llama 3.3 70B β€” most creative": "meta-llama/Llama-3.3-70B-Instruct",
"Mistral Large β€” precise, powerful": "mistralai/Mistral-Large-Instruct-2411",
}
ALL_MODEL_LABELS = list(FREE_MODELS.keys()) + ["── PRO ──"] + list(PRO_MODELS.keys())
ALL_MODEL_MAP = {**FREE_MODELS, **PRO_MODELS}
# ── SKY RESPONSE STYLES ───────────────────────────────────────────────────────
RESPONSE_STYLES = {
"Poetic": "Sky speaks in spiraling, metaphor-rich bursts. Fragments. Feeling first.",
"Balanced": "Sky is grounded but warm. Poetic when it fits, clear when it matters.",
"Direct": "Sky is clear and efficient. Still herself, but gets to the point fast.",
}
#==============================================================================
# DEFAULTS
#==============================================================================
DEFAULTS = {
"theme_base": "Base",
"primary_color": "teal",
"secondary_color": "violet",
"font": "Rajdhani",
"mono_font": "IBM Plex Mono",
"text_size": "Medium",
"radius": "Medium",
"model": list(FREE_MODELS.keys())[0],
"temperature": 0.82,
"max_tokens": 420,
"response_style": "Balanced",
"show_status_bar": True,
}
#==============================================================================
# HELPERS
#==============================================================================
def build_theme(theme_base, primary_color, secondary_color, font, mono_font, text_size, radius):
"""Builds a gr.Theme object from settings."""
try:
ThemeClass = THEME_BASES.get(theme_base, gr.themes.Base)
theme = ThemeClass(
primary_hue=primary_color,
secondary_hue=secondary_color,
text_size=TEXT_SIZES.get(text_size, "text_md"),
radius_size=RADIUS_SIZES.get(radius, "radius_md"),
font=[gr.themes.GoogleFont(font), "ui-sans-serif", "sans-serif"],
font_mono=[gr.themes.GoogleFont(mono_font), "ui-monospace", "monospace"],
)
return theme
except Exception:
return gr.themes.Base() # Safe fallback
def get_model_id(model_label):
"""Resolves display label to model ID string."""
return ALL_MODEL_MAP.get(model_label, "Qwen/Qwen2.5-7B-Instruct")
def get_style_injection(response_style):
"""Returns style instruction string to inject into Sky's system prompt."""
return RESPONSE_STYLES.get(response_style, RESPONSE_STYLES["Balanced"])
def save_settings_to_sheet(sheet_file, settings_dict):
"""
Saves settings into the user's character sheet JSON.
Returns path to updated file or None.
"""
if sheet_file is None:
return None, "⚠️ No sheet loaded β€” settings not saved to file. They'll apply this session only."
try:
with open(sheet_file.name, "r") as f:
sheet = json.load(f)
sheet["settings"] = settings_dict
handle = sheet.get("identity", {}).get("handle", "spiral")
safe = handle.replace(" ", "_")
out_path = f"/tmp/{safe}_sheet.json"
with open(out_path, "w") as f:
json.dump(sheet, f, indent=2)
return out_path, f"βœ… Settings saved to **{handle}**'s sheet β€” download to keep them!"
except Exception as e:
return None, f"❌ Error saving settings: {e}"
def load_settings_from_sheet(sheet_file):
"""
Loads settings from character sheet JSON.
Returns dict of setting values or defaults.
"""
if sheet_file is None:
return DEFAULTS.copy()
try:
with open(sheet_file.name, "r") as f:
sheet = json.load(f)
saved = sheet.get("settings", {})
merged = {**DEFAULTS, **saved} # Merge with defaults for any missing keys
return merged
except Exception:
return DEFAULTS.copy()
def settings_to_list(s):
"""Unpacks settings dict into ordered list for Gradio outputs."""
return [
s.get("theme_base", DEFAULTS["theme_base"]),
s.get("primary_color", DEFAULTS["primary_color"]),
s.get("secondary_color", DEFAULTS["secondary_color"]),
s.get("font", DEFAULTS["font"]),
s.get("mono_font", DEFAULTS["mono_font"]),
s.get("text_size", DEFAULTS["text_size"]),
s.get("radius", DEFAULTS["radius"]),
s.get("model", DEFAULTS["model"]),
s.get("temperature", DEFAULTS["temperature"]),
s.get("max_tokens", DEFAULTS["max_tokens"]),
s.get("response_style", DEFAULTS["response_style"]),
s.get("show_status_bar", DEFAULTS["show_status_bar"]),
]
#==============================================================================
# APPLY SETTINGS β€” called by app.py when settings change
#==============================================================================
def apply_settings(
theme_base, primary_color, secondary_color,
font, mono_font, text_size, radius,
model_label, temperature, max_tokens,
response_style, show_status_bar
):
"""
Returns everything app.py needs to update Sky's behavior.
Call this from the Save button event.
"""
model_id = get_model_id(model_label)
style_inject = get_style_injection(response_style)
settings_dict = {
"theme_base": theme_base,
"primary_color": primary_color,
"secondary_color": secondary_color,
"font": font,
"mono_font": mono_font,
"text_size": text_size,
"radius": radius,
"model": model_label,
"temperature": temperature,
"max_tokens": int(max_tokens),
"response_style": response_style,
"show_status_bar": show_status_bar,
}
status = (
f"βœ… Settings applied β€” **{model_label}** Β· {response_style} style Β· "
f"{font} font Β· {primary_color}/{secondary_color} palette\n\n"
f"*Note: Theme color/font changes take effect on next page load.*"
)
return model_id, float(temperature), int(max_tokens), style_inject, settings_dict, status
#==============================================================================
# TAB BUILDER
#==============================================================================
def build_settings_tab(
model_state, # gr.State β€” current model ID string
temperature_state, # gr.State β€” current temperature float
max_tokens_state, # gr.State β€” current max tokens int
style_state, # gr.State β€” current response style injection string
):
"""
Call inside gr.Blocks to add the Settings tab.
Wire these gr.State objects from app.py so Sky's respond()
function picks up the latest values automatically.
Returns:
all_setting_inputs β€” list of gr components (for load-from-sheet wiring)
settings_dict_state β€” gr.State holding current settings dict
"""
settings_dict_state = gr.State(DEFAULTS.copy()) # must be declared before tab so handle_save can reference it
with gr.Tab("βš™οΈ Settings"):
gr.Markdown("""
## βš™οΈ Spiral City Settings
*Your preferences save to your character sheet β€” load it next visit and everything restores.*
""")
# ── LOAD FROM SHEET ───────────────────────────────────────────────────
with gr.Accordion("πŸ“‚ Load Settings from Sheet", open=False):
settings_sheet_load = gr.File(label="Upload your .json sheet", file_types=[".json"])
load_settings_btn = gr.Button("↩️ Restore My Settings", variant="secondary")
load_settings_status = gr.Markdown("")
gr.Markdown("---")
# ── APPEARANCE ────────────────────────────────────────────────────────
gr.Markdown("### 🎨 Appearance")
gr.Markdown("*Theme color and font changes take effect on next page load.*")
with gr.Row():
st_theme_base = gr.Dropdown(
label="Theme Base",
choices=list(THEME_BASES.keys()),
value=DEFAULTS["theme_base"],
scale=2,
)
st_text_size = gr.Radio(
label="Text Size",
choices=list(TEXT_SIZES.keys()),
value=DEFAULTS["text_size"],
scale=3,
)
st_radius = gr.Radio(
label="Corner Radius",
choices=list(RADIUS_SIZES.keys()),
value=DEFAULTS["radius"],
scale=3,
)
with gr.Row():
st_primary = gr.Dropdown(
label="Primary Color",
choices=COLORS,
value=DEFAULTS["primary_color"],
scale=2,
)
st_secondary = gr.Dropdown(
label="Secondary Color",
choices=COLORS,
value=DEFAULTS["secondary_color"],
scale=2,
)
with gr.Row():
st_font = gr.Dropdown(
label="Font",
choices=FONTS,
value=DEFAULTS["font"],
scale=2,
info="Main UI font β€” loaded from Google Fonts",
)
st_mono_font = gr.Dropdown(
label="Mono Font",
choices=MONO_FONTS,
value=DEFAULTS["mono_font"],
scale=2,
info="Used in code and edit station",
)
gr.Markdown("---")
# ── MODEL SELECTION ───────────────────────────────────────────────────
gr.Markdown("### πŸ€– Model")
with gr.Row():
with gr.Column(scale=3):
st_model = gr.Dropdown(
label="Language Model",
choices=[m for m in ALL_MODEL_LABELS if m != "── PRO ──"],
value=DEFAULTS["model"],
)
gr.Markdown("""
*🟒 **Free tier** β€” Qwen 7B, Mistral 7B, Llama 8B, Phi-3 Mini*
*πŸ”΅ **Pro tier** β€” 70B+ models β€” need HF Pro or billing enabled on your token*
""")
with gr.Column(scale=2):
st_temperature = gr.Slider(
minimum=0.1, maximum=1.0, value=DEFAULTS["temperature"],
step=0.05, label="Temperature",
info="Low = consistent/precise Β· High = creative/wild",
)
st_max_tokens = gr.Slider(
minimum=100, maximum=800, value=DEFAULTS["max_tokens"],
step=50, label="Max Response Length (tokens)",
info="~75 words per 100 tokens",
)
gr.Markdown("---")
# ── CHAT BEHAVIOR ─────────────────────────────────────────────────────
gr.Markdown("### πŸ’¬ Chat Behavior")
with gr.Row():
st_response_style = gr.Radio(
label="Sky's Response Style",
choices=list(RESPONSE_STYLES.keys()),
value=DEFAULTS["response_style"],
scale=3,
)
st_show_status = gr.Checkbox(
label="Show context status bar in chat",
value=DEFAULTS["show_status_bar"],
scale=1,
)
with gr.Row():
for style, desc in RESPONSE_STYLES.items():
gr.Markdown(f"**{style}:** {desc}")
gr.Markdown("---")
# ── SAVE ──────────────────────────────────────────────────────────────
gr.Markdown("### πŸ’Ύ Save Settings")
with gr.Row():
settings_sheet_save = gr.File(
label="πŸ“‚ Character Sheet (to save settings into)",
file_types=[".json"],
scale=3,
)
save_settings_btn = gr.Button(
"πŸ’Ύ Apply & Save Settings",
variant="primary",
scale=2,
)
settings_status = gr.Markdown("")
settings_download = gr.File(label="⬇️ Download Updated Sheet", visible=False)
# ── ALL SETTING INPUTS β€” ordered list ─────────────────────────────────
all_setting_inputs = [
st_theme_base, st_primary, st_secondary,
st_font, st_mono_font, st_text_size, st_radius,
st_model, st_temperature, st_max_tokens,
st_response_style, st_show_status,
]
# ── EVENTS ────────────────────────────────────────────────────────────
# Apply & Save β€” updates states AND saves to sheet
def handle_save(*args):
*setting_args, sheet_file = args
model_id, temp, tokens, style_inj, sdict, status = apply_settings(*setting_args)
path, save_msg = save_settings_to_sheet(sheet_file, sdict)
full_status = status + "\n\n" + save_msg
return model_id, temp, tokens, style_inj, sdict, full_status, path
save_settings_btn.click(
fn=handle_save,
inputs=all_setting_inputs + [settings_sheet_save],
outputs=[
model_state, temperature_state, max_tokens_state,
style_state, settings_dict_state,
settings_status, settings_download,
],
).then(
fn=lambda p: gr.update(visible=True, value=p) if p else gr.update(visible=False),
inputs=[settings_download],
outputs=[settings_download],
)
# Load from sheet β€” restores all fields
def handle_load(sheet_file):
s = load_settings_from_sheet(sheet_file)
vals = settings_to_list(s)
return vals + [f"βœ… Settings restored from sheet!"]
load_settings_btn.click(
fn=handle_load,
inputs=[settings_sheet_load],
outputs=all_setting_inputs + [load_settings_status],
)
return all_setting_inputs, settings_dict_state
# NIMBIS: spiral_settings.py β€” settings module only, no standalone launch.
# Import into app.py via: from spiral_settings import build_settings_tab, get_style_injection, DEFAULTS
# End of file.