CMacD/AIC_PHASE1_POC
0
1"""2aic_utils.py — shared utilities for AIC Streamlit pages.3"""4from __future__ import annotations5 6import base647import streamlit as st8 9ACCENT = "#4E106F"10 11 12 13def save_as_button(14 label: str,15 data: bytes,16 filename: str,17 mime: str,18 key: str = "",19 height: int = 44,20) -> None:21 """22 Render a purple 'Save As…' button using the File System Access API.23 24 On Chrome / Edge the OS native Save-As dialog opens so the user can25 choose the exact save location and filename. On other browsers (or if26 the API is blocked) it falls back to a regular browser download.27 28 Parameters29 ----------30 label : button text (can include emoji)31 data : raw bytes to save32 filename : suggested filename shown in the dialog33 mime : MIME type string34 key : optional unique suffix for the JS function name35 height : iframe height in pixels — increase if button is clipped36 """37 b64 = base64.b64encode(data).decode()38 ext = ("." + filename.rsplit(".", 1)[-1]) if "." in filename else ""39 fn_name = f"saveAs_{key}" if key else "saveAsFile"40 41 # Escape special chars so they survive JS string literals42 safe_fn = filename.replace("\\", "\\\\").replace("'", "\\'")43 safe_mime = mime.replace("'", "\\'")44 safe_ext = ext.replace("'", "\\'")45 46 html = f"""<!doctype html>47<html>48<head>49<style>50 * {{ margin: 0; padding: 0; box-sizing: border-box; }}51 body {{ background: transparent; display: flex; align-items: center; }}52 .sa-btn {{53 background: {ACCENT};54 color: #fff;55 border: none;56 border-radius: 6px;57 padding: 8px 18px;58 font-size: 14px;59 font-family: "Source Sans Pro", sans-serif;60 font-weight: 500;61 cursor: pointer;62 display: inline-flex;63 align-items: center;64 gap: 6px;65 transition: background 0.15s;66 white-space: nowrap;67 }}68 .sa-btn:hover {{ background: #3a0d54; }}69 .sa-btn:active {{ background: #2d0a42; }}70</style>71</head>72<body>73<button class="sa-btn" onclick="{fn_name}()">{label}</button>74<script>75const _b64_{key} = '{b64}';76const _fn_{key} = '{safe_fn}';77const _mime_{key} = '{safe_mime}';78const _ext_{key} = '{safe_ext}';79 80async function {fn_name}() {{81 const bytes = Uint8Array.from(atob(_b64_{key}), c => c.charCodeAt(0));82 83 if (window.showSaveFilePicker) {{84 try {{85 const fh = await window.showSaveFilePicker({{86 suggestedName: _fn_{key},87 types: [{{88 description: 'File',89 accept: {{ [_mime_{key}]: [_ext_{key}] }}90 }}]91 }});92 const w = await fh.createWritable();93 await w.write(bytes);94 await w.close();95 return;96 }} catch (e) {{97 if (e.name === 'AbortError') return; // user hit Cancel — do nothing98 // any other error: fall through to regular download99 }}100 }}101 102 // Fallback: regular browser download (non-Chrome/Edge or API blocked)103 const blob = new Blob([bytes], {{ type: _mime_{key} }});104 const url = URL.createObjectURL(blob);105 const a = Object.assign(document.createElement('a'), {{106 href: url, download: _fn_{key}107 }});108 document.body.appendChild(a);109 a.click();110 document.body.removeChild(a);111 setTimeout(() => URL.revokeObjectURL(url), 10000);112}}113</script>114</body>115</html>"""116 117 st.components.v1.html(html, height=height)118 