| """Styling constants for the digital twin Gradio app.""" |
|
|
| GOLD = "#ecad0a" |
| BLUE = "#209dd7" |
| PURPLE = "#753991" |
|
|
| EXAMPLES = [ |
| "Tell me about your background and experience.", |
| "What kinds of projects are you working on now?", |
| "What are your strongest technical skills?", |
| "How can I get in touch with you?", |
| ] |
|
|
| CSS = """ |
| :root { |
| --twin-gold: #ecad0a; |
| --twin-blue: #209dd7; |
| --twin-purple: #753991; |
| --twin-bg: #0d0d10; |
| --twin-surface: #16161b; |
| --twin-surface-2: #1c1c22; |
| --twin-border: #2a2a32; |
| --twin-border-strong: #3a3a44; |
| --twin-text: #ececef; |
| --twin-muted: #8c8c95; |
| } |
| |
| /* Light mode: Gradio adds `.dark` to <body> when dark; absence = light. |
| Only the neutral palette flips — gold/blue/purple accents stay identical. */ |
| body:not(.dark) { |
| --twin-bg: #f4f4f6; |
| --twin-surface: #ffffff; |
| --twin-surface-2: #ededf0; |
| --twin-border: #dcdce2; |
| --twin-border-strong: #b8b8c0; |
| --twin-text: #1a1a20; |
| --twin-muted: #6a6a72; |
| } |
| |
| footer, .built-with, .show-api, .api-docs { display: none !important; } |
| |
| html, body, gradio-app { background: var(--twin-bg) !important; } |
| |
| /* ---------- Stable layout ---------- */ |
| .gradio-container { |
| background: var(--twin-bg) !important; |
| color: var(--twin-text) !important; |
| font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important; |
| width: 100% !important; |
| max-width: 880px !important; |
| min-width: 0 !important; |
| margin: 0 auto !important; |
| padding: 32px 24px 48px !important; |
| } |
| .gradio-container .main, .gradio-container .contain, .gradio-container .wrap { |
| width: 100% !important; |
| max-width: 100% !important; |
| min-width: 0 !important; |
| } |
| .gradio-container * { min-width: 0; } |
| |
| /* ---------- Title ---------- */ |
| .gradio-container h1 { |
| color: var(--twin-text) !important; |
| font-size: 26px !important; |
| font-weight: 700 !important; |
| letter-spacing: -0.02em !important; |
| border-left: 3px solid var(--twin-gold); |
| padding-left: 12px !important; |
| margin: 4px 0 8px !important; |
| text-align: left !important; |
| } |
| |
| /* ---------- Sharp corners on structural pieces ---------- */ |
| .chatbot, .chatbot *, .block, .form, |
| button, input, textarea, |
| .examples button { |
| border-radius: 0 !important; |
| } |
| |
| /* ---------- Block surfaces ---------- */ |
| .block, .form { background: transparent !important; box-shadow: none !important; } |
| |
| /* ---------- Hide the Chatbot label / header strip ---------- */ |
| .chatbot > .block-label, |
| .chatbot > label, |
| .chatbot .label-wrap, |
| .chatbot .block-label, |
| .chatbot > .label-container { |
| display: none !important; |
| } |
| |
| /* ---------- Chatbot frame ---------- */ |
| .chatbot, .chatbot.block { |
| background: var(--twin-surface) !important; |
| border: 1px solid var(--twin-border) !important; |
| min-height: 460px !important; |
| box-shadow: none !important; |
| } |
| .chatbot .placeholder, .chatbot .placeholder * { color: var(--twin-muted) !important; } |
| |
| /* ---------- Message rows: strip parent backgrounds ---------- */ |
| .message-row, |
| .message-row > div, |
| .message-row .role, |
| .message-wrap, .bubble-wrap { |
| background: transparent !important; |
| border: 0 !important; |
| box-shadow: none !important; |
| } |
| |
| /* ---------- Reset borders on every bubble variant first ---------- */ |
| .message-row .message, |
| .message-row .message-bubble, |
| .message-row .bubble { |
| border: 0 !important; |
| box-shadow: none !important; |
| padding: 6px 10px !important; |
| } |
| |
| /* ---------- Bubble backgrounds (broad to cover Gradio variants) ---------- */ |
| .message-row.user-row .message, |
| .message-row.user-row .message-bubble, |
| .message-row.user-row .bubble, |
| .message-row[data-role="user"] .message, |
| .message-row[data-role="user"] .message-bubble { |
| background: var(--twin-blue) !important; |
| color: #ffffff !important; |
| } |
| |
| .message-row.bot-row .message, |
| .message-row.bot-row .message-bubble, |
| .message-row.bot-row .bubble, |
| .message-row[data-role="assistant"] .message, |
| .message-row[data-role="assistant"] .message-bubble { |
| background: var(--twin-surface-2) !important; |
| color: var(--twin-text) !important; |
| } |
| |
| /* ---------- Purple stripe ---------- |
| Apply to every common bubble class for assistant rows (we don't know which |
| one the running Gradio uses), then suppress on any *nested* instance so the |
| stripe lands on the outermost matching element only — exactly one stripe. */ |
| .message-row.bot-row .message, |
| .message-row.bot-row .bubble, |
| .message-row.bot-row .message-bubble, |
| .message-row[data-role="assistant"] .message, |
| .message-row[data-role="assistant"] .bubble, |
| .message-row[data-role="assistant"] .message-bubble { |
| border-left: 2px solid var(--twin-purple) !important; |
| } |
| |
| .message-row.bot-row .message .message, |
| .message-row.bot-row .message .bubble, |
| .message-row.bot-row .message .message-bubble, |
| .message-row.bot-row .bubble .message, |
| .message-row.bot-row .bubble .bubble, |
| .message-row.bot-row .bubble .message-bubble, |
| .message-row.bot-row .message-bubble .message, |
| .message-row.bot-row .message-bubble .bubble, |
| .message-row.bot-row .message-bubble .message-bubble, |
| .message-row[data-role="assistant"] .message .message, |
| .message-row[data-role="assistant"] .message .bubble, |
| .message-row[data-role="assistant"] .message .message-bubble, |
| .message-row[data-role="assistant"] .bubble .message, |
| .message-row[data-role="assistant"] .bubble .bubble, |
| .message-row[data-role="assistant"] .bubble .message-bubble, |
| .message-row[data-role="assistant"] .message-bubble .message, |
| .message-row[data-role="assistant"] .message-bubble .bubble, |
| .message-row[data-role="assistant"] .message-bubble .message-bubble { |
| border-left: 0 !important; |
| } |
| |
| /* ---------- Uniform font size in bubbles ---------- |
| The "first paragraph different size" was caused by a leaky `.prose p:first-of-type` |
| selector. Force every paragraph in a bubble to the same size. */ |
| .message-row .message, |
| .message-row .message-bubble, |
| .message-row .bubble { |
| font-size: 14px !important; |
| line-height: 1.55 !important; |
| } |
| .message-row .message p, |
| .message-row .message-bubble p, |
| .message-row .bubble p, |
| .message-row .prose p { |
| font-size: 14px !important; |
| line-height: 1.55 !important; |
| margin: 0 0 8px !important; |
| color: inherit !important; |
| } |
| .message-row .message p:last-child, |
| .message-row .message-bubble p:last-child, |
| .message-row .bubble p:last-child, |
| .message-row .prose p:last-child { margin-bottom: 0 !important; } |
| |
| /* Strip stray internal borders/backgrounds from anything inside a bubble */ |
| .message-row .message *, |
| .message-row .message-bubble *, |
| .message-row .bubble * { |
| background: transparent !important; |
| border-color: transparent !important; |
| box-shadow: none !important; |
| color: inherit !important; |
| } |
| .message-row .message a, |
| .message-row .message-bubble a { |
| color: var(--twin-gold) !important; |
| text-decoration: underline; |
| } |
| |
| /* ---------- Input row alignment ---------- */ |
| .input-row, |
| .gr-input-row, |
| .chat-input-row, |
| form[class*="input"] { align-items: stretch !important; } |
| |
| textarea, input[type="text"] { |
| background: var(--twin-surface) !important; |
| border: 1px solid var(--twin-border) !important; |
| color: var(--twin-text) !important; |
| font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif !important; |
| font-size: 14px !important; |
| padding: 12px 14px !important; |
| line-height: 1.4 !important; |
| min-height: 48px !important; |
| } |
| textarea:focus, input[type="text"]:focus { |
| border-color: var(--twin-gold) !important; |
| outline: none !important; |
| box-shadow: 0 0 0 1px var(--twin-gold) !important; |
| } |
| textarea::placeholder, input::placeholder { color: var(--twin-muted) !important; } |
| |
| /* ---------- Buttons ---------- */ |
| button { |
| font-family: 'JetBrains Mono', 'SF Mono', Menlo, monospace !important; |
| letter-spacing: 0.12em !important; |
| text-transform: uppercase !important; |
| font-size: 11px !important; |
| font-weight: 600 !important; |
| border: 1px solid var(--twin-border) !important; |
| background: transparent !important; |
| color: var(--twin-text) !important; |
| padding: 0 16px !important; |
| min-height: 48px !important; |
| align-self: stretch !important; |
| display: inline-flex !important; |
| align-items: center !important; |
| justify-content: center !important; |
| cursor: pointer; |
| transition: background 0.12s ease, color 0.12s ease, border-color 0.12s ease; |
| } |
| button:hover { border-color: var(--twin-gold) !important; color: var(--twin-gold) !important; } |
| |
| button.primary, |
| button[variant="primary"], |
| button.submit, |
| button.submit-button, |
| .submit-button, |
| button.lg.primary { |
| background: var(--twin-gold) !important; |
| border: 1px solid var(--twin-gold) !important; |
| color: #111111 !important; |
| min-height: 48px !important; |
| align-self: stretch !important; |
| padding: 0 14px !important; |
| display: inline-flex !important; |
| align-items: center !important; |
| justify-content: center !important; |
| } |
| button.primary:hover, |
| button.submit:hover, |
| .submit-button:hover, |
| button.lg.primary:hover { |
| background: #ffc320 !important; |
| border-color: #ffc320 !important; |
| color: #111111 !important; |
| } |
| |
| /* ---------- Submit-button icon: center vertically and size correctly ---------- */ |
| button.submit svg, |
| button.submit-button svg, |
| .submit-button svg, |
| button.primary svg, |
| button[variant="primary"] svg { |
| width: 18px !important; |
| height: 18px !important; |
| margin: 0 auto !important; |
| display: block !important; |
| align-self: center !important; |
| color: #111111 !important; |
| fill: currentColor !important; |
| stroke: currentColor !important; |
| } |
| |
| /* ---------- Examples ---------- */ |
| .examples, .examples-holder, [data-testid="examples"] { |
| background: transparent !important; |
| padding: 0 !important; |
| margin-top: 14px !important; |
| } |
| .examples table, .examples-table { background: transparent !important; border: 0 !important; } |
| .examples button, .example, .examples td button, [data-testid="examples"] button { |
| background: var(--twin-surface) !important; |
| border: 1px solid var(--twin-border) !important; |
| color: var(--twin-text) !important; |
| text-transform: none !important; |
| letter-spacing: 0 !important; |
| font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif !important; |
| font-size: 13px !important; |
| font-weight: 400 !important; |
| padding: 10px 14px !important; |
| text-align: left !important; |
| min-height: 0 !important; |
| align-self: auto !important; |
| display: inline-block !important; |
| } |
| .examples button:hover, .example:hover, [data-testid="examples"] button:hover { |
| border-color: var(--twin-blue) !important; |
| color: var(--twin-blue) !important; |
| background: var(--twin-surface) !important; |
| } |
| |
| /* ---------- Icon buttons (clear, retry, copy) ---------- */ |
| .icon-button, .chatbot .icon-button { |
| color: var(--twin-muted) !important; |
| background: transparent !important; |
| border: 0 !important; |
| min-height: 0 !important; |
| align-self: auto !important; |
| padding: 4px !important; |
| display: inline-flex !important; |
| align-items: center !important; |
| justify-content: center !important; |
| } |
| .icon-button:hover, .chatbot .icon-button:hover { color: var(--twin-gold) !important; } |
| |
| /* ---------- Scrollbar ---------- */ |
| ::-webkit-scrollbar { width: 10px; height: 10px; } |
| ::-webkit-scrollbar-track { background: var(--twin-bg); } |
| ::-webkit-scrollbar-thumb { background: var(--twin-border-strong); } |
| ::-webkit-scrollbar-thumb:hover { background: var(--twin-purple); } |
| |
| /* ---------- Selection ---------- */ |
| ::selection { background: var(--twin-gold); color: #111111; } |
| |
| /* ---------- Mobile ---------- */ |
| @media (max-width: 640px) { |
| .gradio-container { padding: 22px 14px 36px !important; } |
| .gradio-container h1 { font-size: 22px !important; } |
| } |
| """ |
|
|
| JS = """ |
| () => { |
| document.title = 'Digital Twin'; |
| |
| const focusInput = () => { |
| const areas = document.querySelectorAll('textarea'); |
| if (areas.length) areas[areas.length - 1].focus(); |
| }; |
| setTimeout(focusInput, 300); |
| |
| // Re-focus the message field whenever Gradio re-enables it |
| // (i.e. after the assistant finishes responding). |
| const watchTextarea = (area) => { |
| if (area.dataset.twinWatched) return; |
| area.dataset.twinWatched = '1'; |
| let wasDisabled = area.disabled || area.readOnly; |
| new MutationObserver(() => { |
| const isDisabled = area.disabled || area.readOnly; |
| if (wasDisabled && !isDisabled) area.focus(); |
| wasDisabled = isDisabled; |
| }).observe(area, { attributes: true, attributeFilter: ['disabled', 'readonly'] }); |
| }; |
| |
| const scan = () => document.querySelectorAll('textarea').forEach(watchTextarea); |
| setTimeout(scan, 500); |
| new MutationObserver(scan).observe(document.body, { childList: true, subtree: true }); |
| } |
| """ |
|
|