updated the ai bot to the next level agent
This commit is contained in:
@@ -12,6 +12,38 @@
|
||||
doesn't carry (the AI accent, the scrim, the panel elevation).
|
||||
========================================================================== */
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
Dock geometry.
|
||||
|
||||
These three live on :root, not on .dai-root, because the LAYOUT has to read
|
||||
them: the assistant is docked into the page rather than floated over it, so
|
||||
the app's content container shifts by exactly the panel's width and animates
|
||||
on the same curve. A token only .dai-root can see cannot do that.
|
||||
|
||||
All three are --dai- prefixed. :root is shared with Dispatch.css and
|
||||
globalPolish.css, and an unprefixed name there is decided by bundle order.
|
||||
--------------------------------------------------------------------------- */
|
||||
:root {
|
||||
/* 30vw, NOT 30%.
|
||||
|
||||
The two consumers resolve a percentage against different boxes: the panel
|
||||
is position:fixed, so `width: 30%` is 30% of the VIEWPORT, while
|
||||
`padding-right: 30%` on the content container is 30% of ITS containing
|
||||
block — which is narrower by the side nav. At 1440px that is 432px of
|
||||
panel against 410px of reserved strip, and the page slides 22px underneath
|
||||
the panel. A viewport unit resolves the same for both.
|
||||
|
||||
max() rather than a separate min-width for the same reason: a min-width
|
||||
that only the panel knows about would reintroduce the mismatch on narrow
|
||||
screens. */
|
||||
/* Default = the NORMAL step. AIPanel overwrites this property on the
|
||||
document element when the operator toggles the width, so the panel and
|
||||
the page's reserved strip always read the same value. */
|
||||
--dai-dock-width: max(300px, 25vw);
|
||||
--dai-duration: 240ms;
|
||||
--dai-ease: cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.dai-root {
|
||||
/* Brand accent — resolves to the app's accent token, which is black
|
||||
(root CLAUDE.md §6.2: "the brand is black", one accent only). Send
|
||||
@@ -20,6 +52,8 @@
|
||||
change here rather than a hunt through the components. */
|
||||
--dai-accent: var(--color-accent, #0f172a);
|
||||
--dai-accent-contrast: #ffffff;
|
||||
/* Focus ring, tinted with the accent rather than a flat grey wash. */
|
||||
--dai-accent-ring: color-mix(in srgb, var(--dai-accent) 14%, transparent);
|
||||
|
||||
/* AI identity accent — used ONLY on the spark/orb marks so the assistant
|
||||
reads as an AI surface without turning the panel into a purple product. */
|
||||
@@ -48,7 +82,14 @@
|
||||
Scrim — deliberately light. The dashboard underneath must stay readable;
|
||||
this is a layering cue, not a modal blackout.
|
||||
-------------------------------------------------------------------------- */
|
||||
/* The scrim exists for the MOBILE presentation only (see the media query at
|
||||
the end of this block). On desktop the panel is docked into the layout and
|
||||
dimming the page behind it would be a lie — the page is still live, still
|
||||
scrollable, and still the thing the operator is working on. Dimming is also
|
||||
the single strongest cue that something is modal, which is exactly the
|
||||
impression this panel should not give. */
|
||||
.dai-scrim {
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1300;
|
||||
@@ -62,31 +103,88 @@
|
||||
/* --------------------------------------------------------------------------
|
||||
Panel
|
||||
-------------------------------------------------------------------------- */
|
||||
/* DOCKED, not floating.
|
||||
|
||||
It used to be an inset card: 12px off every edge, fully rounded, drop
|
||||
shadowed, over a scrim — every cue of a popup. It now sits flush in the
|
||||
right 30% of the workspace, starting under the app header and running to the
|
||||
bottom of the window, with a single hairline on its left edge. The page does
|
||||
not move underneath it; it makes room for it.
|
||||
|
||||
`top` reads Astryx's own measured header height so the dock lines up with
|
||||
the bottom of the top nav rather than guessing a number.
|
||||
|
||||
No radius and no drop shadow: both are what make a surface read as floating
|
||||
ABOVE the page. A soft shadow is kept only as a left-edge falloff so the
|
||||
seam has depth without the panel detaching. */
|
||||
.dai-panel {
|
||||
position: fixed;
|
||||
top: var(--dai-inset);
|
||||
right: var(--dai-inset);
|
||||
bottom: var(--dai-inset);
|
||||
z-index: 1301;
|
||||
width: var(--dai-panel-width);
|
||||
max-width: calc(100vw - (var(--dai-inset) * 2));
|
||||
/* Set from JS by AIPanel — Astryx does not publish a header-height token,
|
||||
despite --appshell-header-height looking like one. */
|
||||
top: var(--dai-dock-top, 57px);
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1200;
|
||||
width: var(--dai-dock-width);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
background: var(--dai-surface);
|
||||
border: 1px solid var(--dai-border);
|
||||
border-radius: 5px;
|
||||
box-shadow: var(--dai-shadow);
|
||||
transform: translateX(calc(100% + var(--dai-inset) * 2));
|
||||
opacity: 0;
|
||||
border-left: 1px solid var(--dai-border);
|
||||
border-radius: 0;
|
||||
box-shadow: -6px 0 20px rgba(15, 23, 42, 0.05);
|
||||
transform: translateX(100%);
|
||||
visibility: hidden;
|
||||
transition:
|
||||
transform var(--dai-duration) var(--dai-ease),
|
||||
opacity var(--dai-duration) var(--dai-ease);
|
||||
visibility 0s linear var(--dai-duration);
|
||||
}
|
||||
.dai-panel[data-open='true'] {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transition:
|
||||
transform var(--dai-duration) var(--dai-ease),
|
||||
visibility 0s;
|
||||
}
|
||||
|
||||
/* ---- The page makes room -------------------------------------------------
|
||||
`.dai-docked` is set on <body> while the panel is open. Padding rather than
|
||||
width/margin: the content container is a flex child inside AppShell's own
|
||||
height:fill chain, and changing its width there fights that chain, whereas
|
||||
padding leaves the box model alone and simply reserves the strip.
|
||||
|
||||
The transition sits on the container unconditionally so the page slides back
|
||||
when the panel closes too — a rule that only exists while `.dai-docked` is
|
||||
applied cannot animate its own removal. */
|
||||
.astryx-layout-content {
|
||||
transition: padding-right var(--dai-duration) var(--dai-ease);
|
||||
}
|
||||
body.dai-docked .astryx-layout-content {
|
||||
padding-right: var(--dai-dock-width);
|
||||
}
|
||||
|
||||
/* ---- Mobile: there is no 30% worth having -------------------------------
|
||||
30% of a phone is ~120px. Below the breakpoint the panel becomes what it
|
||||
used to be everywhere — a full-width overlay with a scrim — and the page
|
||||
stops reserving a strip it cannot afford. */
|
||||
@media (max-width: 900px) {
|
||||
.dai-panel {
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 1301;
|
||||
border-left: none;
|
||||
box-shadow: var(--dai-shadow);
|
||||
}
|
||||
.dai-scrim {
|
||||
display: block;
|
||||
}
|
||||
.dai-scrim[data-open='true'] {
|
||||
opacity: 1;
|
||||
}
|
||||
body.dai-docked .astryx-layout-content {
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
.dai-panel:focus {
|
||||
outline: none;
|
||||
@@ -221,64 +319,102 @@
|
||||
/* --------------------------------------------------------------------------
|
||||
Suggestion cards
|
||||
-------------------------------------------------------------------------- */
|
||||
.dai-suggestion {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
/* The suggestions strip sits between the conversation and the composer, so it
|
||||
needs the composer's horizontal rhythm and a hairline to separate it from
|
||||
the thread scrolling above it. */
|
||||
.dai-root .dai-suggestions-bar {
|
||||
/* Shrinkable, with a hard cap.
|
||||
|
||||
At the normal 25% width the full twenty chips still stack to several rows.
|
||||
As `flex: 0 0 auto` that block refuses to shrink, so on a short laptop it
|
||||
pushes the composer off the bottom of the panel — the input disappears and
|
||||
the assistant cannot be used at all. The cap is a safety valve, not a
|
||||
layout choice: it only engages when the strip would otherwise cost more
|
||||
than a third of the panel, and it never applies once a thread exists,
|
||||
because the strip is four chips by then. */
|
||||
flex: 0 1 auto;
|
||||
min-height: 0;
|
||||
max-height: 34vh;
|
||||
overflow-y: auto;
|
||||
padding: 10px 14px 8px;
|
||||
border-top: 1px solid var(--dai-border);
|
||||
background: var(--dai-surface);
|
||||
}
|
||||
|
||||
/* Follow-up mode: a handful of chips, so the strip is FIXED — it never shrinks
|
||||
and never scrolls.
|
||||
|
||||
The cap and `flex-shrink: 1` above exist for the cold panel's twenty chips.
|
||||
Once a conversation starts the strip is three or four, and leaving it
|
||||
shrinkable meant a long thread squeezed it: the flex row gave the space to
|
||||
the conversation, `overflow-y: auto` quietly hid the remainder, and the
|
||||
follow-ups the operator was meant to act on disappeared below a fold nobody
|
||||
could see. Few chips must always be fully visible. */
|
||||
.dai-root .dai-suggestions-bar[data-compact='true'] {
|
||||
flex: 0 0 auto;
|
||||
max-height: none;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/* ---- Suggested questions: chips ------------------------------------------
|
||||
They wrap, they never scroll. A horizontal scroller can always leave a chip
|
||||
half-visible at the edge, and a half-visible button is a bug no amount of
|
||||
fade masking fixes. */
|
||||
.dai-root .dai-suggestions {
|
||||
width: 100%;
|
||||
padding: 10px 11px;
|
||||
}
|
||||
|
||||
.dai-suggestion {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
max-width: 100%;
|
||||
padding: 5px 10px;
|
||||
text-align: left;
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
line-height: 1.35;
|
||||
font-weight: 500;
|
||||
color: var(--dai-text);
|
||||
background: var(--dai-surface);
|
||||
border: 1px solid var(--dai-border);
|
||||
border-radius: 5px;
|
||||
/* 14px, pinned — NOT 999px.
|
||||
|
||||
On a one-line chip a fully round radius already resolves to about 14px, so
|
||||
these look identical. The difference shows on a chip whose question wraps
|
||||
to two lines: 999px would resolve to half of ~46px and the chip stops
|
||||
reading as a chip and starts reading as a card, so one long suggestion
|
||||
would look like a different component from the nineteen beside it. */
|
||||
border-radius: 14px;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background-color 140ms ease,
|
||||
border-color 140ms ease,
|
||||
color 140ms ease,
|
||||
transform 140ms ease;
|
||||
}
|
||||
.dai-suggestion:hover {
|
||||
background: var(--dai-surface-alt);
|
||||
border-color: var(--dai-border-strong);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.dai-suggestion:active {
|
||||
transform: scale(0.99);
|
||||
transform: translateY(0);
|
||||
}
|
||||
.dai-suggestion:focus-visible {
|
||||
outline: 2px solid var(--dai-accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.dai-root .dai-suggestion-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 auto;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 5px;
|
||||
background: var(--dai-surface-hover);
|
||||
color: var(--dai-text-muted);
|
||||
transition: color 140ms ease;
|
||||
}
|
||||
.dai-suggestion:hover .dai-suggestion-icon {
|
||||
color: var(--dai-text-secondary);
|
||||
}
|
||||
.dai-root .dai-suggestion-text {
|
||||
flex: 1 1 auto;
|
||||
font-size: 13px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.dai-root .dai-suggestion-arrow {
|
||||
flex: 0 0 auto;
|
||||
color: var(--dai-text-muted);
|
||||
opacity: 0;
|
||||
transform: translateX(-3px);
|
||||
transition:
|
||||
opacity 140ms ease,
|
||||
transform 140ms ease;
|
||||
}
|
||||
.dai-suggestion:hover .dai-suggestion-arrow,
|
||||
.dai-suggestion:focus-visible .dai-suggestion-arrow {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
min-width: 0;
|
||||
}
|
||||
/* Plain text link-button ("View more", "Sources") */
|
||||
.dai-link {
|
||||
@@ -565,17 +701,83 @@
|
||||
}
|
||||
.dai-root .dai-composer {
|
||||
border: 1px solid var(--dai-border-strong);
|
||||
border-radius: 5px;
|
||||
/* 14px, the same corner the suggestion chips use. At 5px the input was the
|
||||
one sharp-cornered thing in a panel of rounded surfaces, and it sat
|
||||
directly beneath the chips where the mismatch was most visible. */
|
||||
border-radius: 14px;
|
||||
background: var(--dai-surface);
|
||||
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
||||
padding: 8px 8px 6px 12px;
|
||||
padding: 10px 10px 8px 14px;
|
||||
transition:
|
||||
border-color 140ms ease,
|
||||
box-shadow 140ms ease;
|
||||
}
|
||||
.dai-root .dai-composer[data-focused='true'] {
|
||||
border-color: var(--dai-accent);
|
||||
box-shadow: 0 0 0 3px rgba(15, 23, 42, 0.06);
|
||||
box-shadow: 0 0 0 3px var(--dai-accent-ring);
|
||||
}
|
||||
|
||||
/* Microphone.
|
||||
|
||||
Astryx's smallest dictation button is 28px, two short of the send button it
|
||||
sits beside — a visible step between two controls on the same row. Pinned to
|
||||
match, with the glyph scaled through font-size because the icon inside is
|
||||
sized in em units (the same reason the side nav icons needed font-size
|
||||
rather than width). */
|
||||
.dai-root .dai-mic {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
min-width: 30px;
|
||||
border-radius: 50%;
|
||||
font-size: 16px;
|
||||
}
|
||||
.dai-root .dai-mic svg {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------------
|
||||
Send button.
|
||||
|
||||
This had NO styles at all — the class was referenced once, in a
|
||||
reduced-motion reset, and nowhere else. It rendered as a bare 16px arrow
|
||||
glyph on a transparent background with square corners: not a button, just an
|
||||
icon sitting next to the microphone. Verified in the browser before fixing.
|
||||
-------------------------------------------------------------------------- */
|
||||
.dai-root .dai-send {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 auto;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: var(--dai-accent);
|
||||
color: var(--dai-accent-contrast);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background-color 140ms ease,
|
||||
opacity 140ms ease,
|
||||
transform 140ms ease;
|
||||
}
|
||||
.dai-root .dai-send:hover:not(:disabled) {
|
||||
opacity: 0.86;
|
||||
}
|
||||
.dai-root .dai-send:active:not(:disabled) {
|
||||
transform: scale(0.94);
|
||||
}
|
||||
.dai-root .dai-send:focus-visible {
|
||||
outline: 2px solid var(--dai-accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
/* Disabled is the resting state until something is typed, so it has to read as
|
||||
"not yet", not as "broken" — a filled grey circle, same shape and weight. */
|
||||
.dai-root .dai-send:disabled {
|
||||
background: var(--dai-surface-hover);
|
||||
color: var(--dai-text-muted);
|
||||
cursor: default;
|
||||
}
|
||||
.dai-root .dai-composer textarea {
|
||||
display: block;
|
||||
@@ -597,8 +799,10 @@
|
||||
color: var(--dai-text-muted);
|
||||
}
|
||||
.dai-root .dai-hint {
|
||||
font-size: 11px;
|
||||
font-size: 10.5px;
|
||||
line-height: 1.3;
|
||||
color: var(--dai-text-muted);
|
||||
padding-inline: 2px;
|
||||
}
|
||||
/* --------------------------------------------------------------------------
|
||||
Trigger (lives in the app TopNav)
|
||||
|
||||
Reference in New Issue
Block a user