feat: add assistant window controls
This commit is contained in:
1 parent
bb6e8e5c5a
commit
517d9320dc
3 files changed
+144
-15
No files matched your search
@@ -22,6 +22,8 @@ While a response is running, a compact activity bar appears at the bottom of the
|
||||
avatar shows a thinking indicator. The activity bar can be opened to follow the response. Completed
|
||||
responses add an unread count to the user menu until they are opened or dismissed. The expanded
|
||||
conversation has a minimize action that returns it to the activity bar without interrupting a response.
|
||||
Its desktop-style window controls can also maximize the conversation to the available viewport or close it
|
||||
without leaving an activity bar behind. Each control includes an icon, tooltip, and accessible label.
|
||||
The transcript follows streamed output while it is already at the bottom, including fast responses. Scrolling
|
||||
up releases that follow state until **Scroll to latest** is selected or the user returns to the bottom.
|
||||
|
||||
|
||||
@@ -117,6 +117,15 @@
|
||||
animation: assistant-panel-in 180ms cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
.floatingPanel[data-maximized] {
|
||||
inset: 1rem;
|
||||
width: auto;
|
||||
height: auto;
|
||||
min-height: 0;
|
||||
transform: none;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.widgetPanel {
|
||||
container-name: assistant-widget;
|
||||
container-type: size;
|
||||
@@ -150,6 +159,7 @@
|
||||
}
|
||||
|
||||
.panelHeader {
|
||||
min-width: 0;
|
||||
min-height: 3.75rem;
|
||||
flex: 0 0 auto;
|
||||
padding: 0.65rem 0.75rem 0.65rem 0.9rem;
|
||||
@@ -166,9 +176,52 @@
|
||||
}
|
||||
|
||||
.panelActions {
|
||||
min-width: 0;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.windowControls {
|
||||
flex: 0 0 auto;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.windowControl {
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
border: 0;
|
||||
color: rgba(0, 0, 0, 0.72);
|
||||
transition:
|
||||
filter 160ms cubic-bezier(0.22, 1, 0.36, 1),
|
||||
transform 160ms cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
.windowControl:hover {
|
||||
filter: brightness(0.94) saturate(1.08);
|
||||
}
|
||||
|
||||
.windowControl:active {
|
||||
transform: scale(0.92);
|
||||
}
|
||||
|
||||
.windowControl:focus-visible {
|
||||
outline: 2px solid var(--mantine-primary-color-filled);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.windowControlClose {
|
||||
background: var(--mantine-color-red-6);
|
||||
}
|
||||
|
||||
.windowControlMinimize {
|
||||
background: var(--mantine-color-yellow-5);
|
||||
}
|
||||
|
||||
.windowControlMaximize {
|
||||
background: var(--mantine-color-green-6);
|
||||
}
|
||||
|
||||
.panelAction {
|
||||
width: 2.625rem;
|
||||
height: 2.625rem;
|
||||
@@ -322,12 +375,15 @@
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.messages {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
max-width: 44rem;
|
||||
box-sizing: border-box;
|
||||
margin-inline: auto;
|
||||
padding: 0.75rem 1rem 1.5rem;
|
||||
}
|
||||
@@ -1276,6 +1332,7 @@
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
max-width: none;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
@@ -1368,11 +1425,15 @@
|
||||
|
||||
.composerFooter {
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
padding-inline: 0.2rem;
|
||||
}
|
||||
|
||||
.composerControls {
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -1482,7 +1543,8 @@
|
||||
inset-inline: 0;
|
||||
bottom: calc(100% + 0.45rem);
|
||||
max-height: min(22rem, 45dvh);
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
padding: 0.35rem;
|
||||
border: 1px solid var(--mantine-color-default-border);
|
||||
border-radius: var(--mantine-radius-md);
|
||||
@@ -1492,12 +1554,17 @@
|
||||
|
||||
.triggerList {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
gap: 0.15rem;
|
||||
}
|
||||
|
||||
.triggerItem {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
min-height: 2.75rem;
|
||||
align-items: center;
|
||||
gap: 0.55rem;
|
||||
@@ -1599,6 +1666,14 @@
|
||||
min-height: min(20rem, calc(100dvh - 1rem - env(safe-area-inset-bottom, 0px)));
|
||||
}
|
||||
|
||||
.floatingPanel[data-maximized] {
|
||||
inset: 0;
|
||||
width: 100vw;
|
||||
height: 100dvh;
|
||||
min-height: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.panelHeader {
|
||||
min-height: 3.5rem;
|
||||
padding: 0.5rem;
|
||||
@@ -1642,6 +1717,14 @@
|
||||
}
|
||||
|
||||
@media (max-width: 30em) {
|
||||
.panelIdentity > :first-child {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.panelIdentityText {
|
||||
max-width: 7rem;
|
||||
}
|
||||
|
||||
.historyButton {
|
||||
width: 2.625rem;
|
||||
max-width: 2.625rem;
|
||||
@@ -1726,6 +1809,7 @@
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.activityBar,
|
||||
.floatingPanel,
|
||||
.windowControl,
|
||||
.disclosureIcon,
|
||||
.reasoningChevron {
|
||||
animation: none;
|
||||
|
||||
@@ -78,6 +78,7 @@ import {
|
||||
IconApps,
|
||||
IconArrowUp,
|
||||
IconArrowsMaximize,
|
||||
IconArrowsMinimize,
|
||||
IconAt,
|
||||
IconCheck,
|
||||
IconChevronDown,
|
||||
@@ -2731,6 +2732,9 @@ interface AssistantConversationSurfaceProps extends AssistantConversationControl
|
||||
pendingAction: AssistantPendingAction | undefined;
|
||||
onExpand?: () => void;
|
||||
onMinimize?: () => void;
|
||||
onWindowClose?: () => void;
|
||||
onToggleMaximize?: () => void;
|
||||
maximized?: boolean;
|
||||
}
|
||||
|
||||
export const AssistantConversationSurface = ({
|
||||
@@ -2747,6 +2751,9 @@ export const AssistantConversationSurface = ({
|
||||
onReasoningChange,
|
||||
onExpand,
|
||||
onMinimize,
|
||||
onWindowClose,
|
||||
onToggleMaximize,
|
||||
maximized = false,
|
||||
}: AssistantConversationSurfaceProps) => {
|
||||
const t = useScopedI18n("common.assistant");
|
||||
const reducedMotion = useReducedMotion();
|
||||
@@ -2761,6 +2768,48 @@ export const AssistantConversationSurface = ({
|
||||
return (
|
||||
<>
|
||||
<Group className={classes.panelHeader} justify="space-between" wrap="nowrap" gap="xs">
|
||||
{onWindowClose && onMinimize && onToggleMaximize && (
|
||||
<Group
|
||||
component="fieldset"
|
||||
className={classes.windowControls}
|
||||
gap={4}
|
||||
wrap="nowrap"
|
||||
aria-label={t("windowControls")}
|
||||
>
|
||||
<Tooltip label={t("close")}>
|
||||
<ActionIcon
|
||||
className={`${classes.windowControl} ${classes.windowControlClose}`}
|
||||
onClick={onWindowClose}
|
||||
aria-label={t("close")}
|
||||
>
|
||||
<IconX size={12} stroke={2.4} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label={t("minimize")}>
|
||||
<ActionIcon
|
||||
className={`${classes.windowControl} ${classes.windowControlMinimize}`}
|
||||
onClick={onMinimize}
|
||||
aria-label={t("minimize")}
|
||||
>
|
||||
<IconMinus size={12} stroke={2.4} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label={maximized ? t("restore") : t("maximize")}>
|
||||
<ActionIcon
|
||||
className={`${classes.windowControl} ${classes.windowControlMaximize}`}
|
||||
onClick={onToggleMaximize}
|
||||
aria-label={maximized ? t("restore") : t("maximize")}
|
||||
aria-pressed={maximized}
|
||||
>
|
||||
{maximized ? (
|
||||
<IconArrowsMinimize size={11} stroke={2.4} />
|
||||
) : (
|
||||
<IconArrowsMaximize size={11} stroke={2.4} />
|
||||
)}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
)}
|
||||
<Group className={classes.panelIdentity} gap="xs" wrap="nowrap">
|
||||
<ThemeIcon variant="light" color="red" radius="xl">
|
||||
<IconRobot size={18} />
|
||||
@@ -2800,19 +2849,6 @@ export const AssistantConversationSurface = ({
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
{onMinimize && (
|
||||
<Tooltip label={t("minimize")}>
|
||||
<ActionIcon
|
||||
className={classes.panelAction}
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
onClick={onMinimize}
|
||||
aria-label={t("minimize")}
|
||||
>
|
||||
<IconMinus size={18} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Group>
|
||||
</Group>
|
||||
<AssistantQuestionPortalProvider target={questionPortalTarget}>
|
||||
@@ -2889,6 +2925,7 @@ export const AssistantPanel = ({
|
||||
}: AssistantPanelProps) => {
|
||||
const t = useScopedI18n("common.assistant");
|
||||
const previousFocusRef = useRef<HTMLElement | null>(null);
|
||||
const [maximized, setMaximized] = useState(false);
|
||||
|
||||
useWindowEvent("keydown", (event) => {
|
||||
if (opened && event.key === "Escape") onClose();
|
||||
@@ -2928,7 +2965,7 @@ export const AssistantPanel = ({
|
||||
/>
|
||||
)}
|
||||
{opened && (
|
||||
<dialog className={classes.floatingPanel} aria-label={t("title")} open>
|
||||
<dialog className={classes.floatingPanel} data-maximized={maximized || undefined} aria-label={t("title")} open>
|
||||
<AssistantConversationSurface
|
||||
isRunning={isRunning}
|
||||
pendingAction={pendingAction}
|
||||
@@ -2942,6 +2979,12 @@ export const AssistantPanel = ({
|
||||
onReasoningChange={onReasoningChange}
|
||||
autoFocusComposer
|
||||
onMinimize={onClose}
|
||||
onWindowClose={() => {
|
||||
onDismissActivity();
|
||||
onClose();
|
||||
}}
|
||||
onToggleMaximize={() => setMaximized((current) => !current)}
|
||||
maximized={maximized}
|
||||
/>
|
||||
</dialog>
|
||||
)}
|
||||
|
||||
Reference in new issue
Block a user