Skip to content

Commit 1698769

Browse files
authored
Improvements to agent chat markdown rendering. (Stirling-Tools#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" />
1 parent bd9ef05 commit 1698769

14 files changed

Lines changed: 504 additions & 86 deletions

File tree

app/proprietary/src/main/java/stirling/software/proprietary/model/api/ai/AiWorkflowRequest.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,15 @@
66
import io.swagger.v3.oas.annotations.media.Schema;
77

88
import jakarta.validation.constraints.NotBlank;
9-
import jakarta.validation.constraints.NotNull;
109

1110
import lombok.Data;
1211

1312
@Data
1413
@Schema(description = "Run an AI workflow")
1514
public class AiWorkflowRequest {
1615

17-
@NotNull
1816
@Schema(description = "The input PDF files")
19-
private List<AiWorkflowFileInput> fileInputs;
17+
private List<AiWorkflowFileInput> fileInputs = new ArrayList<>();
2018

2119
@NotBlank
2220
@Schema(description = "The user message to orchestrate", example = "Summarise these documents")

app/proprietary/src/main/java/stirling/software/proprietary/service/AiWorkflowService.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -186,10 +186,7 @@ public AiWorkflowResponse orchestrate(AiWorkflowRequest request, ProgressListene
186186
WorkflowTurnRequest initialRequest = new WorkflowTurnRequest();
187187
initialRequest.setUserMessage(request.getUserMessage().trim());
188188
initialRequest.setFiles(files);
189-
initialRequest.setConversationHistory(
190-
request.getConversationHistory() == null
191-
? new ArrayList<>()
192-
: new ArrayList<>(request.getConversationHistory()));
189+
initialRequest.setConversationHistory(new ArrayList<>(request.getConversationHistory()));
193190
initialRequest.setEnabledEndpoints(endpointResolver.getEnabledEndpointUrls());
194191

195192
listener.onProgress(AiWorkflowProgressEvent.of(AiWorkflowPhase.ANALYZING));
@@ -232,6 +229,12 @@ private WorkflowState onNeedContent(
232229
WorkflowTurnRequest request,
233230
ProgressListener listener)
234231
throws IOException {
232+
if (filesById.isEmpty()) {
233+
return new WorkflowState.Terminal(
234+
cannotContinue(
235+
"No files were uploaded. Please add a PDF to the workbench first."));
236+
}
237+
235238
if (!request.getArtifacts().isEmpty()) {
236239
return new WorkflowState.Terminal(
237240
cannotContinue("AI engine requested content extraction more than once."));

frontend/editor/public/locales/en-GB/translation.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1526,6 +1526,7 @@ section_title = "Agents"
15261526
show_less = "Show less"
15271527
start_chat = "Start chatting"
15281528
stirling_description = "Your general-purpose PDF assistant"
1529+
stirling_running = "Running..."
15291530
stirling_full_name = "Stirling General Agent"
15301531
stirling_long_description = "General purpose PDF assistant that can run tools, create PDFs and extract insights from your documents."
15311532
stirling_name = "Stirling"
@@ -2715,6 +2716,12 @@ title = "Change Permissions"
27152716
[changePermissions.tooltip.warning]
27162717
text = "To make these permissions unchangeable, use the Add Password tool to set an owner password."
27172718

2719+
[chat]
2720+
resize = "Resize chat panel"
2721+
2722+
[chat.actions]
2723+
copy = "Copy message"
2724+
27182725
[chat.header]
27192726
agentMenu = "Stirling agent options"
27202727
clearChat = "Clear chat"

frontend/editor/src/core/components/agents/AgentsPanel.tsx

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,8 @@ export function useAgentsEnabled(): boolean {
1313
}
1414

1515
/**
16-
* Whether the agent chat overlay is currently open. Core builds have no chat,
16+
* Whether the agent chat panel is currently open. Core builds have no chat,
1717
* so this always returns false. Proprietary builds bridge to the ChatContext.
18-
* Used by {@code RightSidebar} so the fullscreen tool picker can yield to the
19-
* chat overlay just like it yields to a selected tool.
2018
*/
2119
export function useAgentChatOpen(): boolean {
2220
return false;
@@ -35,14 +33,6 @@ export function AgentsCollapsedButton(_props: { onExpand: () => void }) {
3533
return null;
3634
}
3735

38-
/**
39-
* Full-rail chat overlay rendered inside {@code ToolPanel}. Covers the panel
40-
* (including the search bar) when an agent conversation is active.
41-
*/
42-
export function AgentsChatOverlay() {
43-
return null;
44-
}
45-
4636
/**
4737
* Agents card rendered inside the fullscreen tool picker. Matches the visual
4838
* language of the fullscreen category cards (gradient border, title, items).
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* Core stub for the chat context.
3+
* The real implementation lives in proprietary/components/chat/ChatContext.tsx
4+
* and shadows this via the @app/* alias cascade in proprietary builds.
5+
*/
6+
7+
export function useChat() {
8+
return {
9+
messages: [] as never[],
10+
isOpen: false,
11+
isLoading: false,
12+
progress: null,
13+
toggleOpen: () => {},
14+
setOpen: (_open: boolean) => {},
15+
sendMessage: async (_content: string) => {},
16+
clearChat: () => {},
17+
};
18+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/**
2+
* Core stub for the chat panel.
3+
* The real implementation lives in proprietary/components/chat/ChatPanel.tsx
4+
* and shadows this via the @app/* alias cascade in proprietary builds.
5+
*/
6+
7+
export interface ChatPanelProps {
8+
onBack: () => void;
9+
backLabel: string;
10+
}
11+
12+
export function ChatPanel(_props: ChatPanelProps) {
13+
return null;
14+
}

frontend/editor/src/core/components/tools/RightSidebar.tsx

Lines changed: 86 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useMemo, useState } from "react";
1+
import { useMemo, useState, useRef, useCallback } from "react";
22
import { ActionIcon } from "@mantine/core";
33
import { useTranslation } from "react-i18next";
44
import { useRainbowThemeContext } from "@app/components/shared/RainbowThemeProvider";
@@ -9,11 +9,12 @@ import { useIsMobile } from "@app/hooks/useIsMobile";
99
import ToolPanel from "@app/components/tools/ToolPanel";
1010
import ToolSearch from "@app/components/tools/toolPicker/ToolSearch";
1111
import {
12-
AgentsChatOverlay,
1312
AgentsCollapsedButton,
1413
AgentsSection,
1514
useAgentsEnabled,
1615
} from "@app/components/agents/AgentsPanel";
16+
import { useChat } from "@app/components/chat/ChatContext";
17+
import { ChatPanel } from "@app/components/chat/ChatPanel";
1718
import { useFavoriteToolItems } from "@app/hooks/tools/useFavoriteToolItems";
1819
import { useToolSections } from "@app/hooks/useToolSections";
1920
import type { SubcategoryGroup } from "@app/hooks/useToolSections";
@@ -32,6 +33,10 @@ import {
3233
import { useToolPanelGeometry } from "@app/hooks/tools/useToolPanelGeometry";
3334
import "@app/components/tools/ToolPanel.css";
3435

36+
const DEFAULT_CHAT_WIDTH_PX = 18.5 * 16; // 18.5rem in px
37+
const MIN_CHAT_WIDTH_PX = 240;
38+
const MAX_CHAT_WIDTH_PX = 720;
39+
3540
/**
3641
* Right-side rail wrapping the tool panel.
3742
*
@@ -86,6 +91,57 @@ export default function RightSidebar() {
8691

8792
const [allToolsView, setAllToolsView] = useState(false);
8893

94+
const { isOpen: isChatOpen, setOpen: setChatOpen } = useChat();
95+
const [chatWidthPx, setChatWidthPx] = useState(DEFAULT_CHAT_WIDTH_PX);
96+
const [isChatDragging, setIsChatDragging] = useState(false);
97+
const chatDragState = useRef<{ startX: number; startWidth: number } | null>(
98+
null,
99+
);
100+
101+
const handleChatClose = useCallback(() => {
102+
withViewTransition(() => setChatOpen(false));
103+
setChatWidthPx(DEFAULT_CHAT_WIDTH_PX);
104+
}, [setChatOpen]);
105+
106+
const handleResizeChatPointerDown = useCallback(
107+
(e: React.PointerEvent<HTMLDivElement>) => {
108+
e.preventDefault();
109+
chatDragState.current = { startX: e.clientX, startWidth: chatWidthPx };
110+
setIsChatDragging(true);
111+
document.body.style.cursor = "col-resize";
112+
document.body.style.userSelect = "none";
113+
114+
const onMove = (ev: PointerEvent) => {
115+
if (!chatDragState.current) return;
116+
const delta = chatDragState.current.startX - ev.clientX;
117+
setChatWidthPx(
118+
Math.max(
119+
MIN_CHAT_WIDTH_PX,
120+
Math.min(
121+
MAX_CHAT_WIDTH_PX,
122+
chatDragState.current.startWidth + delta,
123+
),
124+
),
125+
);
126+
};
127+
128+
const cleanup = () => {
129+
chatDragState.current = null;
130+
setIsChatDragging(false);
131+
document.body.style.removeProperty("cursor");
132+
document.body.style.removeProperty("user-select");
133+
window.removeEventListener("pointermove", onMove);
134+
window.removeEventListener("pointerup", cleanup);
135+
window.removeEventListener("pointercancel", cleanup);
136+
};
137+
138+
window.addEventListener("pointermove", onMove);
139+
window.addEventListener("pointerup", cleanup);
140+
window.addEventListener("pointercancel", cleanup);
141+
},
142+
[chatWidthPx],
143+
);
144+
89145
const handleShowAllTools = () => {
90146
withViewTransition(() => setAllToolsView(true));
91147
};
@@ -147,6 +203,7 @@ export default function RightSidebar() {
147203

148204
const computedWidth = () => {
149205
if (isMobile) return "100%";
206+
if (isChatOpen) return `${chatWidthPx}px`;
150207
if (!isPanelVisible) return "3.5rem";
151208
return "18.5rem";
152209
};
@@ -182,15 +239,39 @@ export default function RightSidebar() {
182239
ref={toolPanelRef}
183240
data-sidebar="tool-panel"
184241
data-tour={fullscreenExpanded ? undefined : "tool-panel"}
185-
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 ${
242+
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 ${
186243
isRainbowMode ? rainbowStyles.rainbowPaper : ""
187244
} ${isMobile ? "h-full border-r-0" : "h-screen"} ${fullscreenExpanded ? "tool-panel--fullscreen" : ""}`}
188245
style={{
189246
width: computedWidth(),
190247
padding: "0",
248+
...(isChatDragging ? { transition: "none" } : {}),
191249
}}
192250
>
193-
{!fullscreenExpanded && !isPanelVisible && !isMobile && (
251+
{!fullscreenExpanded && isChatOpen && (
252+
<div
253+
style={{
254+
height: "100%",
255+
width: "100%",
256+
display: "flex",
257+
flexDirection: "column",
258+
}}
259+
>
260+
<div
261+
className="agents-takeover__resize-handle"
262+
onPointerDown={handleResizeChatPointerDown}
263+
role="separator"
264+
aria-label={t("chat.resize", "Resize chat panel")}
265+
aria-orientation="vertical"
266+
/>
267+
<ChatPanel
268+
onBack={handleChatClose}
269+
backLabel={t("agents.back_to_tools", "Back to tools")}
270+
/>
271+
</div>
272+
)}
273+
274+
{!fullscreenExpanded && !isChatOpen && !isPanelVisible && !isMobile && (
194275
<div className="tool-panel__collapsed-strip">
195276
<div className="tool-panel__collapsed-top">
196277
<ActionIcon
@@ -234,7 +315,7 @@ export default function RightSidebar() {
234315
</div>
235316
)}
236317

237-
{!fullscreenExpanded && isPanelVisible && (
318+
{!fullscreenExpanded && !isChatOpen && isPanelVisible && (
238319
<div
239320
/* Fixed width matches the expanded panel width so the inner content is
240321
laid out at its final size from the moment it mounts. The outer
@@ -323,7 +404,6 @@ export default function RightSidebar() {
323404
onToolSelect={handleToolSelectWithTransition}
324405
compact={agentsEnabled && !allToolsView}
325406
/>
326-
<AgentsChatOverlay />
327407
</div>
328408
)}
329409

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export function formatRelativeTime(timestamp: number): string {
2+
const diff = Date.now() - timestamp;
3+
const mins = Math.floor(diff / 60_000);
4+
if (mins < 1) return "just now";
5+
if (mins < 60) return `${mins}m ago`;
6+
const hours = Math.floor(mins / 60);
7+
if (hours < 24) return `${hours}h ago`;
8+
const days = Math.floor(hours / 24);
9+
return `${days}d ago`;
10+
}

0 commit comments

Comments
 (0)