@@ -251,7 +251,7 @@ function ToolFallbackResult({
@@ -316,7 +316,7 @@ const ToolFallbackImpl: ToolCallMessagePartComponent = ({
return (
diff --git a/studio/frontend/src/components/assistant-ui/tool-group.tsx b/studio/frontend/src/components/assistant-ui/tool-group.tsx
index f29adb510f..bf7a6a9a25 100644
--- a/studio/frontend/src/components/assistant-ui/tool-group.tsx
+++ b/studio/frontend/src/components/assistant-ui/tool-group.tsx
@@ -26,11 +26,11 @@ const toolGroupVariants = cva("aui-tool-group-root group/tool-group w-full", {
variants: {
variant: {
outline: "corner-squircle rounded-lg border py-3",
- ghost: "",
+ ghost: "rounded-lg bg-muted/10 py-2",
muted: "corner-squircle rounded-lg border border-muted-foreground/30 bg-muted/30 py-3",
},
},
- defaultVariants: { variant: "outline" },
+ defaultVariants: { variant: "ghost" },
});
export type ToolGroupRootProps = Omit<
@@ -76,7 +76,7 @@ function ToolGroupRoot({
{label}
@@ -189,6 +188,7 @@ function ToolGroupContent({
"mt-2 flex flex-col gap-2",
"group-data-[variant=outline]/tool-group-root:mt-3 group-data-[variant=outline]/tool-group-root:border-t group-data-[variant=outline]/tool-group-root:px-4 group-data-[variant=outline]/tool-group-root:pt-3",
"group-data-[variant=muted]/tool-group-root:mt-3 group-data-[variant=muted]/tool-group-root:border-t group-data-[variant=muted]/tool-group-root:px-4 group-data-[variant=muted]/tool-group-root:pt-3",
+ "group-data-[variant=ghost]/tool-group-root:mt-1 group-data-[variant=ghost]/tool-group-root:gap-1",
)}
>
{children}
diff --git a/studio/frontend/src/components/assistant-ui/tool-ui-python.tsx b/studio/frontend/src/components/assistant-ui/tool-ui-python.tsx
index 28468a10ad..a510ed0d9e 100644
--- a/studio/frontend/src/components/assistant-ui/tool-ui-python.tsx
+++ b/studio/frontend/src/components/assistant-ui/tool-ui-python.tsx
@@ -7,7 +7,7 @@ import { copyToClipboard } from "@/lib/copy-to-clipboard";
import type { ToolCallMessagePartComponent } from "@assistant-ui/react";
import { code as codePlugin } from "@streamdown/code";
import { CheckIcon, CodeIcon, CopyIcon, LoaderIcon } from "lucide-react";
-import { memo, useCallback, useMemo, useRef, useState } from "react";
+import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Streamdown } from "streamdown";
import {
ToolFallbackContent,
@@ -28,6 +28,15 @@ function truncate(text: string): string {
function CopyBtn({ text }: { text: string }) {
const [copied, setCopied] = useState(false);
const timer = useRef | null>(null);
+
+ useEffect(() => {
+ return () => {
+ if (timer.current) {
+ clearTimeout(timer.current);
+ }
+ };
+ }, []);
+
const copy = useCallback(() => {
if (copyToClipboard(text)) {
setCopied(true);
@@ -98,14 +107,14 @@ const PythonToolUIImpl: ToolCallMessagePartComponent = ({
icon={CodeIcon}
/>
-
+
{/* Code + copy */}
{code && (
)}
-
+ {code && }
{/* Output */}
{isRunning ? (
diff --git a/studio/frontend/src/components/assistant-ui/tool-ui-terminal.tsx b/studio/frontend/src/components/assistant-ui/tool-ui-terminal.tsx
index 1b65b3c081..f233f951d3 100644
--- a/studio/frontend/src/components/assistant-ui/tool-ui-terminal.tsx
+++ b/studio/frontend/src/components/assistant-ui/tool-ui-terminal.tsx
@@ -6,7 +6,7 @@
import { copyToClipboard } from "@/lib/copy-to-clipboard";
import type { ToolCallMessagePartComponent } from "@assistant-ui/react";
import { CheckIcon, CopyIcon, LoaderIcon, TerminalIcon } from "lucide-react";
-import { memo, useCallback, useRef, useState } from "react";
+import { memo, useCallback, useEffect, useRef, useState } from "react";
import {
ToolFallbackContent,
ToolFallbackRoot,
@@ -25,6 +25,15 @@ function truncate(text: string): string {
function CopyBtn({ text }: { text: string }) {
const [copied, setCopied] = useState(false);
const timer = useRef | null>(null);
+
+ useEffect(() => {
+ return () => {
+ if (timer.current) {
+ clearTimeout(timer.current);
+ }
+ };
+ }, []);
+
const copy = useCallback(() => {
if (copyToClipboard(text)) {
setCopied(true);
@@ -74,7 +83,7 @@ const TerminalToolUIImpl: ToolCallMessagePartComponent = ({
icon={TerminalIcon}
/>
-
+
{isRunning ? (
diff --git a/studio/frontend/src/components/assistant-ui/tool-ui-web-search.tsx b/studio/frontend/src/components/assistant-ui/tool-ui-web-search.tsx
index 0635a83ec4..d3a86846de 100644
--- a/studio/frontend/src/components/assistant-ui/tool-ui-web-search.tsx
+++ b/studio/frontend/src/components/assistant-ui/tool-ui-web-search.tsx
@@ -81,29 +81,27 @@ const WebSearchToolUIImpl: ToolCallMessagePartComponent = ({
/>
{isRunning ? (
-
+
Searching for “{query}”…
) : sources.length > 0 ? (
-
- {sources.map((source) => (
+
+ {sources.map((source, i) => (
-
-
- {source.title}
-
+
+ {source.title}
))}
) : result ? (
-
+
{typeof result === "string"
? result
diff --git a/studio/frontend/src/lib/latex.ts b/studio/frontend/src/lib/latex.ts
new file mode 100644
index 0000000000..954d0f7bc6
--- /dev/null
+++ b/studio/frontend/src/lib/latex.ts
@@ -0,0 +1,97 @@
+// Adapted from LibreChat's latex.ts
+// https://github.com/danny-avila/LibreChat/blob/main/client/src/utils/latex.ts
+//
+// Escapes currency dollar signs so they are not misinterpreted as LaTeX math
+// delimiters when singleDollarTextMath is enabled.
+
+/**
+ * Matches a single $ followed by a number pattern (currency), e.g.:
+ * $5, $1,000, $5.99, $100K, $3.5M
+ *
+ * Does NOT match:
+ * $$ (display math), \$ (already escaped), $\alpha (LaTeX command)
+ */
+const CURRENCY_REGEX =
+ /(? {
+ const regions: Array<[number, number]> = [];
+
+ // Fenced code blocks: ```...```
+ const fencedRe = /```[\s\S]*?```/g;
+ let match: RegExpExecArray | null;
+ while ((match = fencedRe.exec(content)) !== null) {
+ regions.push([match.index, match.index + match[0].length]);
+ }
+
+ // Inline code: `...` (but not inside fenced blocks -- we filter below)
+ const inlineRe = /`[^`\n]+`/g;
+ while ((match = inlineRe.exec(content)) !== null) {
+ const start = match.index;
+ const end = start + match[0].length;
+ // Skip if this backtick span falls inside a fenced block
+ let inside = false;
+ for (const [rs, re] of regions) {
+ if (start >= rs && end <= re) {
+ inside = true;
+ break;
+ }
+ }
+ if (!inside) {
+ regions.push([start, end]);
+ }
+ }
+
+ // Sort by start position for binary search
+ regions.sort((a, b) => a[0] - b[0]);
+ return regions;
+}
+
+/**
+ * Binary search to check if a position falls inside any code region.
+ */
+function isInCodeBlock(
+ position: number,
+ regions: Array<[number, number]>,
+): boolean {
+ let lo = 0;
+ let hi = regions.length - 1;
+ while (lo <= hi) {
+ const mid = (lo + hi) >>> 1;
+ const [start, end] = regions[mid];
+ if (position < start) {
+ hi = mid - 1;
+ } else if (position >= end) {
+ lo = mid + 1;
+ } else {
+ return true;
+ }
+ }
+ return false;
+}
+
+/**
+ * Preprocess a markdown string to escape currency dollar signs so they are not
+ * parsed as LaTeX math delimiters.
+ *
+ * - `$5` alone becomes `\$5` (currency, not math)
+ * - `$\alpha$` is untouched (real LaTeX)
+ * - `$$E = mc^2$$` is untouched (display math)
+ * - Currency inside code blocks/spans is untouched
+ */
+export function preprocessLaTeX(content: string): string {
+ if (!content.includes("$")) return content;
+
+ const codeRegions = findCodeBlockRegions(content);
+
+ return content.replace(CURRENCY_REGEX, (match, offset) => {
+ if (isInCodeBlock(offset, codeRegions)) {
+ return match;
+ }
+ return "\\" + match;
+ });
+}