mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 11:23:10 +02:00
Improvements to agent chat markdown rendering. (#6507)
### To test
- Ask the agent to “list all the things you can do and put them in a
markdown table”. I know we’re explicitly asking it for markdown, but I
don’t want to update the system prompt to ask it to make tables when
necessary because it’ll probably turn everything into a table, not sure
though, we can test in future.
- Notice how the loading is different
- Notice how the user chat is in a bubble but the agent chat is flat
(super standard design practice in AI tools, and looks much better when
the agent outputs mardown, expecially tables and needs room to do so)
- Ask it to do something different, then close the chat, and see that
the agent is marked as running and has a green outline and a green dot.
- Play around with resizing the chat to make it bigger/smaller
Open to any and all criticisms on any of the design choices, and of
course the usual, code etc.
Resizing
<img width="1572" height="812" alt="Screenshot 2026-06-01 at 2 47 53 PM"
src="https://github.com/user-attachments/assets/ec0ac1d0-01da-4025-bf7e-eea4eb544181"
/>
Loading (cool animation not visible through screenshot obviously)
<img width="559" height="141" alt="Screenshot 2026-06-01 at 2 53 41 PM"
src="https://github.com/user-attachments/assets/99f0b1f5-1719-4d78-8947-21b142293052"
/>
Removed bubbles for agent chat (maybe controversial, let me know) and
markdown now renders properly again
<img width="654" height="1060" alt="Screenshot 2026-06-01 at 2 55 01 PM"
src="https://github.com/user-attachments/assets/445f0889-a632-4751-9a16-f80ae388c632"
/>
This commit is contained in:
@@ -13,10 +13,8 @@ export function useAgentsEnabled(): boolean {
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the agent chat overlay is currently open. Core builds have no chat,
|
||||
* Whether the agent chat panel is currently open. Core builds have no chat,
|
||||
* so this always returns false. Proprietary builds bridge to the ChatContext.
|
||||
* Used by {@code RightSidebar} so the fullscreen tool picker can yield to the
|
||||
* chat overlay just like it yields to a selected tool.
|
||||
*/
|
||||
export function useAgentChatOpen(): boolean {
|
||||
return false;
|
||||
@@ -35,14 +33,6 @@ export function AgentsCollapsedButton(_props: { onExpand: () => void }) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Full-rail chat overlay rendered inside {@code ToolPanel}. Covers the panel
|
||||
* (including the search bar) when an agent conversation is active.
|
||||
*/
|
||||
export function AgentsChatOverlay() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Agents card rendered inside the fullscreen tool picker. Matches the visual
|
||||
* language of the fullscreen category cards (gradient border, title, items).
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Core stub for the chat context.
|
||||
* The real implementation lives in proprietary/components/chat/ChatContext.tsx
|
||||
* and shadows this via the @app/* alias cascade in proprietary builds.
|
||||
*/
|
||||
|
||||
export function useChat() {
|
||||
return {
|
||||
messages: [] as never[],
|
||||
isOpen: false,
|
||||
isLoading: false,
|
||||
progress: null,
|
||||
toggleOpen: () => {},
|
||||
setOpen: (_open: boolean) => {},
|
||||
sendMessage: async (_content: string) => {},
|
||||
clearChat: () => {},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Core stub for the chat panel.
|
||||
* The real implementation lives in proprietary/components/chat/ChatPanel.tsx
|
||||
* and shadows this via the @app/* alias cascade in proprietary builds.
|
||||
*/
|
||||
|
||||
export interface ChatPanelProps {
|
||||
onBack: () => void;
|
||||
backLabel: string;
|
||||
}
|
||||
|
||||
export function ChatPanel(_props: ChatPanelProps) {
|
||||
return null;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { useMemo, useState, useRef, useCallback } from "react";
|
||||
import { ActionIcon } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useRainbowThemeContext } from "@app/components/shared/RainbowThemeProvider";
|
||||
@@ -9,11 +9,12 @@ import { useIsMobile } from "@app/hooks/useIsMobile";
|
||||
import ToolPanel from "@app/components/tools/ToolPanel";
|
||||
import ToolSearch from "@app/components/tools/toolPicker/ToolSearch";
|
||||
import {
|
||||
AgentsChatOverlay,
|
||||
AgentsCollapsedButton,
|
||||
AgentsSection,
|
||||
useAgentsEnabled,
|
||||
} from "@app/components/agents/AgentsPanel";
|
||||
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";
|
||||
@@ -32,6 +33,10 @@ 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.
|
||||
*
|
||||
@@ -86,6 +91,57 @@ 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));
|
||||
};
|
||||
@@ -147,6 +203,7 @@ export default function RightSidebar() {
|
||||
|
||||
const computedWidth = () => {
|
||||
if (isMobile) return "100%";
|
||||
if (isChatOpen) return `${chatWidthPx}px`;
|
||||
if (!isPanelVisible) return "3.5rem";
|
||||
return "18.5rem";
|
||||
};
|
||||
@@ -182,15 +239,39 @@ 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" : "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" : isChatOpen ? "" : "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" } : {}),
|
||||
}}
|
||||
>
|
||||
{!fullscreenExpanded && !isPanelVisible && !isMobile && (
|
||||
{!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 && (
|
||||
<div className="tool-panel__collapsed-strip">
|
||||
<div className="tool-panel__collapsed-top">
|
||||
<ActionIcon
|
||||
@@ -234,7 +315,7 @@ export default function RightSidebar() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!fullscreenExpanded && isPanelVisible && (
|
||||
{!fullscreenExpanded && !isChatOpen && 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
|
||||
@@ -323,7 +404,6 @@ export default function RightSidebar() {
|
||||
onToolSelect={handleToolSelectWithTransition}
|
||||
compact={agentsEnabled && !allToolsView}
|
||||
/>
|
||||
<AgentsChatOverlay />
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
export function formatRelativeTime(timestamp: number): string {
|
||||
const diff = Date.now() - timestamp;
|
||||
const mins = Math.floor(diff / 60_000);
|
||||
if (mins < 1) return "just now";
|
||||
if (mins < 60) return `${mins}m ago`;
|
||||
const hours = Math.floor(mins / 60);
|
||||
if (hours < 24) return `${hours}h ago`;
|
||||
const days = Math.floor(hours / 24);
|
||||
return `${days}d ago`;
|
||||
}
|
||||
Reference in New Issue
Block a user