Spaces:
Sleeping
Sleeping
| """ | |
| ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: | |
| 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. |