loopable / web /src /assistant /assistant.css
fsanyoto's picture
Deploy AIOS web (React glide grid + FastAPI slice)
9b857f7 verified
Raw
History Blame
14.5 kB
/* ---------------------------------------------------------------------------
assistant/assistant.css β€” the AI assistant's chat surface (owner item 2).
The shape is borrowed, reference by reference: ChatGPT's centred column and
in-field composer, Glean's source chips under the field, Airtable's "New
chat" panel. The PALETTE is Loopable's own β€” see AssistantPage.tsx's note on
why the dark canvas and the vendor logos are the parts not copied.
EVERY FONT SIZE IS A `--lp-fs-*` TOKEN AND EVERY COLOUR A `--lp-*` TOKEN.
Wave-21 R5 is gated app-wide over every CSS file under src, and a literal
here reds `web_ui` for the whole tree. No emojis, no ALL-CAPS chrome
(DESIGN.md R6).
AND DO NOT WRITE A GLOB IN THIS COMMENT: a star-star followed by a slash is
the CSS comment terminator, and `lightningcss` then dies far from the cause.
--------------------------------------------------------------------------- */
.as-chat {
display: flex;
width: 100%;
height: 100%;
min-width: 0;
overflow: hidden;
background: var(--lp-surface);
}
/* ── Airtable's panel: New chat, then the history ─────────────────────────── */
.as-history {
flex: 0 0 var(--lp-rail-w);
width: var(--lp-rail-w);
min-width: var(--lp-rail-w);
height: 100%;
box-sizing: border-box;
display: flex;
flex-direction: column;
padding: 10px 8px;
border-right: 1px solid var(--lp-line);
background: var(--lp-surface);
overflow: hidden;
}
/* ── R14 / C2: the Chat / Query toggle, directly above New chat ───────────── */
.as-mode-toggle {
flex: 0 0 auto;
display: flex;
gap: 2px;
margin-bottom: 10px;
padding: 2px;
border-radius: var(--lp-r-md);
background: var(--lp-surface-2);
}
.as-mode {
flex: 1 1 0;
min-width: 0;
min-height: 26px;
padding: 4px 8px;
border: 0;
border-radius: 6px;
background: transparent;
color: var(--lp-muted);
font: inherit;
font-size: var(--lp-fs-2xs);
font-weight: 600;
cursor: pointer;
}
.as-mode:hover { color: var(--lp-ink); }
.as-mode.is-on { background: var(--lp-surface); color: var(--lp-ink); }
/* The Query list is the SAME `QueryRail` the standalone page renders β€” only its host changed.
These are the overrides a rail needs when it is nested inside a panel that is already one rail
wide: it must FILL that panel rather than claim a second `--lp-rail-w` beside it, and it must
not draw a second divider against the one `.as-history` already has.
`.qy-rail-ask` goes because it links to the Assistant, which is the surface it is now inside. */
.as-history .qy-rail {
flex: 1 1 auto;
width: auto;
min-width: 0;
height: auto;
min-height: 0;
padding: 0;
border-right: 0;
}
.as-history .qy-rail-ask { display: none; }
/* Query fills the conversation column while the toggle is on Query. */
.as-conversation > .shell-db-frame,
.as-conversation > .qy-empty {
flex: 1 1 auto;
min-height: 0;
}
.as-new {
flex: 0 0 auto;
display: flex;
align-items: center;
gap: 8px;
min-height: 34px;
padding: 7px 10px;
border: 1px solid var(--lp-line);
border-radius: var(--lp-r-md);
background: var(--lp-surface);
color: var(--lp-ink);
font: inherit;
font-size: var(--lp-fs-xs);
font-weight: 600;
text-align: left;
cursor: pointer;
}
.as-new:hover { background: var(--lp-surface-2); }
.as-new svg {
fill: none;
stroke: currentColor;
stroke-width: 1.6;
stroke-linecap: round;
}
.as-thread-list {
flex: 1 1 auto;
min-height: 0;
margin-top: 10px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 1px;
}
.as-thread-row {
display: flex;
align-items: center;
min-width: 0;
border-radius: var(--lp-r-md);
}
.as-thread-row:hover { background: var(--lp-surface-2); }
.as-thread-row.is-active { background: var(--lp-blue-tint); }
.as-thread-row.is-active .as-thread { font-weight: 600; }
.as-thread {
flex: 1 1 auto;
min-width: 0;
min-height: 30px;
padding: 6px 10px;
border: 0;
border-radius: var(--lp-r-md);
background: transparent;
color: var(--lp-ink);
font: inherit;
font-size: var(--lp-fs-xs);
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
}
.as-thread-del {
flex: 0 0 auto;
width: 24px;
height: 24px;
margin-right: 4px;
display: inline-flex;
align-items: center;
justify-content: center;
border: 0;
border-radius: 6px;
background: transparent;
color: var(--lp-muted);
opacity: 0;
cursor: pointer;
}
.as-thread-del svg {
fill: none;
stroke: currentColor;
stroke-width: 1.3;
stroke-linecap: round;
stroke-linejoin: round;
}
.as-thread-row:hover .as-thread-del,
.as-thread-row.is-active .as-thread-del,
.as-thread-del:focus-visible { opacity: 1; }
.as-thread-confirm {
flex: 0 0 auto;
margin-right: 4px;
padding: 3px 8px;
border: 0;
border-radius: 6px;
background: var(--lp-red-deep);
color: var(--lp-surface);
font: inherit;
font-size: var(--lp-fs-2xs);
font-weight: 600;
cursor: pointer;
}
.as-thread-empty {
margin: 8px 10px 0;
color: var(--lp-muted);
font-size: var(--lp-fs-2xs);
}
/* ── ChatGPT's column ─────────────────────────────────────────────────────── */
.as-conversation {
flex: 1 1 auto;
min-width: 0;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* One measure, used by the stream, the dock and the opening state, so the
composer does not shift sideways when the first answer arrives. */
.as-column {
width: 100%;
max-width: 760px;
margin: 0 auto;
padding: 0 24px;
box-sizing: border-box;
}
.as-stream {
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
padding: 26px 0 8px;
}
.as-dock {
flex: 0 0 auto;
padding: 6px 0 18px;
background: var(--lp-surface);
}
.as-opening {
flex: 1 1 auto;
min-height: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 24px;
box-sizing: border-box;
text-align: center;
}
.as-opening-h {
margin: 0 0 6px;
color: var(--lp-ink);
font-size: var(--lp-fs-lg);
font-weight: 650;
}
.as-opening-p {
max-width: 52ch;
margin: 0 0 20px;
color: var(--lp-muted);
font-size: var(--lp-fs-sm);
line-height: var(--lp-lh);
}
.as-opening .as-composer {
width: 100%;
max-width: 700px;
text-align: left;
}
/* ── the messages ─────────────────────────────────────────────────────────── */
.as-message { margin: 0 0 22px; }
.as-message--user {
display: flex;
justify-content: flex-end;
}
.as-bubble {
max-width: 78%;
margin: 0;
padding: 9px 14px;
border-radius: 18px;
background: var(--lp-surface-2);
color: var(--lp-ink);
font-size: var(--lp-fs-sm);
line-height: var(--lp-lh);
white-space: pre-wrap;
overflow-wrap: anywhere;
}
/* No bubble and no "Assistant" label: the reference prints the answer as the
page's own prose, and a role label on every turn is the thing that makes a
chat read like a transcript instead of a reply. */
.as-message-text {
margin: 0;
color: var(--lp-ink);
font-size: var(--lp-fs-sm);
line-height: var(--lp-lh);
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.as-message--assistant.is-refusal .as-message-text { color: var(--lp-muted); }
.as-numeric {
display: flex;
align-items: baseline;
gap: 8px;
margin: 10px 0 0;
}
.as-numeric-label { color: var(--lp-muted); font-size: var(--lp-fs-2xs); }
.as-numeric-value { color: var(--lp-ink); font-size: var(--lp-fs-md); font-weight: 650; }
.as-preview {
display: flex;
flex-direction: column;
gap: 2px;
margin-top: 12px;
padding: 9px 13px;
border: 1px solid var(--lp-line);
border-radius: var(--lp-r-md);
background: var(--lp-surface);
font: inherit;
text-align: left;
cursor: pointer;
}
.as-preview:hover { background: var(--lp-surface-2); }
.as-preview-name { color: var(--lp-ink); font-size: var(--lp-fs-xs); font-weight: 600; }
.as-preview-meta { color: var(--lp-blue-solid); font-size: var(--lp-fs-2xs); font-weight: 600; }
/* ── R20: the thumbs, which the server reads back into the next turn ───────── */
.as-rate {
display: flex;
align-items: center;
gap: 4px;
margin-top: 10px;
}
.as-rate-btn {
width: 26px;
height: 26px;
display: inline-flex;
align-items: center;
justify-content: center;
border: 0;
border-radius: 6px;
background: transparent;
color: var(--lp-muted);
cursor: pointer;
}
.as-rate-btn svg {
fill: none;
stroke: currentColor;
stroke-width: 1.3;
stroke-linecap: round;
stroke-linejoin: round;
}
.as-rate-btn:hover { background: var(--lp-surface-2); color: var(--lp-ink); }
.as-rate-btn.is-on { background: var(--lp-blue-tint); color: var(--lp-blue-solid); }
.as-rate-why-said {
margin-left: 4px;
color: var(--lp-muted);
font-size: var(--lp-fs-2xs);
}
.as-rate-why {
display: flex;
gap: 6px;
margin-top: 6px;
}
.as-rate-input {
flex: 1 1 auto;
min-width: 0;
padding: 6px 10px;
border: 1px solid var(--lp-line);
border-radius: var(--lp-r-md);
background: var(--lp-surface);
color: var(--lp-ink);
font: inherit;
font-size: var(--lp-fs-2xs);
}
.as-rate-input:focus { outline: none; border-color: var(--lp-blue-solid); }
.as-rate-send {
flex: 0 0 auto;
padding: 6px 12px;
border: 1px solid var(--lp-line);
border-radius: var(--lp-r-md);
background: var(--lp-surface);
color: var(--lp-ink);
font: inherit;
font-size: var(--lp-fs-2xs);
font-weight: 600;
cursor: pointer;
}
.as-rate-send:hover { background: var(--lp-surface-2); }
.as-sources { margin-top: 8px; }
.as-sources-btn {
padding: 0;
border: 0;
background: transparent;
color: var(--lp-muted);
font: inherit;
font-size: var(--lp-fs-2xs);
font-weight: 600;
cursor: pointer;
}
.as-sources-btn:hover { color: var(--lp-ink); }
.as-sources-list {
display: flex;
flex-direction: column;
gap: 4px;
margin-top: 6px;
}
.as-citation {
color: var(--lp-muted);
font-size: var(--lp-fs-2xs);
line-height: var(--lp-lh);
text-decoration: none;
overflow-wrap: anywhere;
}
.as-citation:hover { color: var(--lp-blue-solid); text-decoration: underline; }
.as-working { margin: 0 0 22px; }
.as-problem {
margin: 0 0 10px;
color: var(--lp-red-deep);
font-size: var(--lp-fs-2xs);
}
/* ── the composer ─────────────────────────────────────────────────────────── */
.as-field {
border: 1px solid var(--lp-line);
border-radius: 22px;
background: var(--lp-surface);
padding: 10px 8px 8px 16px;
box-shadow: 0 1px 2px rgba(20, 24, 40, 0.05);
}
.as-field:focus-within { border-color: var(--lp-blue-solid); }
.as-prompt {
display: block;
width: 100%;
max-height: 180px;
border: 0;
outline: none;
padding: 0 8px 0 0;
background: transparent;
color: var(--lp-ink);
font: inherit;
font-size: var(--lp-fs-sm);
line-height: var(--lp-lh);
resize: none;
box-sizing: border-box;
}
.as-prompt::placeholder { color: var(--lp-muted); }
.as-field-foot {
display: flex;
align-items: center;
gap: 12px;
margin-top: 8px;
}
.as-pick {
display: inline-flex;
align-items: center;
gap: 5px;
min-width: 0;
}
.as-pick-label { color: var(--lp-muted); font-size: var(--lp-fs-2xs); }
/* Text, not a boxed control β€” ChatGPT's "Instant" reads as part of the field. */
.as-pick-select {
max-width: 150px;
border: 0;
background: transparent;
color: var(--lp-ink);
font: inherit;
font-size: var(--lp-fs-2xs);
font-weight: 600;
cursor: pointer;
}
.as-send {
flex: 0 0 auto;
margin-left: auto;
width: 30px;
height: 30px;
display: inline-flex;
align-items: center;
justify-content: center;
border: 0;
border-radius: 999px;
background: var(--lp-ink);
color: var(--lp-surface);
cursor: pointer;
}
.as-send svg { fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
.as-send:disabled { background: var(--lp-surface-2); color: var(--lp-muted); cursor: default; }
/* ── Glean's chips ────────────────────────────────────────────────────────── */
.as-chips {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 10px;
padding: 0 6px;
}
.as-chip {
display: inline-flex;
align-items: center;
gap: 6px;
max-width: 220px;
padding: 5px 11px;
border: 1px solid transparent;
border-radius: 999px;
background: var(--lp-surface-2);
color: var(--lp-ink);
font: inherit;
font-size: var(--lp-fs-2xs);
font-weight: 500;
cursor: pointer;
}
.as-chip:hover { border-color: var(--lp-line); }
.as-chip.is-selected { background: var(--lp-blue-tint); border-color: var(--lp-blue-tint); font-weight: 600; }
/* The one the answer will be built on, when more than one is picked (R3). */
.as-chip.is-target { border-color: var(--lp-blue-solid); }
.as-chip-mark {
flex: 0 0 auto;
display: inline-flex;
width: 14px;
height: 14px;
color: var(--lp-muted);
}
.as-chip-mark svg {
width: 14px;
height: 14px;
fill: none;
stroke: currentColor;
stroke-width: 1.4;
stroke-linecap: round;
stroke-linejoin: round;
}
.as-chip.is-selected .as-chip-mark { color: var(--lp-blue-solid); }
.as-chip-name {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* A source this caller holds and cannot ask (see AssistantPage's note): still visible, because
an artefact already built on it stays listed β€” but plainly not a door. */
.as-chip.is-blocked {
color: var(--lp-muted);
background: transparent;
border-color: var(--lp-line);
border-style: dashed;
cursor: default;
}
.as-chip.is-blocked .as-chip-mark { opacity: 0.5; }
.as-chip.is-blocked:hover { border-color: var(--lp-line); }
.as-chips-note {
flex: 1 0 100%;
margin: 2px 0 0;
color: var(--lp-muted);
font-size: var(--lp-fs-2xs);
}
.as-chips-empty {
margin: 2px 0 0;
color: var(--lp-muted);
font-size: var(--lp-fs-2xs);
}