quarterbitgames commited on
Commit
773f263
Β·
verified Β·
1 Parent(s): 0a49693

Create spiral_settings.py

Browse files
Files changed (1) hide show
  1. spiral_settings.py +456 -0
spiral_settings.py ADDED
@@ -0,0 +1,456 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
3
+ Script Name : settings_tab.py
4
+ Placement : HuggingFace Space β€” root/settings_tab.py
5
+ Type of Script : Python / Gradio Tab Module
6
+ Purpose : Settings panel for Spiral City.
7
+ Controls appearance (theme, color, font),
8
+ model selection (free/paid), chat behavior,
9
+ and saves all preferences into the user's
10
+ character sheet JSON so they persist.
11
+ Version : 1.0
12
+ Dependencies : gradio, huggingface_hub
13
+ Last Updated : 2026-03-10
14
+ :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
15
+ """
16
+
17
+ import json
18
+ import gradio as gr
19
+
20
+ #==============================================================================
21
+ # CONFIG β€” AVAILABLE OPTIONS
22
+ #==============================================================================
23
+
24
+ # ── THEME BASES ───────────────────────────────────────────────────────────────
25
+ THEME_BASES = {
26
+ "Base": gr.themes.Base,
27
+ "Soft": gr.themes.Soft,
28
+ "Glass": gr.themes.Glass,
29
+ "Monochrome": gr.themes.Monochrome,
30
+ "Default": gr.themes.Default,
31
+ }
32
+
33
+ # ── PRIMARY COLORS ────────────────────────────────────────────────────────────
34
+ COLORS = [
35
+ "teal", "cyan", "emerald", "violet", "fuchsia",
36
+ "pink", "rose", "indigo", "blue", "sky",
37
+ "amber", "yellow", "lime", "green", "orange",
38
+ "red", "purple", "slate", "gray", "zinc",
39
+ ]
40
+
41
+ # ── GOOGLE FONTS β€” curated for Spiral City aesthetic ─────────────────────────
42
+ FONTS = [
43
+ "Rajdhani", # clean, techy, Spiral City default feel
44
+ "Orbitron", # sci-fi, strong, Cold energy
45
+ "Space Grotesk", # modern editorial
46
+ "Exo 2", # futuristic but readable
47
+ "Quicksand", # soft, rounded, Sky energy
48
+ "Nunito", # friendly, warm
49
+ "Josefin Sans", # geometric, elegant
50
+ "Syne", # editorial, distinctive
51
+ "DM Sans", # clean, neutral
52
+ "IBM Plex Mono", # monospace, Cold/hacker vibe
53
+ ]
54
+
55
+ MONO_FONTS = [
56
+ "IBM Plex Mono",
57
+ "Space Mono",
58
+ "Fira Code",
59
+ "JetBrains Mono",
60
+ "Courier Prime",
61
+ ]
62
+
63
+ # ── TEXT SIZES ────────────────────────────────────────────────────────────────
64
+ TEXT_SIZES = {
65
+ "Small": "text_sm",
66
+ "Medium": "text_md",
67
+ "Large": "text_lg",
68
+ }
69
+
70
+ # ── RADIUS OPTIONS ────────────────────────────────────────────────────────────
71
+ RADIUS_SIZES = {
72
+ "None": "radius_none",
73
+ "Small": "radius_sm",
74
+ "Medium": "radius_md",
75
+ "Large": "radius_lg",
76
+ }
77
+
78
+ # ── MODELS β€” FREE TIER (HF Inference API, no billing needed) ─────────────────
79
+ FREE_MODELS = {
80
+ "Qwen 2.5 7B β€” balanced, great character": "Qwen/Qwen2.5-7B-Instruct",
81
+ "Mistral 7B v0.3 β€” fast, sharp": "mistralai/Mistral-7B-Instruct-v0.3",
82
+ "Llama 3.1 8B β€” creative, expressive": "meta-llama/Llama-3.1-8B-Instruct",
83
+ "Phi-3 Mini β€” tiny, surprisingly good": "microsoft/Phi-3-mini-4k-instruct",
84
+ }
85
+
86
+ # ── MODELS β€” BETTER TIER (requires HF Pro or token with billing) ──────────────
87
+ PRO_MODELS = {
88
+ "Qwen 2.5 72B β€” strongest character hold": "Qwen/Qwen2.5-72B-Instruct",
89
+ "Llama 3.3 70B β€” most creative": "meta-llama/Llama-3.3-70B-Instruct",
90
+ "Mistral Large β€” precise, powerful": "mistralai/Mistral-Large-Instruct-2411",
91
+ }
92
+
93
+ ALL_MODEL_LABELS = list(FREE_MODELS.keys()) + ["── PRO ──"] + list(PRO_MODELS.keys())
94
+ ALL_MODEL_MAP = {**FREE_MODELS, **PRO_MODELS}
95
+
96
+ # ── SKY RESPONSE STYLES ───────────────────────────────────────────────────────
97
+ RESPONSE_STYLES = {
98
+ "Poetic": "Sky speaks in spiraling, metaphor-rich bursts. Fragments. Feeling first.",
99
+ "Balanced": "Sky is grounded but warm. Poetic when it fits, clear when it matters.",
100
+ "Direct": "Sky is clear and efficient. Still herself, but gets to the point fast.",
101
+ }
102
+
103
+ #==============================================================================
104
+ # DEFAULTS
105
+ #==============================================================================
106
+
107
+ DEFAULTS = {
108
+ "theme_base": "Base",
109
+ "primary_color": "teal",
110
+ "secondary_color": "violet",
111
+ "font": "Rajdhani",
112
+ "mono_font": "IBM Plex Mono",
113
+ "text_size": "Medium",
114
+ "radius": "Medium",
115
+ "model": list(FREE_MODELS.keys())[0],
116
+ "temperature": 0.82,
117
+ "max_tokens": 420,
118
+ "response_style": "Balanced",
119
+ "show_status_bar": True,
120
+ }
121
+
122
+ #==============================================================================
123
+ # HELPERS
124
+ #==============================================================================
125
+
126
+ def build_theme(theme_base, primary_color, secondary_color, font, mono_font, text_size, radius):
127
+ """Builds a gr.Theme object from settings."""
128
+ try:
129
+ ThemeClass = THEME_BASES.get(theme_base, gr.themes.Base)
130
+ theme = ThemeClass(
131
+ primary_hue=primary_color,
132
+ secondary_hue=secondary_color,
133
+ text_size=TEXT_SIZES.get(text_size, "text_md"),
134
+ radius_size=RADIUS_SIZES.get(radius, "radius_md"),
135
+ font=[gr.themes.GoogleFont(font), "ui-sans-serif", "sans-serif"],
136
+ font_mono=[gr.themes.GoogleFont(mono_font), "ui-monospace", "monospace"],
137
+ )
138
+ return theme
139
+ except Exception:
140
+ return gr.themes.Base() # Safe fallback
141
+
142
+
143
+ def get_model_id(model_label):
144
+ """Resolves display label to model ID string."""
145
+ return ALL_MODEL_MAP.get(model_label, "Qwen/Qwen2.5-7B-Instruct")
146
+
147
+
148
+ def get_style_injection(response_style):
149
+ """Returns style instruction string to inject into Sky's system prompt."""
150
+ return RESPONSE_STYLES.get(response_style, RESPONSE_STYLES["Balanced"])
151
+
152
+
153
+ def save_settings_to_sheet(sheet_file, settings_dict):
154
+ """
155
+ Saves settings into the user's character sheet JSON.
156
+ Returns path to updated file or None.
157
+ """
158
+ if sheet_file is None:
159
+ return None, "⚠️ No sheet loaded β€” settings not saved to file. They'll apply this session only."
160
+ try:
161
+ with open(sheet_file.name, "r") as f:
162
+ sheet = json.load(f)
163
+ sheet["settings"] = settings_dict
164
+ handle = sheet.get("identity", {}).get("handle", "spiral")
165
+ safe = handle.replace(" ", "_")
166
+ out_path = f"/tmp/{safe}_sheet.json"
167
+ with open(out_path, "w") as f:
168
+ json.dump(sheet, f, indent=2)
169
+ return out_path, f"βœ… Settings saved to **{handle}**'s sheet β€” download to keep them!"
170
+ except Exception as e:
171
+ return None, f"❌ Error saving settings: {e}"
172
+
173
+
174
+ def load_settings_from_sheet(sheet_file):
175
+ """
176
+ Loads settings from character sheet JSON.
177
+ Returns dict of setting values or defaults.
178
+ """
179
+ if sheet_file is None:
180
+ return DEFAULTS.copy()
181
+ try:
182
+ with open(sheet_file.name, "r") as f:
183
+ sheet = json.load(f)
184
+ saved = sheet.get("settings", {})
185
+ merged = {**DEFAULTS, **saved} # Merge with defaults for any missing keys
186
+ return merged
187
+ except Exception:
188
+ return DEFAULTS.copy()
189
+
190
+
191
+ def settings_to_list(s):
192
+ """Unpacks settings dict into ordered list for Gradio outputs."""
193
+ return [
194
+ s.get("theme_base", DEFAULTS["theme_base"]),
195
+ s.get("primary_color", DEFAULTS["primary_color"]),
196
+ s.get("secondary_color", DEFAULTS["secondary_color"]),
197
+ s.get("font", DEFAULTS["font"]),
198
+ s.get("mono_font", DEFAULTS["mono_font"]),
199
+ s.get("text_size", DEFAULTS["text_size"]),
200
+ s.get("radius", DEFAULTS["radius"]),
201
+ s.get("model", DEFAULTS["model"]),
202
+ s.get("temperature", DEFAULTS["temperature"]),
203
+ s.get("max_tokens", DEFAULTS["max_tokens"]),
204
+ s.get("response_style", DEFAULTS["response_style"]),
205
+ s.get("show_status_bar", DEFAULTS["show_status_bar"]),
206
+ ]
207
+
208
+ #==============================================================================
209
+ # APPLY SETTINGS β€” called by app.py when settings change
210
+ #==============================================================================
211
+
212
+ def apply_settings(
213
+ theme_base, primary_color, secondary_color,
214
+ font, mono_font, text_size, radius,
215
+ model_label, temperature, max_tokens,
216
+ response_style, show_status_bar
217
+ ):
218
+ """
219
+ Returns everything app.py needs to update Sky's behavior.
220
+ Call this from the Save button event.
221
+ """
222
+ model_id = get_model_id(model_label)
223
+ style_inject = get_style_injection(response_style)
224
+ settings_dict = {
225
+ "theme_base": theme_base,
226
+ "primary_color": primary_color,
227
+ "secondary_color": secondary_color,
228
+ "font": font,
229
+ "mono_font": mono_font,
230
+ "text_size": text_size,
231
+ "radius": radius,
232
+ "model": model_label,
233
+ "temperature": temperature,
234
+ "max_tokens": int(max_tokens),
235
+ "response_style": response_style,
236
+ "show_status_bar": show_status_bar,
237
+ }
238
+ status = (
239
+ f"βœ… Settings applied β€” **{model_label}** Β· {response_style} style Β· "
240
+ f"{font} font Β· {primary_color}/{secondary_color} palette\n\n"
241
+ f"*Note: Theme color/font changes take effect on next page load.*"
242
+ )
243
+ return model_id, float(temperature), int(max_tokens), style_inject, settings_dict, status
244
+
245
+ #==============================================================================
246
+ # TAB BUILDER
247
+ #==============================================================================
248
+
249
+ def build_settings_tab(
250
+ model_state, # gr.State β€” current model ID string
251
+ temperature_state, # gr.State β€” current temperature float
252
+ max_tokens_state, # gr.State β€” current max tokens int
253
+ style_state, # gr.State β€” current response style injection string
254
+ ):
255
+ """
256
+ Call inside gr.Blocks to add the Settings tab.
257
+
258
+ Wire these gr.State objects from app.py so Sky's respond()
259
+ function picks up the latest values automatically.
260
+
261
+ Returns:
262
+ all_setting_inputs β€” list of gr components (for load-from-sheet wiring)
263
+ settings_dict_state β€” gr.State holding current settings dict
264
+ """
265
+
266
+ settings_dict_state = gr.State(DEFAULTS.copy()) # must be declared before tab so handle_save can reference it
267
+
268
+ with gr.Tab("βš™οΈ Settings"):
269
+
270
+ gr.Markdown("""
271
+ ## βš™οΈ Spiral City Settings
272
+ *Your preferences save to your character sheet β€” load it next visit and everything restores.*
273
+ """)
274
+
275
+ # ── LOAD FROM SHEET ───────────────────────────────────────────────────
276
+ with gr.Accordion("πŸ“‚ Load Settings from Sheet", open=False):
277
+ settings_sheet_load = gr.File(label="Upload your .json sheet", file_types=[".json"])
278
+ load_settings_btn = gr.Button("↩️ Restore My Settings", variant="secondary")
279
+ load_settings_status = gr.Markdown("")
280
+
281
+ gr.Markdown("---")
282
+
283
+ # ── APPEARANCE ────────────────────────────────────────────────────────
284
+ gr.Markdown("### 🎨 Appearance")
285
+ gr.Markdown("*Theme color and font changes take effect on next page load.*")
286
+
287
+ with gr.Row():
288
+ st_theme_base = gr.Dropdown(
289
+ label="Theme Base",
290
+ choices=list(THEME_BASES.keys()),
291
+ value=DEFAULTS["theme_base"],
292
+ scale=2,
293
+ )
294
+ st_text_size = gr.Radio(
295
+ label="Text Size",
296
+ choices=list(TEXT_SIZES.keys()),
297
+ value=DEFAULTS["text_size"],
298
+ scale=3,
299
+ )
300
+ st_radius = gr.Radio(
301
+ label="Corner Radius",
302
+ choices=list(RADIUS_SIZES.keys()),
303
+ value=DEFAULTS["radius"],
304
+ scale=3,
305
+ )
306
+
307
+ with gr.Row():
308
+ st_primary = gr.Dropdown(
309
+ label="Primary Color",
310
+ choices=COLORS,
311
+ value=DEFAULTS["primary_color"],
312
+ scale=2,
313
+ )
314
+ st_secondary = gr.Dropdown(
315
+ label="Secondary Color",
316
+ choices=COLORS,
317
+ value=DEFAULTS["secondary_color"],
318
+ scale=2,
319
+ )
320
+
321
+ with gr.Row():
322
+ st_font = gr.Dropdown(
323
+ label="Font",
324
+ choices=FONTS,
325
+ value=DEFAULTS["font"],
326
+ scale=2,
327
+ info="Main UI font β€” loaded from Google Fonts",
328
+ )
329
+ st_mono_font = gr.Dropdown(
330
+ label="Mono Font",
331
+ choices=MONO_FONTS,
332
+ value=DEFAULTS["mono_font"],
333
+ scale=2,
334
+ info="Used in code and edit station",
335
+ )
336
+
337
+ gr.Markdown("---")
338
+
339
+ # ── MODEL SELECTION ───────────────────────────────────────────────────
340
+ gr.Markdown("### πŸ€– Model")
341
+
342
+ with gr.Row():
343
+ with gr.Column(scale=3):
344
+ st_model = gr.Dropdown(
345
+ label="Language Model",
346
+ choices=[m for m in ALL_MODEL_LABELS if m != "── PRO ──"],
347
+ value=DEFAULTS["model"],
348
+ )
349
+ gr.Markdown("""
350
+ *🟒 **Free tier** β€” Qwen 7B, Mistral 7B, Llama 8B, Phi-3 Mini*
351
+ *πŸ”΅ **Pro tier** β€” 70B+ models β€” need HF Pro or billing enabled on your token*
352
+ """)
353
+ with gr.Column(scale=2):
354
+ st_temperature = gr.Slider(
355
+ minimum=0.1, maximum=1.0, value=DEFAULTS["temperature"],
356
+ step=0.05, label="Temperature",
357
+ info="Low = consistent/precise Β· High = creative/wild",
358
+ )
359
+ st_max_tokens = gr.Slider(
360
+ minimum=100, maximum=800, value=DEFAULTS["max_tokens"],
361
+ step=50, label="Max Response Length (tokens)",
362
+ info="~75 words per 100 tokens",
363
+ )
364
+
365
+ gr.Markdown("---")
366
+
367
+ # ── CHAT BEHAVIOR ─────────────────────────────────────────────────────
368
+ gr.Markdown("### πŸ’¬ Chat Behavior")
369
+
370
+ with gr.Row():
371
+ st_response_style = gr.Radio(
372
+ label="Sky's Response Style",
373
+ choices=list(RESPONSE_STYLES.keys()),
374
+ value=DEFAULTS["response_style"],
375
+ scale=3,
376
+ )
377
+ st_show_status = gr.Checkbox(
378
+ label="Show context status bar in chat",
379
+ value=DEFAULTS["show_status_bar"],
380
+ scale=1,
381
+ )
382
+
383
+ with gr.Row():
384
+ for style, desc in RESPONSE_STYLES.items():
385
+ gr.Markdown(f"**{style}:** {desc}")
386
+
387
+ gr.Markdown("---")
388
+
389
+ # ── SAVE ──────────────────────────────────────────────────────────────
390
+ gr.Markdown("### πŸ’Ύ Save Settings")
391
+
392
+ with gr.Row():
393
+ settings_sheet_save = gr.File(
394
+ label="πŸ“‚ Character Sheet (to save settings into)",
395
+ file_types=[".json"],
396
+ scale=3,
397
+ )
398
+ save_settings_btn = gr.Button(
399
+ "πŸ’Ύ Apply & Save Settings",
400
+ variant="primary",
401
+ scale=2,
402
+ )
403
+
404
+ settings_status = gr.Markdown("")
405
+ settings_download = gr.File(label="⬇️ Download Updated Sheet", visible=False)
406
+
407
+ # ── ALL SETTING INPUTS β€” ordered list ─────────────────────────────────
408
+ all_setting_inputs = [
409
+ st_theme_base, st_primary, st_secondary,
410
+ st_font, st_mono_font, st_text_size, st_radius,
411
+ st_model, st_temperature, st_max_tokens,
412
+ st_response_style, st_show_status,
413
+ ]
414
+
415
+ # ── EVENTS ────────────────────────────────────────────────────────────
416
+
417
+ # Apply & Save β€” updates states AND saves to sheet
418
+ def handle_save(*args):
419
+ *setting_args, sheet_file = args
420
+ model_id, temp, tokens, style_inj, sdict, status = apply_settings(*setting_args)
421
+ path, save_msg = save_settings_to_sheet(sheet_file, sdict)
422
+ full_status = status + "\n\n" + save_msg
423
+ return model_id, temp, tokens, style_inj, sdict, full_status, path
424
+
425
+ save_settings_btn.click(
426
+ fn=handle_save,
427
+ inputs=all_setting_inputs + [settings_sheet_save],
428
+ outputs=[
429
+ model_state, temperature_state, max_tokens_state,
430
+ style_state, settings_dict_state,
431
+ settings_status, settings_download,
432
+ ],
433
+ ).then(
434
+ fn=lambda p: gr.update(visible=True, value=p) if p else gr.update(visible=False),
435
+ inputs=[settings_download],
436
+ outputs=[settings_download],
437
+ )
438
+
439
+ # Load from sheet β€” restores all fields
440
+ def handle_load(sheet_file):
441
+ s = load_settings_from_sheet(sheet_file)
442
+ vals = settings_to_list(s)
443
+ return vals + [f"βœ… Settings restored from sheet!"]
444
+
445
+ load_settings_btn.click(
446
+ fn=handle_load,
447
+ inputs=[settings_sheet_load],
448
+ outputs=all_setting_inputs + [load_settings_status],
449
+ )
450
+
451
+ return all_setting_inputs, settings_dict_state
452
+
453
+
454
+ # NIMBIS: spiral_settings.py β€” settings module only, no standalone launch.
455
+ # Import into app.py via: from spiral_settings import build_settings_tab, get_style_injection, DEFAULTS
456
+ # End of file.