tiny-army-zerogpu / web /shell /persona.css
polats's picture
Portraits step 3: persona panel Portrait section + per-hero caching
6a861b9
Raw
History Blame Contribute Delete
20.8 kB
/* Persona panel chrome — parchment palette, self-contained (scoped on .persona-view)
* so it matches the app whether the OS is light or dark. Mirrors the spriteScene.css
* approach. */
.persona-view {
--p-ink: #141821; --p-muted: #6d6a5f; --p-paper: #f3ebdc; --p-paper-2: #ece2cc;
--p-card: #fbf6ea; --p-transmit: #d8271a;
--p-sans: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--p-mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
display: flex; height: 100%; width: 100%; box-sizing: border-box;
color: var(--p-ink); font-family: var(--p-sans);
}
.persona-view * { box-sizing: border-box; }
.persona-controls {
width: 280px; flex-shrink: 0; border-right: 2px solid var(--p-ink);
background: var(--p-paper-2); overflow-y: auto; padding: 16px;
display: flex; flex-direction: column; gap: 8px;
}
.persona-title {
margin: 0 0 6px !important; font-family: var(--p-mono) !important; font-size: 11px !important;
font-weight: 500 !important; letter-spacing: .2em; text-transform: uppercase;
color: var(--p-transmit) !important; line-height: 1.4 !important;
}
.persona-label {
font-family: var(--p-mono); font-size: 10px; letter-spacing: .14em; text-transform: uppercase;
color: var(--p-muted); margin-top: 6px;
}
.persona-input {
font-family: var(--p-sans) !important; font-size: 14px !important; color: var(--p-ink) !important;
background: var(--p-card) !important; border: 1.5px solid var(--p-ink) !important;
border-radius: 0 !important; padding: 7px 9px !important; width: 100%;
}
/* ── Class picker — a custom dropdown with an animated idle-pose icon per class.
Mirrors a native <select> (.persona-input chrome) but each row shows the
character's looping idle sprite beside the name. ──────────────────────────── */
.persona-classdrop { position: relative; }
.persona-classdrop-trigger {
display: flex !important; align-items: center; gap: 8px; cursor: pointer; text-align: left;
}
.persona-classdrop-label { flex: 1; min-width: 0; }
.persona-classdrop-chev { color: var(--p-muted); font-size: 11px; transition: transform .12s; }
.persona-classdrop.open .persona-classdrop-chev { transform: rotate(180deg); }
.persona-class-ico {
width: 30px; height: 30px; flex-shrink: 0; display: inline-block; overflow: hidden;
background-repeat: no-repeat; background-position: 0 0; image-rendering: pixelated;
}
.persona-classdrop-menu {
display: none; position: absolute; top: calc(100% + 2px); left: 0; right: 0; z-index: 30;
background: var(--p-card); border: 1.5px solid var(--p-ink);
box-shadow: 3px 3px 0 var(--p-transmit); max-height: 300px; overflow-y: auto;
}
.persona-classdrop.open .persona-classdrop-menu { display: block; }
.persona-classdrop-opt {
display: flex; align-items: center; gap: 8px; width: 100%; cursor: pointer; text-align: left;
font-family: var(--p-sans); font-size: 14px; color: var(--p-ink);
background: transparent; border: 0; border-bottom: 1px solid var(--p-paper-2); padding: 5px 9px;
}
.persona-classdrop-opt:last-child { border-bottom: 0; }
.persona-classdrop-opt:hover { background: var(--p-paper-2); }
.persona-classdrop-opt.sel { background: var(--p-ink); color: var(--p-paper); }
.persona-go {
margin-top: 10px; font-family: var(--p-mono) !important; font-size: 12px !important;
font-weight: 700 !important; letter-spacing: .04em; text-transform: uppercase;
color: var(--p-paper) !important; background: var(--p-ink) !important;
border: 1.5px solid var(--p-ink) !important; border-radius: 0 !important;
padding: 9px 12px !important; cursor: pointer; box-shadow: 2px 2px 0 var(--p-transmit);
}
.persona-go:hover { background: var(--p-transmit) !important; }
.persona-go:disabled { opacity: .55; cursor: default; box-shadow: none; }
.persona-status {
margin-top: 8px; font-family: var(--p-mono); font-size: 10px; letter-spacing: .06em;
color: var(--p-muted); min-height: 14px; line-height: 1.5;
}
.persona-result {
flex: 1; min-width: 0; overflow-y: auto; padding: 28px 32px;
background: var(--p-paper);
}
.persona-name {
font-family: 'Fraunces', Georgia, serif; font-weight: 900; font-size: 34px;
line-height: 1; letter-spacing: -.02em; color: var(--p-ink);
}
.persona-tags { display: flex; flex-wrap: wrap; gap: 6px; margin: 12px 0 16px; }
.persona-tag {
font-family: var(--p-mono); font-size: 10px; letter-spacing: .04em; text-transform: uppercase;
color: var(--p-ink); background: var(--p-card); border: 1.5px solid var(--p-ink);
padding: 3px 8px;
}
.persona-about {
font-size: 17px; line-height: 1.6; max-width: 60ch; color: var(--p-ink);
white-space: pre-wrap;
}
/* ── Section headers — like the sidebar: a short ink line, red heading, with the
action button anchored right after the title (not at the page edge). ───────── */
.persona-sec { display: flex; align-items: center; gap: 10px; margin-top: 22px; }
.persona-sec-title {
display: flex; align-items: center; gap: 8px;
font-family: var(--p-mono); font-size: 10px; font-weight: 500; letter-spacing: .2em;
text-transform: uppercase; color: var(--p-transmit) !important; /* beat Gradio's text var */
}
.persona-sec-title::before { content: ''; height: 2px; width: 18px; background: var(--p-ink); flex-shrink: 0; }
.persona-voice-desc {
font-family: var(--p-mono); font-size: 12px; line-height: 1.5; color: var(--p-muted);
max-width: 60ch; margin-top: 8px; font-style: italic;
}
/* Read-only when the provider isn't Qwen3-TTS (the design text isn't used then). */
.persona-voice-desc.readonly { opacity: .6; cursor: default; }
.persona-voice-desc.readonly:hover, .persona-voice-desc.readonly:focus { background: transparent; box-shadow: none; }
/* Per-hero named-voice picker (Kokoro/Kitten/Web Speech). */
.persona-voice-pick-row { margin-top: 12px; max-width: 320px; }
.persona-voice-pick-row .persona-label { margin-top: 0; }
.persona-voice-pick { margin-top: 4px; }
/* Portrait — an editable appearance prompt + the painted image. */
.persona-appearance {
font-family: var(--p-mono); font-size: 12px; line-height: 1.5; color: var(--p-muted);
max-width: 60ch; margin-top: 8px; font-style: italic;
}
.persona-portrait-wrap { margin-top: 12px; }
.persona-portrait-wrap:not(.has-img) { display: none; }
.persona-portrait-img {
width: 320px; max-width: 100%; aspect-ratio: 1 / 1; object-fit: cover; display: block;
border: 1.5px solid var(--p-ink); box-shadow: 4px 4px 0 var(--p-transmit); background: var(--p-card);
}
.persona-quote {
margin: 8px 0 0; padding: 4px 0 4px 16px; border-left: 3px solid var(--p-transmit);
font-family: 'Fraunces', Georgia, serif; font-size: 21px; font-style: italic;
line-height: 1.35; color: var(--p-ink); max-width: 54ch;
}
.persona-quote:not(:empty)::before { content: '“'; }
.persona-quote:not(:empty)::after { content: '”'; }
/* Simple icon button anchored after a section heading. */
.persona-ico {
position: relative; cursor: pointer; flex-shrink: 0; line-height: 1;
font-size: 12px !important; color: var(--p-ink) !important; background: var(--p-card) !important;
border: 1.5px solid var(--p-ink) !important; border-radius: 0 !important; padding: 3px 9px !important;
}
.persona-ico:hover { background: var(--p-paper-2) !important; }
.persona-ico.busy { cursor: default; }
/* Working (voice/portrait) → hide the glyph and spin a small ring in its place. */
.persona-ico.busy { color: transparent !important; }
.persona-ico.busy::before {
content: ''; position: absolute; inset: 0; margin: auto; width: 11px; height: 11px;
border: 2px solid var(--p-paper-2); border-top-color: var(--p-transmit); border-radius: 50%;
animation: tac-spin .7s linear infinite;
}
@keyframes tac-spin { to { transform: rotate(360deg); } }
/* Badge = "nothing made yet, or the inputs changed — tap to (re)make it." Pulses. */
.persona-ico.badged::after {
content: ''; position: absolute; top: -4px; right: -4px; width: 9px; height: 9px;
background: var(--p-transmit); border: 1.5px solid var(--p-card); border-radius: 50%;
animation: tac-badge-pulse 1.3s ease-out infinite;
}
@keyframes tac-badge-pulse {
0% { box-shadow: 0 0 0 0 rgba(216, 39, 26, .6); }
70% { box-shadow: 0 0 0 7px rgba(216, 39, 26, 0); }
100% { box-shadow: 0 0 0 0 rgba(216, 39, 26, 0); }
}
/* Click-to-edit fields (name / about / quote / voice) — auto-saved on blur. */
.persona-edit { cursor: text; border-radius: 0; outline: none; transition: background .12s; }
.persona-edit:hover { background: color-mix(in srgb, var(--p-card) 60%, transparent); box-shadow: 0 0 0 1px var(--p-paper-2); }
.persona-edit:focus { background: var(--p-card); box-shadow: 0 0 0 1.5px var(--p-transmit); }
.persona-edit:empty::before { content: attr(data-ph); color: var(--p-muted); opacity: .6; font-style: italic; }
/* Empty state — shown until a hero is recruited or picked from the barracks. */
.persona-empty {
font-family: var(--p-mono); font-size: 12px; letter-spacing: .04em; color: var(--p-muted);
padding: 28px 0; max-width: 50ch;
}
/* ── Barracks roster (saved heroes) ────────────────────────────────────────── */
.persona-roster-label { margin-top: 18px; }
.persona-roster { display: flex; flex-direction: column; gap: 4px; margin-top: 4px; max-height: 230px; overflow-y: auto; }
.persona-roster-empty { font-family: var(--p-mono); font-size: 10px; color: var(--p-muted); padding: 4px 0; }
.persona-roster-item { display: flex; align-items: stretch; gap: 4px; }
.persona-roster-name {
flex: 1; text-align: left; cursor: pointer;
font-family: var(--p-sans) !important; font-size: 13px !important; color: var(--p-ink) !important;
background: var(--p-card) !important; border: 1.5px solid var(--p-ink) !important; border-radius: 0 !important;
padding: 7px 9px !important; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.persona-roster-name:hover { background: var(--p-paper-2) !important; }
/* The selected hero is highlighted in the barracks. */
.persona-roster-item.active .persona-roster-name {
background: var(--p-ink) !important; color: var(--p-paper) !important;
border-color: var(--p-ink) !important; box-shadow: inset 4px 0 0 var(--p-transmit); font-weight: 700;
}
.persona-roster-x {
cursor: pointer; flex-shrink: 0; font-size: 11px !important;
color: var(--p-transmit) !important; background: var(--p-card) !important;
border: 1.5px solid var(--p-transmit) !important; border-radius: 0 !important; padding: 0 8px !important;
}
.persona-roster-x:hover { background: var(--p-transmit) !important; }
/* ── Model picker + cache controls ─────────────────────────────────────────── */
.model-bar { display: flex; flex-direction: column; gap: 4px; padding-bottom: 10px; margin-bottom: 6px; border-bottom: 1px dashed var(--p-ink); }
.model-select {
font-family: var(--p-sans) !important; font-size: 13px !important; color: var(--p-ink) !important;
background: var(--p-card) !important; border: 1.5px solid var(--p-ink) !important;
border-radius: 0 !important; padding: 6px 8px !important; width: 100%;
}
.engine-select { border-color: var(--p-transmit) !important; margin-bottom: 2px; }
.model-select option:disabled { color: var(--p-muted); }
.model-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.model-info { font-family: var(--p-mono); font-size: 9px; letter-spacing: .04em; color: var(--p-muted); line-height: 1.4; flex: 1; }
.model-del {
font-family: var(--p-mono) !important; font-size: 9px !important; letter-spacing: .04em; text-transform: uppercase;
color: var(--p-transmit) !important; background: var(--p-card) !important; border: 1.5px solid var(--p-transmit) !important;
border-radius: 0 !important; padding: 3px 6px !important; cursor: pointer; flex-shrink: 0;
}
.model-del:hover { background: var(--p-transmit) !important; color: var(--p-card) !important; }
.model-del:disabled { opacity: .5; cursor: default; }
/* ── Live stats (tok/s) ────────────────────────────────────────────────────── */
.persona-stats {
font-family: var(--p-mono); font-size: 11px; letter-spacing: .04em; color: var(--p-transmit);
min-height: 14px; margin-top: 6px;
}
/* ── "Thinking" raw stream (see progress as tokens arrive) ──────────────────── */
.persona-think-wrap { margin-top: 22px; }
.persona-think-wrap > summary {
cursor: pointer; font-family: var(--p-mono); font-size: 10px; letter-spacing: .12em; text-transform: uppercase;
color: var(--p-muted); list-style: none;
}
.persona-think-wrap > summary::-webkit-details-marker { display: none; }
.persona-think-wrap > summary::before { content: '▸ '; }
.persona-think-wrap[open] > summary::before { content: '▾ '; }
.persona-think {
margin: 8px 0 0; max-height: 240px; overflow-y: auto; white-space: pre-wrap; word-break: break-word;
font-family: var(--p-mono); font-size: 11px; line-height: 1.5; color: var(--p-muted);
background: var(--p-paper-2); border: 1px solid var(--p-ink); padding: 8px 10px;
}
.persona-copy {
margin-top: 8px; font-family: var(--p-mono) !important; font-size: 10px !important; letter-spacing: .04em; text-transform: uppercase;
color: var(--p-transmit) !important; background: var(--p-card) !important; border: 1.5px solid var(--p-transmit) !important;
border-radius: 0 !important; padding: 5px 9px !important; cursor: pointer;
}
.persona-copy:hover { background: var(--p-transmit) !important; color: var(--p-card) !important; }
/* ── TTS / voice controls (war-diary read-aloud) ───────────────────────────── */
.tts-bar { margin-top: 16px; }
.tts-auto-row {
display: flex; align-items: center; gap: 6px; margin-top: 6px;
font-family: var(--p-mono); font-size: 10px; letter-spacing: .04em; text-transform: uppercase; color: var(--p-muted); cursor: pointer;
}
.tts-auto { accent-color: var(--p-transmit); width: 14px; height: 14px; }
/* secondary (outline) button for "Read aloud" */
.persona-go-alt {
margin-top: 8px; color: var(--p-ink) !important; background: var(--p-card) !important;
box-shadow: 2px 2px 0 var(--p-ink);
}
.persona-go-alt:hover { background: var(--p-paper-2) !important; color: var(--p-ink) !important; }
.tts-status { min-height: 14px; }
/* ── "Local AI Model" section injected into Gradio's own Settings page ──────── */
/* The model bar's styles use --p-* vars (normally scoped to .persona-view); define
them here too so the picker renders correctly inside Gradio's settings modal. */
.tac-set-section {
--p-ink: #141821; --p-muted: #6d6a5f; --p-paper: #f3ebdc; --p-paper-2: #ece2cc;
--p-card: #fbf6ea; --p-transmit: #d8271a;
--p-sans: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--p-mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
}
.tac-set-section * { box-sizing: border-box; }
.tac-set-section .model-bar { border-bottom: 0; padding-bottom: 0; }
/* Gradio's Settings header prints the app URL under the "Settings" title — hide it. */
.banner-wrap .title .url { display: none !important; }
.tac-set-intro { font-size: 14px; line-height: 1.5; opacity: .75; margin: 2px 0 14px; }
/* Recommended settings — quality preset picker (High / Medium / Low / Custom). */
.tac-quality { display: flex; flex-direction: column; gap: 12px; }
.tac-preset-row { display: flex; gap: 8px; flex-wrap: wrap; }
.tac-preset-btn {
flex: 1 1 120px; display: flex; flex-direction: column; gap: 4px; text-align: left;
background: var(--p-card) !important; border: 1.5px solid var(--p-ink) !important; border-radius: 0 !important;
padding: 14px 16px !important; cursor: pointer; color: var(--p-ink) !important; transition: background .12s, color .12s;
height: auto !important; min-height: 0 !important;
}
.tac-preset-btn:hover:not(:disabled) { background: var(--p-paper-2) !important; }
.tac-preset-btn.active { background: var(--p-ink) !important; color: var(--p-paper) !important; box-shadow: 2px 2px 0 var(--p-transmit); }
/* A selected button turns light on hover — keep its text dark so it stays readable. */
.tac-preset-btn.active:hover:not(:disabled) { color: var(--p-ink) !important; }
.tac-preset-btn:disabled { cursor: default; }
.tac-preset-name { font-family: var(--p-mono); font-size: 12px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; }
.tac-preset-sub { font-family: var(--p-mono); font-size: 10px; opacity: .7; }
/* Expandable device readout (debug / benchmark). */
.tac-device { border: 1px dashed var(--p-ink); padding: 8px 10px; }
.tac-device > summary {
cursor: pointer; font-family: var(--p-mono); font-size: 11px; letter-spacing: .08em;
text-transform: uppercase; color: var(--p-transmit);
}
.tac-device-actions { display: flex; flex-direction: column; gap: 6px; margin: 10px 0 4px; }
.tac-device-rec { align-self: flex-start; margin-top: 0 !important; }
.tac-device-rec-note { min-height: 0; }
.tac-device-body { margin-top: 8px; display: flex; flex-direction: column; gap: 4px; }
.tac-device-row { display: flex; gap: 10px; font-family: var(--p-mono); font-size: 11px; line-height: 1.4; }
.tac-device-k { flex: 0 0 130px; color: var(--p-muted); }
.tac-device-v { flex: 1; min-width: 0; color: var(--p-ink); word-break: break-word; }
/* Persona-prompt editor (Settings → Persona Prompt). */
.persona-prompt-bar { display: flex; flex-direction: column; gap: 10px; width: 100%; }
.persona-prompt-edit {
display: block; width: 100%; max-width: none; box-sizing: border-box;
font-family: var(--p-mono); font-size: 12px; line-height: 1.55;
color: var(--p-ink); background: var(--p-card); border: 1.5px solid var(--p-ink);
border-radius: 0; padding: 10px 12px; resize: vertical; min-height: 200px;
}
.persona-prompt-edit:focus { outline: none; box-shadow: 0 0 0 1.5px var(--p-transmit); }
.persona-prompt-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
/* Save sits left, Reset is pushed to the far right so the two are clearly separated. */
.persona-prompt-save { margin-top: 0 !important; position: relative; }
.persona-prompt-reset { margin-left: auto; }
/* Pulsing badge on Save = unsaved edits in the prompt. Reuses the play-button badge. */
.persona-prompt-save.badged::after {
content: ''; position: absolute; top: -5px; right: -5px; width: 10px; height: 10px;
background: var(--p-transmit); border: 1.5px solid var(--p-card); border-radius: 50%;
animation: tac-badge-pulse 1.3s ease-out infinite;
}
/* Collapsible control sections (model / voice). Desktop: no toggle, always shown. */
.ctl-collapse > summary { display: none; }
@media (max-width: 768px) {
/* The whole view scrolls as one column (instead of two fighting scroll panes in
a fixed-height box), so the story gets real room and the page scrolls. */
.persona-view { flex-direction: column; height: 100%; overflow-y: auto; -webkit-overflow-scrolling: touch; }
/* Mobile: tuck the model/voice bars behind a tap-to-expand summary. */
.ctl-collapse > summary {
display: block; cursor: pointer; list-style: none; margin-top: 6px;
font-family: var(--p-mono); font-size: 11px; letter-spacing: .12em; text-transform: uppercase;
color: var(--p-ink); background: var(--p-card); border: 1.5px solid var(--p-ink); padding: 10px;
}
.ctl-collapse > summary::-webkit-details-marker { display: none; }
.ctl-collapse > summary::after { content: ' ▾'; float: right; color: var(--p-muted); }
.ctl-collapse[open] > summary::after { content: ' ▴'; }
.persona-label { margin-top: 4px; }
.persona-controls {
width: 100%; border-right: 0; border-bottom: 2px solid var(--p-ink);
overflow: visible; flex-shrink: 0; gap: 6px;
padding: 54px 14px 16px; /* top clears the floating ☰ reopen button */
}
/* The generated content (persona / war story) gets the majority of the screen. */
.persona-result { flex: 1 0 auto; min-height: 62vh; padding: 18px 16px; overflow: visible; }
.persona-name { font-size: 26px; }
.persona-about { font-size: 16px; max-width: none; }
.persona-tags { margin: 10px 0 14px; }
.persona-think { max-height: 150px; }
/* 16px inputs avoid iOS focus-zoom; bigger pads = easier taps. */
.persona-input { font-size: 16px !important; padding: 10px !important; }
.model-select { font-size: 15px !important; padding: 9px 10px !important; }
.persona-go, .persona-go-alt { padding: 12px !important; }
}