mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-14 10:34:06 +02:00
Kill off agents pane now that we have FAB (#6613)
# Description of Changes Kill off agents pane now that we have the FAB. Also fixes a bug with the FAB where it would sometimes fail to render the chat, and fixes a duplicated entry in the Vite config which was throwing a warning
This commit is contained in:
@@ -1,43 +0,0 @@
|
||||
/**
|
||||
* Core stubs for the right-rail Agents UI.
|
||||
*
|
||||
* The real implementations live in {@code proprietary/components/agents/AgentsPanel.tsx}
|
||||
* and shadow these stubs via the {@code @app/*} alias cascade when the proprietary
|
||||
* build is active. Core builds render nothing, so the right rail collapses to the
|
||||
* tool list unchanged.
|
||||
*/
|
||||
|
||||
/** Whether the right rail should reserve space for agents UI. False in core. */
|
||||
export function useAgentsEnabled(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the agent chat panel is currently open. Core builds have no chat,
|
||||
* so this always returns false. Proprietary builds bridge to the ChatContext.
|
||||
*/
|
||||
export function useAgentChatOpen(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Inline "Agents" section rendered above the tool list in {@code ToolPicker}. */
|
||||
export function AgentsSection() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Icon-only agent button rendered in the collapsed (minimised) right rail.
|
||||
* Returns null in core; proprietary renders the Stirling agent shortcut.
|
||||
*/
|
||||
export function AgentsCollapsedButton(_props: { onExpand: () => void }) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Agents card rendered inside the fullscreen tool picker. Matches the visual
|
||||
* language of the fullscreen category cards (gradient border, title, items).
|
||||
* Returns null in core; proprietary renders the Stirling agent.
|
||||
*/
|
||||
export function AgentsFullscreenSection() {
|
||||
return null;
|
||||
}
|
||||
@@ -7,12 +7,9 @@
|
||||
export function useChat() {
|
||||
return {
|
||||
messages: [] as never[],
|
||||
isOpen: false,
|
||||
isLoading: false,
|
||||
progress: null,
|
||||
progressLog: [] as never[],
|
||||
toggleOpen: () => {},
|
||||
setOpen: (_open: boolean) => {},
|
||||
sendMessage: async (_content: string) => {},
|
||||
clearChat: () => {},
|
||||
};
|
||||
|
||||
@@ -3,11 +3,6 @@ import { useToolWorkflow } from "@app/contexts/ToolWorkflowContext";
|
||||
import { useIsMobile } from "@app/hooks/useIsMobile";
|
||||
import { usePreferences } from "@app/contexts/PreferencesContext";
|
||||
import { useWorkbenchBar } from "@app/contexts/WorkbenchBarContext";
|
||||
import {
|
||||
AgentsFullscreenSection,
|
||||
useAgentChatOpen,
|
||||
useAgentsEnabled,
|
||||
} from "@app/components/agents/AgentsPanel";
|
||||
import FullscreenToolSurface from "@app/components/tools/FullscreenToolSurface";
|
||||
import { ToolId } from "@app/types/toolId";
|
||||
import type { ToolPanelGeometry } from "@app/hooks/tools/useToolPanelGeometry";
|
||||
@@ -16,13 +11,11 @@ import type { ToolPanelGeometry } from "@app/hooks/tools/useToolPanelGeometry";
|
||||
export function useIsFullscreenExpanded(): boolean {
|
||||
const { toolPanelMode, leftPanelView, readerMode } = useToolWorkflow();
|
||||
const isMobile = useIsMobile();
|
||||
const agentChatOpen = useAgentChatOpen();
|
||||
return (
|
||||
toolPanelMode === "fullscreen" &&
|
||||
leftPanelView === "toolPicker" &&
|
||||
!isMobile &&
|
||||
!readerMode &&
|
||||
!agentChatOpen
|
||||
!readerMode
|
||||
);
|
||||
}
|
||||
|
||||
@@ -50,8 +43,6 @@ export function FullscreenToolPanel({ geometry }: FullscreenToolPanelProps) {
|
||||
handleToolSelect,
|
||||
} = useToolWorkflow();
|
||||
const isMobile = useIsMobile();
|
||||
const agentsEnabled = useAgentsEnabled();
|
||||
const agentChatOpen = useAgentChatOpen();
|
||||
const { setAllButtonsDisabled } = useWorkbenchBar();
|
||||
const { preferences, updatePreference } = usePreferences();
|
||||
|
||||
@@ -59,8 +50,7 @@ export function FullscreenToolPanel({ geometry }: FullscreenToolPanelProps) {
|
||||
toolPanelMode === "fullscreen" &&
|
||||
leftPanelView === "toolPicker" &&
|
||||
!isMobile &&
|
||||
!readerMode &&
|
||||
!agentChatOpen;
|
||||
!readerMode;
|
||||
|
||||
useEffect(() => {
|
||||
setAllButtonsDisabled(fullscreenExpanded);
|
||||
@@ -94,9 +84,6 @@ export function FullscreenToolPanel({ geometry }: FullscreenToolPanelProps) {
|
||||
}
|
||||
onExitFullscreenMode={() => setToolPanelMode("sidebar")}
|
||||
geometry={geometry}
|
||||
agentsSlot={
|
||||
agentsEnabled && !searchQuery ? <AgentsFullscreenSection /> : null
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,8 +25,6 @@ interface FullscreenToolSurfaceProps {
|
||||
onToggleDescriptions: () => void;
|
||||
onExitFullscreenMode: () => void;
|
||||
geometry: ToolPanelGeometry | null;
|
||||
/** Optional agents block rendered above the tool list. */
|
||||
agentsSlot?: React.ReactNode;
|
||||
}
|
||||
|
||||
const FullscreenToolSurface = ({
|
||||
@@ -41,7 +39,6 @@ const FullscreenToolSurface = ({
|
||||
onToggleDescriptions,
|
||||
onExitFullscreenMode: _onExitFullscreenMode,
|
||||
geometry,
|
||||
agentsSlot,
|
||||
}: FullscreenToolSurfaceProps) => {
|
||||
const { t } = useTranslation();
|
||||
const surfaceRef = useRef<HTMLDivElement>(null);
|
||||
@@ -92,9 +89,6 @@ const FullscreenToolSurface = ({
|
||||
className="tool-panel__fullscreen-scroll"
|
||||
offsetScrollbars
|
||||
>
|
||||
{agentsSlot && (
|
||||
<div className="tool-panel__fullscreen-agents">{agentsSlot}</div>
|
||||
)}
|
||||
<FullscreenToolList
|
||||
filteredTools={filteredTools}
|
||||
searchQuery={searchQuery}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMemo, useState, useRef, useCallback } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { ActionIcon } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useRainbowThemeContext } from "@app/components/shared/RainbowThemeProvider";
|
||||
@@ -8,11 +8,6 @@ import rainbowStyles from "@app/styles/rainbow.module.css";
|
||||
import { useIsMobile } from "@app/hooks/useIsMobile";
|
||||
import ToolPanel from "@app/components/tools/ToolPanel";
|
||||
import ToolSearch from "@app/components/tools/toolPicker/ToolSearch";
|
||||
import {
|
||||
AgentsCollapsedButton,
|
||||
AgentsSection,
|
||||
useAgentsEnabled,
|
||||
} from "@app/components/agents/AgentsPanel";
|
||||
import {
|
||||
PoliciesCollapsedButton,
|
||||
PoliciesSection,
|
||||
@@ -21,8 +16,6 @@ import {
|
||||
usePolicyDetailActive,
|
||||
} from "@app/components/policies/PoliciesSidebar";
|
||||
import { PolicyAutoRunController } from "@app/components/policies/PolicyAutoRunController";
|
||||
import { useChat } from "@app/components/chat/ChatContext";
|
||||
import { ChatPanel } from "@app/components/chat/ChatPanel";
|
||||
import { useFavoriteToolItems } from "@app/hooks/tools/useFavoriteToolItems";
|
||||
import { useToolSections } from "@app/hooks/useToolSections";
|
||||
import type { SubcategoryGroup } from "@app/hooks/useToolSections";
|
||||
@@ -41,16 +34,12 @@ import {
|
||||
import { useToolPanelGeometry } from "@app/hooks/tools/useToolPanelGeometry";
|
||||
import "@app/components/tools/ToolPanel.css";
|
||||
|
||||
const DEFAULT_CHAT_WIDTH_PX = 18.5 * 16; // 18.5rem in px
|
||||
const MIN_CHAT_WIDTH_PX = 240;
|
||||
const MAX_CHAT_WIDTH_PX = 720;
|
||||
|
||||
/**
|
||||
* Right-side rail wrapping the tool panel.
|
||||
*
|
||||
* Owns the rail-level concerns: collapse/expand chrome, the AGENTS top label,
|
||||
* the collapsed strip (agent button + favourite/recommended icon shortcuts),
|
||||
* and the agents chat overlay. Fullscreen takeover lives in FullscreenToolPanel.
|
||||
* Owns the rail-level concerns: collapse/expand chrome and the collapsed strip
|
||||
* (favourite/recommended icon shortcuts). Fullscreen takeover lives in
|
||||
* FullscreenToolPanel.
|
||||
*/
|
||||
export default function RightSidebar() {
|
||||
const { t } = useTranslation();
|
||||
@@ -77,7 +66,6 @@ export default function RightSidebar() {
|
||||
favoriteTools,
|
||||
} = useToolWorkflow();
|
||||
|
||||
const agentsEnabled = useAgentsEnabled();
|
||||
const policiesEnabled = usePoliciesEnabled();
|
||||
const rawPolicyDetailActive = usePolicyDetailActive();
|
||||
const fullscreenExpanded = useIsFullscreenExpanded();
|
||||
@@ -101,57 +89,6 @@ export default function RightSidebar() {
|
||||
|
||||
const [allToolsView, setAllToolsView] = useState(false);
|
||||
|
||||
const { isOpen: isChatOpen, setOpen: setChatOpen } = useChat();
|
||||
const [chatWidthPx, setChatWidthPx] = useState(DEFAULT_CHAT_WIDTH_PX);
|
||||
const [isChatDragging, setIsChatDragging] = useState(false);
|
||||
const chatDragState = useRef<{ startX: number; startWidth: number } | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const handleChatClose = useCallback(() => {
|
||||
withViewTransition(() => setChatOpen(false));
|
||||
setChatWidthPx(DEFAULT_CHAT_WIDTH_PX);
|
||||
}, [setChatOpen]);
|
||||
|
||||
const handleResizeChatPointerDown = useCallback(
|
||||
(e: React.PointerEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
chatDragState.current = { startX: e.clientX, startWidth: chatWidthPx };
|
||||
setIsChatDragging(true);
|
||||
document.body.style.cursor = "col-resize";
|
||||
document.body.style.userSelect = "none";
|
||||
|
||||
const onMove = (ev: PointerEvent) => {
|
||||
if (!chatDragState.current) return;
|
||||
const delta = chatDragState.current.startX - ev.clientX;
|
||||
setChatWidthPx(
|
||||
Math.max(
|
||||
MIN_CHAT_WIDTH_PX,
|
||||
Math.min(
|
||||
MAX_CHAT_WIDTH_PX,
|
||||
chatDragState.current.startWidth + delta,
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const cleanup = () => {
|
||||
chatDragState.current = null;
|
||||
setIsChatDragging(false);
|
||||
document.body.style.removeProperty("cursor");
|
||||
document.body.style.removeProperty("user-select");
|
||||
window.removeEventListener("pointermove", onMove);
|
||||
window.removeEventListener("pointerup", cleanup);
|
||||
window.removeEventListener("pointercancel", cleanup);
|
||||
};
|
||||
|
||||
window.addEventListener("pointermove", onMove);
|
||||
window.addEventListener("pointerup", cleanup);
|
||||
window.addEventListener("pointercancel", cleanup);
|
||||
},
|
||||
[chatWidthPx],
|
||||
);
|
||||
|
||||
const handleShowAllTools = () => {
|
||||
withViewTransition(() => setAllToolsView(true));
|
||||
};
|
||||
@@ -186,12 +123,11 @@ export default function RightSidebar() {
|
||||
policiesEnabled && !allToolsView && leftPanelView === "toolPicker";
|
||||
// When Policies are shown, the search moves OUT of the header to sit between
|
||||
// the Policies and Tools sections (separating them); otherwise it stays in the
|
||||
// header. Show the header search when there's a close button, or when agents
|
||||
// are off in the default tool-picker view (inline filtering).
|
||||
// header. Show the header search when there's a close button, or in the
|
||||
// default tool-picker view.
|
||||
const showInlineSearch = showPolicies && !showCloseButton;
|
||||
const showHeaderSearch =
|
||||
!showInlineSearch &&
|
||||
(showCloseButton || (!agentsEnabled && leftPanelView === "toolPicker"));
|
||||
!showInlineSearch && (showCloseButton || leftPanelView === "toolPicker");
|
||||
|
||||
const handleHeaderBack = () => {
|
||||
if (inToolView) {
|
||||
@@ -224,13 +160,6 @@ export default function RightSidebar() {
|
||||
? (toolRegistry[selectedToolKey as ToolId] ?? null)
|
||||
: null;
|
||||
|
||||
// Agents header + section is hidden when:
|
||||
// - the AI engine is off, or
|
||||
// - the user is in the all-tools view, or
|
||||
// - a specific tool is being rendered (leftPanelView ≠ "toolPicker").
|
||||
const showAgents =
|
||||
agentsEnabled && !allToolsView && leftPanelView === "toolPicker";
|
||||
|
||||
// The detail takeover replaces the tool list ONLY in the same default view —
|
||||
// never over an open tool or the all-tools view (which must keep priority).
|
||||
// A lingering selection is harmless: it stays hidden behind a tool and the
|
||||
@@ -243,7 +172,6 @@ export default function RightSidebar() {
|
||||
|
||||
const computedWidth = () => {
|
||||
if (isMobile) return "100%";
|
||||
if (isChatOpen) return `${chatWidthPx}px`;
|
||||
if (!isPanelVisible) return "3.5rem";
|
||||
return expandedWidth;
|
||||
};
|
||||
@@ -279,41 +207,17 @@ export default function RightSidebar() {
|
||||
ref={toolPanelRef}
|
||||
data-sidebar="tool-panel"
|
||||
data-tour={fullscreenExpanded ? undefined : "tool-panel"}
|
||||
className={`tool-panel flex flex-col ${fullscreenExpanded ? "tool-panel--fullscreen-active" : isChatOpen ? "" : "overflow-hidden"} bg-[var(--bg-toolbar)] border-l border-[var(--border-subtle)] transition-all duration-300 ease-out ${
|
||||
className={`tool-panel flex flex-col ${fullscreenExpanded ? "tool-panel--fullscreen-active" : "overflow-hidden"} bg-[var(--bg-toolbar)] border-l border-[var(--border-subtle)] transition-all duration-300 ease-out ${
|
||||
isRainbowMode ? rainbowStyles.rainbowPaper : ""
|
||||
} ${isMobile ? "h-full border-r-0" : "h-screen"} ${fullscreenExpanded ? "tool-panel--fullscreen" : ""}`}
|
||||
style={{
|
||||
width: computedWidth(),
|
||||
padding: "0",
|
||||
...(isChatDragging ? { transition: "none" } : {}),
|
||||
}}
|
||||
>
|
||||
{/* Headless: enforces enabled policies on every uploaded file. */}
|
||||
{policiesEnabled && <PolicyAutoRunController />}
|
||||
{!fullscreenExpanded && isChatOpen && (
|
||||
<div
|
||||
style={{
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="agents-takeover__resize-handle"
|
||||
onPointerDown={handleResizeChatPointerDown}
|
||||
role="separator"
|
||||
aria-label={t("chat.resize", "Resize chat panel")}
|
||||
aria-orientation="vertical"
|
||||
/>
|
||||
<ChatPanel
|
||||
onBack={handleChatClose}
|
||||
backLabel={t("agents.back_to_tools", "Back to tools")}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!fullscreenExpanded && !isChatOpen && !isPanelVisible && !isMobile && (
|
||||
{!fullscreenExpanded && !isPanelVisible && !isMobile && (
|
||||
<div className="tool-panel__collapsed-strip">
|
||||
<div className="tool-panel__collapsed-top">
|
||||
<ActionIcon
|
||||
@@ -327,7 +231,6 @@ export default function RightSidebar() {
|
||||
>
|
||||
<ChevronLeftIcon sx={{ fontSize: "1.1rem" }} />
|
||||
</ActionIcon>
|
||||
<AgentsCollapsedButton onExpand={handleExpand} />
|
||||
</div>
|
||||
<div className="tool-panel__collapsed-divider" />
|
||||
<PoliciesCollapsedButton onExpand={handleOpenPolicy} />
|
||||
@@ -358,7 +261,7 @@ export default function RightSidebar() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!fullscreenExpanded && !isChatOpen && isPanelVisible && (
|
||||
{!fullscreenExpanded && isPanelVisible && (
|
||||
<div
|
||||
/* Fixed width matches the expanded panel width so the inner content is
|
||||
laid out at its final size from the moment it mounts. The outer
|
||||
@@ -409,13 +312,7 @@ export default function RightSidebar() {
|
||||
autoFocus={allToolsView && !inToolView}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
showAgents && (
|
||||
<span className="tool-panel__section-label">
|
||||
{t("agents.section_title", "Agents")}
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
) : null}
|
||||
{showCloseButton ? (
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
@@ -447,8 +344,6 @@ export default function RightSidebar() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showAgents && <AgentsSection />}
|
||||
|
||||
{showPolicies && (
|
||||
<PoliciesSection
|
||||
leadingControl={
|
||||
@@ -481,7 +376,7 @@ export default function RightSidebar() {
|
||||
allToolsView={allToolsView}
|
||||
onShowAllTools={handleShowAllTools}
|
||||
onToolSelect={handleToolSelectWithTransition}
|
||||
compact={agentsEnabled && !allToolsView}
|
||||
compact={false}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -385,15 +385,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.tool-panel__section-label {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--text-muted);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.tool-panel__mode-toggle {
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
@@ -855,33 +846,6 @@
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
/* Agents card lives in its own column above the tool grid. It uses the same
|
||||
.tool-panel__fullscreen-group base styles but with a colourful gradient
|
||||
border so it stands out as a distinct surface (and doesn't blend into the
|
||||
neighbouring category cards). */
|
||||
.tool-panel__fullscreen-agents {
|
||||
padding: 1.5rem 1.75rem 0;
|
||||
}
|
||||
|
||||
.tool-panel__fullscreen-group--agents {
|
||||
position: relative;
|
||||
background:
|
||||
linear-gradient(var(--fullscreen-bg-group), var(--fullscreen-bg-group))
|
||||
padding-box,
|
||||
linear-gradient(
|
||||
135deg,
|
||||
var(--mantine-color-blue-6) 0%,
|
||||
var(--mantine-color-violet-6) 45%,
|
||||
var(--mantine-color-pink-6) 100%
|
||||
)
|
||||
border-box;
|
||||
border: 1.5px solid transparent;
|
||||
}
|
||||
|
||||
.tool-panel__fullscreen-section-icon--agents {
|
||||
color: var(--mantine-color-blue-6);
|
||||
}
|
||||
|
||||
@keyframes tool-panel-fullscreen-slide-in {
|
||||
from {
|
||||
transform: translateX(6%) scaleX(0.85);
|
||||
|
||||
@@ -1,483 +0,0 @@
|
||||
/* ─── Chat takeover ─────────────────────────────────────────────────────── */
|
||||
|
||||
.agents-takeover {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bg-toolbar);
|
||||
border-left: 1px solid var(--border-subtle);
|
||||
view-transition-name: agents-rail;
|
||||
}
|
||||
|
||||
.agents-takeover__resize-handle {
|
||||
position: absolute;
|
||||
left: -3px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 7px;
|
||||
cursor: col-resize;
|
||||
z-index: 10;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.agents-takeover__resize-handle::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 3px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
background: transparent;
|
||||
transition:
|
||||
background 150ms ease,
|
||||
width 150ms ease,
|
||||
left 150ms ease;
|
||||
}
|
||||
|
||||
.agents-takeover__resize-handle:hover::after {
|
||||
left: 2px;
|
||||
width: 3px;
|
||||
background: var(--mantine-color-blue-4);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* ─── Sidebar agents section ─────────────────────────────────────────────── */
|
||||
|
||||
.agents-section {
|
||||
padding: 0.5rem 0.75rem 0.875rem;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
background: var(--bg-toolbar);
|
||||
flex-shrink: 0;
|
||||
view-transition-name: agents-rail;
|
||||
}
|
||||
|
||||
/* Hero button — real Stirling agent */
|
||||
.agent-button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid
|
||||
color-mix(in srgb, var(--mantine-color-blue-5) 40%, var(--border-subtle));
|
||||
border-radius: 0.625rem;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
color-mix(
|
||||
in srgb,
|
||||
var(--mantine-color-blue-6) 6%,
|
||||
var(--mantine-color-body)
|
||||
)
|
||||
0%,
|
||||
var(--mantine-color-body) 100%
|
||||
);
|
||||
transition:
|
||||
background 150ms ease-out,
|
||||
border-color 150ms ease-out;
|
||||
}
|
||||
|
||||
.agent-button:hover {
|
||||
border-color: color-mix(
|
||||
in srgb,
|
||||
var(--mantine-color-blue-5) 65%,
|
||||
var(--border-subtle)
|
||||
);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
color-mix(
|
||||
in srgb,
|
||||
var(--mantine-color-blue-6) 12%,
|
||||
var(--mantine-color-default-hover)
|
||||
)
|
||||
0%,
|
||||
var(--mantine-color-default-hover) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.agent-button__logo {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--mantine-color-blue-filled);
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Coming-soon agent rows */
|
||||
.agents-sidebar-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.375rem;
|
||||
margin-top: 0.375rem;
|
||||
}
|
||||
|
||||
.agent-button--coming-soon {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: 0.625rem;
|
||||
background: var(--mantine-color-body);
|
||||
cursor: not-allowed !important;
|
||||
opacity: 0.65;
|
||||
transition:
|
||||
opacity 180ms ease-out,
|
||||
transform 180ms ease-out,
|
||||
border-color 120ms ease-out;
|
||||
}
|
||||
|
||||
@starting-style {
|
||||
.agent-button--coming-soon {
|
||||
opacity: 0;
|
||||
transform: translateY(5px);
|
||||
}
|
||||
}
|
||||
|
||||
.agent-button--coming-soon:hover {
|
||||
opacity: 0.85;
|
||||
border-color: color-mix(
|
||||
in srgb,
|
||||
var(--mantine-color-gray-4) 60%,
|
||||
var(--border-subtle)
|
||||
);
|
||||
background: var(--mantine-color-default-hover);
|
||||
}
|
||||
|
||||
.agent-button__icon-plain {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
color: var(--mantine-color-dimmed);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.agents-view-all {
|
||||
all: unset;
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-top: 0.375rem;
|
||||
padding: 0.25rem 0.625rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
color: var(--mantine-color-dimmed);
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
opacity: 0.7;
|
||||
border-radius: 0.375rem;
|
||||
transition: opacity 120ms ease-out;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.agents-view-all:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Dark mode — sidebar */
|
||||
[data-mantine-color-scheme="dark"] .agent-button--coming-soon {
|
||||
background: transparent;
|
||||
border-color: color-mix(
|
||||
in srgb,
|
||||
var(--mantine-color-dark-4) 60%,
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
[data-mantine-color-scheme="dark"] .agent-button--coming-soon:hover {
|
||||
border-color: var(--mantine-color-dark-3);
|
||||
background: var(--mantine-color-dark-6);
|
||||
}
|
||||
|
||||
[data-mantine-color-scheme="dark"] .agent-button {
|
||||
border-color: color-mix(
|
||||
in srgb,
|
||||
var(--mantine-color-blue-7) 55%,
|
||||
var(--border-subtle)
|
||||
);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
color-mix(in srgb, var(--mantine-color-blue-9) 35%, transparent) 0%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
[data-mantine-color-scheme="dark"] .agent-button:hover {
|
||||
border-color: color-mix(
|
||||
in srgb,
|
||||
var(--mantine-color-blue-5) 60%,
|
||||
var(--border-subtle)
|
||||
);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
color-mix(
|
||||
in srgb,
|
||||
var(--mantine-color-blue-8) 45%,
|
||||
rgba(255, 255, 255, 0.04)
|
||||
)
|
||||
0%,
|
||||
rgba(255, 255, 255, 0.04) 100%
|
||||
);
|
||||
}
|
||||
|
||||
[data-mantine-color-scheme="dark"] .agent-button__logo {
|
||||
color: var(--mantine-color-blue-3, var(--mantine-color-blue-filled));
|
||||
}
|
||||
|
||||
/* ─── Collapsed-rail agent button ────────────────────────────────────────── */
|
||||
|
||||
.agents-collapsed-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
background: transparent;
|
||||
color: var(--mantine-color-blue-filled);
|
||||
cursor: pointer;
|
||||
transition: background 120ms ease-out;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.agents-collapsed-btn:hover {
|
||||
background: var(--mantine-color-default-hover);
|
||||
}
|
||||
|
||||
/* ─── Running / in-progress status dot ──────────────────────────────────── */
|
||||
|
||||
.agent-status-dot {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--mantine-color-blue-5);
|
||||
border: 1.5px solid var(--mantine-color-body);
|
||||
animation: agent-dot-pulse 2.4s ease-in-out infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@keyframes agent-dot-pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.45;
|
||||
}
|
||||
}
|
||||
|
||||
/* Blue outline on hero Stirling button when agent is running */
|
||||
.agent-button.agent-button--running {
|
||||
border-color: color-mix(
|
||||
in srgb,
|
||||
var(--mantine-color-blue-5) 60%,
|
||||
var(--border-subtle)
|
||||
);
|
||||
}
|
||||
|
||||
[data-mantine-color-scheme="dark"] .agent-button.agent-button--running {
|
||||
border-color: color-mix(
|
||||
in srgb,
|
||||
var(--mantine-color-blue-4) 55%,
|
||||
var(--border-subtle)
|
||||
);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.agent-status-dot {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Fullscreen hero card ───────────────────────────────────────────────── */
|
||||
|
||||
/*
|
||||
* .tool-panel__fullscreen-group--agents gives the gradient border via the
|
||||
* padding-box / border-box trick. We only override the padding-box with a
|
||||
* very light tint so the card doesn't read as a colourful surface.
|
||||
*/
|
||||
.agents-hero {
|
||||
padding: 1.25rem 1.5rem 1.5rem;
|
||||
border-radius: 1rem;
|
||||
background:
|
||||
linear-gradient(
|
||||
125deg,
|
||||
color-mix(in srgb, var(--mantine-color-blue-1) 35%, white) 0%,
|
||||
color-mix(in srgb, var(--mantine-color-violet-1) 45%, white) 50%,
|
||||
color-mix(in srgb, var(--mantine-color-pink-0) 35%, white) 100%
|
||||
)
|
||||
padding-box,
|
||||
linear-gradient(
|
||||
135deg,
|
||||
var(--mantine-color-blue-6) 0%,
|
||||
var(--mantine-color-violet-6) 45%,
|
||||
var(--mantine-color-pink-6) 100%
|
||||
)
|
||||
border-box;
|
||||
}
|
||||
|
||||
/* Body: left CTA + right grid */
|
||||
.agents-hero__body {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
/* ── Left CTA — plain content, only the button inside is interactive ── */
|
||||
.agents-hero__cta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
flex: 0 0 18rem;
|
||||
box-sizing: border-box;
|
||||
padding: 0.25rem 0;
|
||||
}
|
||||
|
||||
.agents-hero__cta-logo {
|
||||
color: var(--mantine-color-blue-filled);
|
||||
margin-bottom: 0.625rem;
|
||||
}
|
||||
|
||||
.agents-hero__cta-headline {
|
||||
font-size: 1.375rem;
|
||||
}
|
||||
|
||||
.agents-hero__cta-btn {
|
||||
all: unset;
|
||||
margin-top: 1.25rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.4375rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
background: var(--mantine-color-blue-6);
|
||||
color: white;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
width: fit-content;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.agents-hero__cta-btn:hover {
|
||||
background: var(--mantine-color-blue-7);
|
||||
}
|
||||
|
||||
.agents-hero__cta-btn:focus-visible {
|
||||
outline: 2px solid var(--mantine-color-blue-5);
|
||||
outline-offset: 3px;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
/* ── Right 2×3 grid — sizes to content, pushed to the right ── */
|
||||
.agents-hero__grid {
|
||||
flex: 0 0 auto;
|
||||
margin-left: auto;
|
||||
display: grid;
|
||||
/* auto columns: each column = widest item in that column */
|
||||
grid-template-columns: repeat(2, auto);
|
||||
gap: 0.625rem;
|
||||
align-content: start;
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
/* Solid white cards — no opacity so the white is opaque */
|
||||
.agents-hero__grid-item {
|
||||
all: unset;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 0.875rem;
|
||||
border: 1px solid rgba(0, 0, 0, 0.07);
|
||||
border-radius: 0.75rem;
|
||||
background: var(--mantine-color-body);
|
||||
cursor: not-allowed;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.agents-hero__grid-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
color: var(--mantine-color-blue-5);
|
||||
}
|
||||
|
||||
.agents-hero__grid-body {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* ── Dark mode — fullscreen ── */
|
||||
[data-mantine-color-scheme="dark"] .agents-hero {
|
||||
background:
|
||||
linear-gradient(
|
||||
125deg,
|
||||
color-mix(in srgb, var(--mantine-color-blue-9) 55%, #1a1b2e) 0%,
|
||||
color-mix(in srgb, var(--mantine-color-violet-9) 45%, #1a1b2e) 50%,
|
||||
color-mix(in srgb, var(--mantine-color-pink-9) 40%, #1a1b2e) 100%
|
||||
)
|
||||
padding-box,
|
||||
linear-gradient(
|
||||
135deg,
|
||||
var(--mantine-color-blue-6) 0%,
|
||||
var(--mantine-color-violet-6) 45%,
|
||||
var(--mantine-color-pink-6) 100%
|
||||
)
|
||||
border-box;
|
||||
}
|
||||
|
||||
[data-mantine-color-scheme="dark"] .agents-hero__cta-logo {
|
||||
color: var(--mantine-color-blue-3);
|
||||
}
|
||||
|
||||
[data-mantine-color-scheme="dark"] .agents-hero__grid-item {
|
||||
background: var(--fullscreen-bg-item);
|
||||
border-color: var(--fullscreen-border-subtle-70);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
[data-mantine-color-scheme="dark"] .agents-hero__grid-icon {
|
||||
color: var(--mantine-color-blue-3);
|
||||
}
|
||||
|
||||
/* ─── agents-rail view transition — slide up ─────────────────────────────── */
|
||||
|
||||
::view-transition-old(agents-rail) {
|
||||
animation: vt-agents-out 150ms ease-out forwards;
|
||||
}
|
||||
|
||||
::view-transition-new(agents-rail) {
|
||||
animation: vt-agents-in 260ms cubic-bezier(0.22, 0.61, 0.36, 1) forwards;
|
||||
}
|
||||
|
||||
@keyframes vt-agents-out {
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes vt-agents-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
::view-transition-old(agents-rail),
|
||||
::view-transition-new(agents-rail) {
|
||||
animation-duration: 1ms !important;
|
||||
}
|
||||
}
|
||||
@@ -1,275 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import type { ElementType } from "react";
|
||||
import { Box, Group, Text, UnstyledButton } from "@mantine/core";
|
||||
import TableChartRoundedIcon from "@mui/icons-material/TableChartRounded";
|
||||
import SummarizeRoundedIcon from "@mui/icons-material/SummarizeRounded";
|
||||
import VisibilityOffRoundedIcon from "@mui/icons-material/VisibilityOffRounded";
|
||||
import GavelRoundedIcon from "@mui/icons-material/GavelRounded";
|
||||
import AssignmentRoundedIcon from "@mui/icons-material/AssignmentRounded";
|
||||
import CodeRoundedIcon from "@mui/icons-material/CodeRounded";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAppConfig } from "@app/contexts/AppConfigContext";
|
||||
import { useChat } from "@app/components/chat/ChatContext";
|
||||
import { Tooltip as AppTooltip } from "@app/components/shared/Tooltip";
|
||||
import { StirlingLogoOutline } from "@app/components/agents/StirlingLogoOutline";
|
||||
import { withViewTransition } from "@app/utils/viewTransition";
|
||||
import "@app/components/agents/AgentsPanel.css";
|
||||
|
||||
interface ComingSoonAgent {
|
||||
id: string;
|
||||
nameKey: string;
|
||||
descriptionKey: string;
|
||||
Icon: ElementType;
|
||||
}
|
||||
|
||||
const COMING_SOON_AGENTS: ComingSoonAgent[] = [
|
||||
{
|
||||
id: "data-extraction",
|
||||
nameKey: "agents.data_extraction_name",
|
||||
descriptionKey: "agents.data_extraction_description",
|
||||
Icon: TableChartRoundedIcon,
|
||||
},
|
||||
{
|
||||
id: "doc-summary",
|
||||
nameKey: "agents.doc_summary_name",
|
||||
descriptionKey: "agents.doc_summary_description",
|
||||
Icon: SummarizeRoundedIcon,
|
||||
},
|
||||
{
|
||||
id: "auto-redaction",
|
||||
nameKey: "agents.auto_redaction_name",
|
||||
descriptionKey: "agents.auto_redaction_description",
|
||||
Icon: VisibilityOffRoundedIcon,
|
||||
},
|
||||
{
|
||||
id: "compliance",
|
||||
nameKey: "agents.compliance_name",
|
||||
descriptionKey: "agents.compliance_description",
|
||||
Icon: GavelRoundedIcon,
|
||||
},
|
||||
{
|
||||
id: "form-filler",
|
||||
nameKey: "agents.form_filler_name",
|
||||
descriptionKey: "agents.form_filler_description",
|
||||
Icon: AssignmentRoundedIcon,
|
||||
},
|
||||
{
|
||||
id: "pdf-to-markdown",
|
||||
nameKey: "agents.pdf_to_markdown_name",
|
||||
descriptionKey: "agents.pdf_to_markdown_description",
|
||||
Icon: CodeRoundedIcon,
|
||||
},
|
||||
];
|
||||
|
||||
export function useAgentsEnabled(): boolean {
|
||||
const { config } = useAppConfig();
|
||||
return Boolean(config?.aiEngineEnabled);
|
||||
}
|
||||
|
||||
export function useAgentChatOpen(): boolean {
|
||||
const { isOpen } = useChat();
|
||||
return isOpen;
|
||||
}
|
||||
|
||||
const PREVIEW_COUNT = 3;
|
||||
|
||||
/** Sidebar agents section — Stirling as hero CTA, coming-soon agents below. */
|
||||
export function AgentsSection() {
|
||||
const { t } = useTranslation();
|
||||
const { isOpen, setOpen, isLoading } = useChat();
|
||||
const enabled = useAgentsEnabled();
|
||||
const [showAll, setShowAll] = useState(false);
|
||||
|
||||
if (!enabled || isOpen) return null;
|
||||
|
||||
const comingSoonLabel = t("agents.coming_soon", "Coming soon");
|
||||
const visibleAgents = showAll
|
||||
? COMING_SOON_AGENTS
|
||||
: COMING_SOON_AGENTS.slice(0, PREVIEW_COUNT);
|
||||
|
||||
return (
|
||||
<Box className="agents-section" w="100%">
|
||||
{/* Main Stirling agent — real and clickable */}
|
||||
<UnstyledButton
|
||||
className={`agent-button agent-button--hero${isLoading ? " agent-button--running" : ""}`}
|
||||
onClick={() => withViewTransition(() => setOpen(true))}
|
||||
aria-label={t("agents.stirling_name", "Stirling")}
|
||||
>
|
||||
<Group gap="sm" wrap="nowrap" align="center">
|
||||
<Box className="agent-button__logo">
|
||||
<StirlingLogoOutline size={28} />
|
||||
{isLoading && <span className="agent-status-dot" />}
|
||||
</Box>
|
||||
<Box style={{ minWidth: 0, flex: 1 }}>
|
||||
<Text size="sm" fw={600} truncate>
|
||||
{t("agents.stirling_name", "Stirling")}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" truncate>
|
||||
{isLoading
|
||||
? t("agents.stirling_running", "Running...")
|
||||
: t(
|
||||
"agents.stirling_description",
|
||||
"Your general-purpose PDF assistant",
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
|
||||
{/* Coming-soon agents */}
|
||||
<div className="agents-sidebar-list">
|
||||
{visibleAgents.map(({ id, nameKey, descriptionKey, Icon }) => (
|
||||
<AppTooltip
|
||||
key={id}
|
||||
content={comingSoonLabel}
|
||||
position="left"
|
||||
arrow
|
||||
delay={0}
|
||||
>
|
||||
<UnstyledButton
|
||||
className="agent-button agent-button--coming-soon"
|
||||
aria-disabled="true"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<Group gap="sm" wrap="nowrap" align="center">
|
||||
<Box className="agent-button__icon-plain">
|
||||
<Icon sx={{ fontSize: "1.1rem" }} />
|
||||
</Box>
|
||||
<Box style={{ minWidth: 0, flex: 1 }}>
|
||||
<Text size="sm" fw={500} truncate>
|
||||
{t(nameKey)}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" truncate>
|
||||
{t(descriptionKey)}
|
||||
</Text>
|
||||
</Box>
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
</AppTooltip>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{!showAll ? (
|
||||
<button
|
||||
type="button"
|
||||
className="agents-view-all"
|
||||
onClick={() => withViewTransition(() => setShowAll(true))}
|
||||
>
|
||||
{t("agents.view_all", "View all agents")} →
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="agents-view-all"
|
||||
onClick={() => withViewTransition(() => setShowAll(false))}
|
||||
>
|
||||
{t("agents.show_less", "Show less")} ↑
|
||||
</button>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
/** Icon-only agent button in the collapsed (minimised) right rail. */
|
||||
export function AgentsCollapsedButton({ onExpand }: { onExpand: () => void }) {
|
||||
const { t } = useTranslation();
|
||||
const { setOpen, isLoading } = useChat();
|
||||
const enabled = useAgentsEnabled();
|
||||
|
||||
if (!enabled) return null;
|
||||
|
||||
const label = t("agents.stirling_tooltip", "Stirling agent");
|
||||
|
||||
return (
|
||||
<AppTooltip content={label} position="left" arrow delay={300}>
|
||||
<UnstyledButton
|
||||
onClick={() => {
|
||||
onExpand();
|
||||
setOpen(true);
|
||||
}}
|
||||
aria-label={label}
|
||||
className="agents-collapsed-btn"
|
||||
>
|
||||
<StirlingLogoOutline size={22} />
|
||||
{isLoading && <span className="agent-status-dot" />}
|
||||
</UnstyledButton>
|
||||
</AppTooltip>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fullscreen hero card — Stirling CTA on the left, 2×3 coming-soon grid on
|
||||
* the right. Matches the gradient border of the other fullscreen category cards.
|
||||
*/
|
||||
export function AgentsFullscreenSection() {
|
||||
const { t } = useTranslation();
|
||||
const { isOpen, setOpen } = useChat();
|
||||
const enabled = useAgentsEnabled();
|
||||
|
||||
if (!enabled || isOpen) return null;
|
||||
|
||||
const comingSoonLabel = t("agents.coming_soon", "Coming soon");
|
||||
|
||||
return (
|
||||
<section
|
||||
className="agents-hero tool-panel__fullscreen-group--agents"
|
||||
aria-label={t("agents.section_title", "Agents")}
|
||||
>
|
||||
<div className="agents-hero__body">
|
||||
{/* Left: Stirling content — only the button is interactive */}
|
||||
<div className="agents-hero__cta">
|
||||
<div className="agents-hero__cta-logo">
|
||||
<StirlingLogoOutline size={36} />
|
||||
</div>
|
||||
<Text className="agents-hero__cta-headline" fw={700} lh={1.2} mt="xs">
|
||||
{t("agents.stirling_full_name", "Stirling General Agent")}
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" mt={8} lh={1.55}>
|
||||
{t(
|
||||
"agents.stirling_long_description",
|
||||
"General purpose PDF assistant that can run tools, create PDFs and extract insights from your documents.",
|
||||
)}
|
||||
</Text>
|
||||
<button
|
||||
type="button"
|
||||
className="agents-hero__cta-btn"
|
||||
onClick={() => withViewTransition(() => setOpen(true))}
|
||||
>
|
||||
{t("agents.start_chat", "Start chatting")} →
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Right: 2×3 grid of coming-soon agents */}
|
||||
<div className="agents-hero__grid">
|
||||
{COMING_SOON_AGENTS.map(({ id, nameKey, descriptionKey, Icon }) => (
|
||||
<AppTooltip
|
||||
key={id}
|
||||
content={comingSoonLabel}
|
||||
position="top"
|
||||
arrow
|
||||
delay={0}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="agents-hero__grid-item"
|
||||
aria-disabled="true"
|
||||
>
|
||||
<span className="agents-hero__grid-icon">
|
||||
<Icon sx={{ fontSize: "1rem" }} />
|
||||
</span>
|
||||
<div className="agents-hero__grid-body">
|
||||
<Text size="sm" fw={500} truncate>
|
||||
{t(nameKey)}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" truncate>
|
||||
{t(descriptionKey)}
|
||||
</Text>
|
||||
</div>
|
||||
</button>
|
||||
</AppTooltip>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -183,7 +183,6 @@ interface AiWorkflowResponse {
|
||||
|
||||
interface ChatState {
|
||||
messages: ChatMessage[];
|
||||
isOpen: boolean;
|
||||
isLoading: boolean;
|
||||
progress: AiWorkflowProgress | null;
|
||||
/** Ordered log of every progress event in the current request. UI shows the last N entries. */
|
||||
@@ -200,8 +199,6 @@ type ChatAction =
|
||||
| { type: "SET_LOADING"; loading: boolean }
|
||||
| { type: "SET_PROGRESS"; progress: AiWorkflowProgress | null }
|
||||
| { type: "APPEND_PROGRESS"; progress: AiWorkflowProgress }
|
||||
| { type: "TOGGLE_OPEN" }
|
||||
| { type: "SET_OPEN"; open: boolean }
|
||||
| { type: "CLEAR" };
|
||||
|
||||
function chatReducer(state: ChatState, action: ChatAction): ChatState {
|
||||
@@ -231,10 +228,6 @@ function chatReducer(state: ChatState, action: ChatAction): ChatState {
|
||||
action.progress,
|
||||
],
|
||||
};
|
||||
case "TOGGLE_OPEN":
|
||||
return { ...state, isOpen: !state.isOpen };
|
||||
case "SET_OPEN":
|
||||
return { ...state, isOpen: action.open };
|
||||
case "CLEAR":
|
||||
return {
|
||||
...state,
|
||||
@@ -363,13 +356,10 @@ async function consumeSSEStream(
|
||||
|
||||
interface ChatContextValue {
|
||||
messages: ChatMessage[];
|
||||
isOpen: boolean;
|
||||
isLoading: boolean;
|
||||
progress: AiWorkflowProgress | null;
|
||||
/** Ordered log of every progress event for the current in-flight request. */
|
||||
progressLog: AiWorkflowProgress[];
|
||||
toggleOpen: () => void;
|
||||
setOpen: (open: boolean) => void;
|
||||
sendMessage: (content: string) => Promise<void>;
|
||||
/** Abort any in-flight request and reset the chat to an empty conversation. */
|
||||
clearChat: () => void;
|
||||
@@ -379,7 +369,6 @@ const ChatContext = createContext<ChatContextValue | null>(null);
|
||||
|
||||
const initialState: ChatState = {
|
||||
messages: [],
|
||||
isOpen: false,
|
||||
isLoading: false,
|
||||
progress: null,
|
||||
progressLog: [],
|
||||
@@ -465,11 +454,6 @@ export function ChatProvider({ children }: { children: ReactNode }) {
|
||||
[fileActions, downloadFile],
|
||||
);
|
||||
|
||||
const toggleOpen = useCallback(() => dispatch({ type: "TOGGLE_OPEN" }), []);
|
||||
const setOpen = useCallback(
|
||||
(open: boolean) => dispatch({ type: "SET_OPEN", open }),
|
||||
[],
|
||||
);
|
||||
const clearChat = useCallback(() => {
|
||||
abortRef.current?.abort();
|
||||
abortRef.current = null;
|
||||
@@ -649,12 +633,9 @@ export function ChatProvider({ children }: { children: ReactNode }) {
|
||||
<ChatContext.Provider
|
||||
value={{
|
||||
messages: state.messages,
|
||||
isOpen: state.isOpen,
|
||||
isLoading: state.isLoading,
|
||||
progress: state.progress,
|
||||
progressLog: state.progressLog,
|
||||
toggleOpen,
|
||||
setOpen,
|
||||
sendMessage,
|
||||
clearChat,
|
||||
}}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { ChatFABButton } from "@shared/components/ChatFABButton";
|
||||
import { ChatFABWindow } from "@shared/components/ChatFABWindow";
|
||||
import { ChatPanel } from "@app/components/chat/ChatPanel";
|
||||
import { useChat } from "@app/components/chat/ChatContext";
|
||||
import { useAgentsEnabled } from "@app/components/agents/AgentsPanel";
|
||||
import { useAppConfig } from "@app/contexts/AppConfigContext";
|
||||
import { Z_INDEX_CHAT_FAB_OVERLAY } from "@app/styles/zIndex";
|
||||
import "@app/components/chat/ChatFAB.css";
|
||||
|
||||
@@ -54,7 +54,8 @@ export function ChatFAB() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [hasUnviewedResult, setHasUnviewedResult] = useState(false);
|
||||
const { isLoading } = useChat();
|
||||
const enabled = useAgentsEnabled();
|
||||
const { config } = useAppConfig();
|
||||
const enabled = Boolean(config?.aiEngineEnabled);
|
||||
|
||||
// Detect loading → done transition. If the FAB is closed when the agent
|
||||
// finishes, show the tick badge until the user opens the panel.
|
||||
@@ -90,9 +91,13 @@ export function ChatFAB() {
|
||||
};
|
||||
|
||||
useLayoutEffect(() => {
|
||||
// The overlay only mounts once the AI engine is enabled (config can load
|
||||
// after first render), so re-measure when that flips true rather than only
|
||||
// on initial mount, otherwise the default position never gets computed.
|
||||
if (!enabled) return;
|
||||
const pos = getDefaultPos();
|
||||
if (pos) setRndPos(pos);
|
||||
}, []);
|
||||
}, [enabled]);
|
||||
|
||||
// Clear the reset timer on unmount to avoid state updates on dead components.
|
||||
// Also ensure body user-select is restored if we unmount mid-resize.
|
||||
@@ -140,6 +145,12 @@ export function ChatFAB() {
|
||||
<ChatFABButton
|
||||
className={`chat-fab-trigger${isOpen ? " chat-fab-trigger--hidden" : ""}`}
|
||||
onClick={() => {
|
||||
// Fallback: ensure a position exists before opening, in case the
|
||||
// layout effect measured before the overlay was laid out.
|
||||
if (rndPos === null) {
|
||||
const pos = getDefaultPos();
|
||||
if (pos) setRndPos(pos);
|
||||
}
|
||||
setIsOpen(true);
|
||||
setHasUnviewedResult(false);
|
||||
}}
|
||||
|
||||
@@ -35,7 +35,6 @@
|
||||
transition:
|
||||
background 120ms ease-out,
|
||||
border-color 120ms ease-out;
|
||||
/* Pair with .agent-button to morph between the card and the pill. */
|
||||
view-transition-name: stirling-agent;
|
||||
}
|
||||
|
||||
@@ -64,6 +63,36 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Running / in-progress status dot, anchored to the agent pill icon. */
|
||||
.agent-status-dot {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--mantine-color-blue-5);
|
||||
border: 1.5px solid var(--mantine-color-body);
|
||||
animation: agent-dot-pulse 2.4s ease-in-out infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@keyframes agent-dot-pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.45;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.agent-status-dot {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-panel__agent-pill--loading {
|
||||
border-color: color-mix(
|
||||
in srgb,
|
||||
|
||||
@@ -242,11 +242,6 @@ export default defineConfig(async ({ mode }) => {
|
||||
},
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"@shared": path.resolve(__dirname, "../shared"),
|
||||
},
|
||||
},
|
||||
optimizeDeps: {
|
||||
exclude: ["@embedpdf/pdfium"],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user