updated the ai bot to the next level agent

This commit is contained in:
2026-08-20 18:23:37 +05:30
parent 1bf89adb6a
commit 505d50cc0a
7 changed files with 774 additions and 122 deletions

View File

@@ -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)