Skip to content

Commit ba2be1f

Browse files
lramos33iskhakovjoeyorlandobrojd
authored
fix: smaller bugs (#1311)
> Add version to the bottom of the chat page like on the other pages In `version.tsx`, I just updated the if conditional to `"/chat/"`. This will render the version on the new chat page (only `/chat`), but not on the chat page (`/chat/:id`) > Copy paste from ms word to archestra chat input doesn’t work due to formatting issues I couldn't reproduce this one, but I added sanitization to remove problematic characters (zero-width and control characters) > If you open a tool response in the chat UI that has alooooot of data, it crashes the Chrome tab This was crashing for a few reasons: - The collapsible in the tool component was rendering its children even when closed (unecessary) - `code-block.tsx` was rendering >**_TWO_**< complete copies of the syntax-highlighted code - Simply rendering too many lines can end up breaking the page So I implemented three optimizations: - Lazy rendering in the collapsible - Fix the `code-block.tsx` to render only once - Truncated large outputs to 50 lines and add a 'show more' button #1102 <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Improves chat UI stability by lazy-rendering tool content, consolidating code block theming, truncating large outputs with expand control, sanitizing pasted text, and tweaking version display routing. > > - **AI Elements**: > - **Tool**: > - Add lazy rendering via context so `ToolContent` mounts children only after first open. > - Truncate large `output` to 50 lines with “Show more/less”; handle string/object outputs. > - **Code Block**: > - Use `next-themes` to select `oneDark`/`oneLight` and render a single `SyntaxHighlighter` instance. > - **Prompt Input**: > - Sanitize pasted text (strip zero-width/control chars) and handle file pastes; prevent default paste when sanitizing. > - **Version**: > - Only hide version on routes starting with `/chat/` (still show on `/chat`). > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit fefb107. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Ildar Iskhakov <[email protected]> Co-authored-by: Joey Orlando <[email protected]> Co-authored-by: Dominik Broj <[email protected]>
1 parent dbb05e5 commit ba2be1f

File tree

4 files changed

+166
-85
lines changed

4 files changed

+166
-85
lines changed

platform/frontend/src/components/ai-elements/code-block.tsx

Lines changed: 46 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"use client";
22

33
import { CheckIcon, CopyIcon } from "lucide-react";
4+
import { useTheme } from "next-themes";
45
import type { ComponentProps, HTMLAttributes, ReactNode } from "react";
56
import { createContext, useContext, useState } from "react";
67
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
@@ -33,71 +34,53 @@ export const CodeBlock = ({
3334
className,
3435
children,
3536
...props
36-
}: CodeBlockProps) => (
37-
<CodeBlockContext.Provider value={{ code }}>
38-
<div
39-
className={cn(
40-
"relative w-full overflow-hidden rounded-md border bg-background text-foreground",
41-
className,
42-
)}
43-
{...props}
44-
>
45-
<div className="relative">
46-
<SyntaxHighlighter
47-
className="overflow-hidden dark:hidden"
48-
codeTagProps={{
49-
className: "font-mono text-sm",
50-
}}
51-
customStyle={{
52-
margin: 0,
53-
padding: "1rem",
54-
fontSize: "0.875rem",
55-
background: "hsl(var(--background))",
56-
color: "hsl(var(--foreground))",
57-
}}
58-
language={language}
59-
lineNumberStyle={{
60-
color: "hsl(var(--muted-foreground))",
61-
paddingRight: "1rem",
62-
minWidth: "2.5rem",
63-
}}
64-
showLineNumbers={showLineNumbers}
65-
style={oneLight}
66-
>
67-
{code}
68-
</SyntaxHighlighter>
69-
<SyntaxHighlighter
70-
className="hidden overflow-hidden dark:block"
71-
codeTagProps={{
72-
className: "font-mono text-sm",
73-
}}
74-
customStyle={{
75-
margin: 0,
76-
padding: "1rem",
77-
fontSize: "0.875rem",
78-
background: "hsl(var(--background))",
79-
color: "hsl(var(--foreground))",
80-
}}
81-
language={language}
82-
lineNumberStyle={{
83-
color: "hsl(var(--muted-foreground))",
84-
paddingRight: "1rem",
85-
minWidth: "2.5rem",
86-
}}
87-
showLineNumbers={showLineNumbers}
88-
style={oneDark}
89-
>
90-
{code}
91-
</SyntaxHighlighter>
92-
{children && (
93-
<div className="absolute top-2 right-2 flex items-center gap-2">
94-
{children}
95-
</div>
37+
}: CodeBlockProps) => {
38+
const { resolvedTheme } = useTheme();
39+
const isDark = resolvedTheme === "dark";
40+
41+
return (
42+
<CodeBlockContext.Provider value={{ code }}>
43+
<div
44+
className={cn(
45+
"relative w-full overflow-hidden rounded-md border bg-background text-foreground",
46+
className,
9647
)}
48+
{...props}
49+
>
50+
<div className="relative">
51+
<SyntaxHighlighter
52+
className="overflow-hidden"
53+
codeTagProps={{
54+
className: "font-mono text-sm",
55+
}}
56+
customStyle={{
57+
margin: 0,
58+
padding: "1rem",
59+
fontSize: "0.875rem",
60+
background: "hsl(var(--background))",
61+
color: "hsl(var(--foreground))",
62+
}}
63+
language={language}
64+
lineNumberStyle={{
65+
color: "hsl(var(--muted-foreground))",
66+
paddingRight: "1rem",
67+
minWidth: "2.5rem",
68+
}}
69+
showLineNumbers={showLineNumbers}
70+
style={isDark ? oneDark : oneLight}
71+
>
72+
{code}
73+
</SyntaxHighlighter>
74+
{children && (
75+
<div className="absolute top-2 right-2 flex items-center gap-2">
76+
{children}
77+
</div>
78+
)}
79+
</div>
9780
</div>
98-
</div>
99-
</CodeBlockContext.Provider>
100-
);
81+
</CodeBlockContext.Provider>
82+
);
83+
};
10184

10285
export type CodeBlockCopyButtonProps = ComponentProps<typeof Button> & {
10386
onCopy?: () => void;

platform/frontend/src/components/ai-elements/prompt-input.tsx

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -529,17 +529,41 @@ export const PromptInputTextarea = ({
529529
}
530530

531531
const files: File[] = [];
532+
let hasText = false;
532533

533534
for (const item of items) {
534535
if (item.kind === "file") {
535536
const file = item.getAsFile();
536537
if (file) {
537538
files.push(file);
538539
}
540+
} else if (item.type === "text/plain") {
541+
hasText = true;
539542
}
540543
}
541544

542-
if (files.length > 0) {
545+
// Sanitize to remove problematic characters
546+
if (hasText && files.length === 0) {
547+
event.preventDefault();
548+
const text = event.clipboardData.getData("text/plain");
549+
550+
const sanitized = text
551+
.replace(/[\u200B-\u200D\uFEFF]/g, "")
552+
// biome-ignore lint/suspicious/noControlCharactersInRegex: Intentionally filtering control characters from pasted text to prevent ms word copy paste bug (issue #1102)
553+
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
554+
555+
const target = event.target as HTMLTextAreaElement;
556+
const start = target.selectionStart;
557+
const end = target.selectionEnd;
558+
const current = target.value;
559+
560+
target.value =
561+
current.substring(0, start) + sanitized + current.substring(end);
562+
target.selectionStart = target.selectionEnd = start + sanitized.length;
563+
564+
const changeEvent = new Event("input", { bubbles: true });
565+
target.dispatchEvent(changeEvent);
566+
} else if (files.length > 0) {
543567
event.preventDefault();
544568
attachments.add(files);
545569
}

platform/frontend/src/components/ai-elements/tool.tsx

Lines changed: 94 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import {
1010
XCircleIcon,
1111
} from "lucide-react";
1212
import type { ComponentProps, ReactNode } from "react";
13+
import { createContext, useContext, useState } from "react";
1314
import { Badge } from "@/components/ui/badge";
15+
import { Button } from "@/components/ui/button";
1416
import {
1517
Collapsible,
1618
CollapsibleContent,
@@ -21,13 +23,36 @@ import { CodeBlock } from "./code-block";
2123

2224
export type ToolProps = ComponentProps<typeof Collapsible>;
2325

24-
export const Tool = ({ className, ...props }: ToolProps) => (
25-
<Collapsible
26-
defaultOpen={false}
27-
className={cn("not-prose mb-4 w-full rounded-md border", className)}
28-
{...props}
29-
/>
30-
);
26+
const ToolContext = createContext<{ hasOpened: boolean }>({ hasOpened: false });
27+
28+
export const Tool = ({
29+
className,
30+
onOpenChange,
31+
children,
32+
...props
33+
}: ToolProps) => {
34+
const [hasOpened, setHasOpened] = useState(
35+
props.defaultOpen || props.open || false,
36+
);
37+
38+
const handleOpenChange = (open: boolean) => {
39+
if (open) setHasOpened(true);
40+
onOpenChange?.(open);
41+
};
42+
43+
return (
44+
<ToolContext.Provider value={{ hasOpened }}>
45+
<Collapsible
46+
defaultOpen={false}
47+
className={cn("not-prose mb-4 w-full rounded-md border", className)}
48+
onOpenChange={handleOpenChange}
49+
{...props}
50+
>
51+
{children}
52+
</Collapsible>
53+
</ToolContext.Provider>
54+
);
55+
};
3156

3257
export type ToolHeaderProps = {
3358
title?: string;
@@ -108,15 +133,25 @@ export const ToolHeader = ({
108133

109134
export type ToolContentProps = ComponentProps<typeof CollapsibleContent>;
110135

111-
export const ToolContent = ({ className, ...props }: ToolContentProps) => (
112-
<CollapsibleContent
113-
className={cn(
114-
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-popover-foreground outline-none data-[state=closed]:animate-out data-[state=open]:animate-in",
115-
className,
116-
)}
117-
{...props}
118-
/>
119-
);
136+
export const ToolContent = ({
137+
className,
138+
children,
139+
...props
140+
}: ToolContentProps) => {
141+
const { hasOpened } = useContext(ToolContext);
142+
143+
return (
144+
<CollapsibleContent
145+
className={cn(
146+
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-popover-foreground outline-none data-[state=closed]:animate-out data-[state=open]:animate-in",
147+
className,
148+
)}
149+
{...props}
150+
>
151+
{hasOpened ? children : null}
152+
</CollapsibleContent>
153+
);
154+
};
120155

121156
export type ToolInputProps = ComponentProps<"div"> & {
122157
input: ToolUIPart["input"];
@@ -151,6 +186,8 @@ export const ToolOutput = ({
151186
conversations,
152187
...props
153188
}: ToolOutputProps) => {
189+
const [isExpanded, setIsExpanded] = useState(false);
190+
154191
if (!(output || errorText || conversations)) {
155192
return null;
156193
}
@@ -200,13 +237,50 @@ export const ToolOutput = ({
200237

201238
let Output = <div>{output as ReactNode}</div>;
202239

203-
if (typeof output === "object") {
240+
if (typeof output === "object" || typeof output === "string") {
241+
const codeString =
242+
typeof output === "object" ? JSON.stringify(output, null, 2) : output;
243+
const lines = codeString.split("\n");
244+
const MAX_LINES = 50;
245+
const isLarge = lines.length > MAX_LINES;
246+
247+
const displayCode =
248+
isExpanded || !isLarge
249+
? codeString
250+
: `${lines.slice(0, MAX_LINES).join("\n")}\n... (${
251+
lines.length - MAX_LINES
252+
} more lines)`;
253+
204254
Output = (
205-
<CodeBlock code={JSON.stringify(output, null, 2)} language="json" />
255+
<div className="relative group">
256+
<CodeBlock code={displayCode} language="json" />
257+
{isLarge && (
258+
<div
259+
className={cn(
260+
"absolute bottom-4 left-0 right-0 flex justify-center transition-all duration-200",
261+
!isExpanded &&
262+
"pt-16 pb-2 bg-gradient-to-t from-background/80 to-transparent",
263+
)}
264+
>
265+
<Button
266+
variant="secondary"
267+
size="sm"
268+
onClick={(e) => {
269+
e.stopPropagation();
270+
setIsExpanded(!isExpanded);
271+
}}
272+
className="h-7 text-xs shadow-sm bg-background/80 backdrop-blur-sm hover:bg-background border"
273+
>
274+
{isExpanded
275+
? "Show Less"
276+
: `Show ${lines.length - MAX_LINES} more lines`}
277+
</Button>
278+
</div>
279+
)}
280+
</div>
206281
);
207-
} else if (typeof output === "string") {
208-
Output = <CodeBlock code={output} language="json" />;
209282
}
283+
210284
return (
211285
<div className={cn("space-y-2 p-4", className)} {...props}>
212286
<h4 className="font-medium text-muted-foreground text-xs uppercase tracking-wide">

platform/frontend/src/components/version.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export function Version() {
77
const { data } = useHealth();
88
const pathname = usePathname();
99

10-
if (pathname.startsWith("/chat")) {
10+
if (pathname.startsWith("/chat/")) {
1111
return null;
1212
}
1313

0 commit comments

Comments
 (0)