Skip to content

Commit a897fbc

Browse files
feat(inspector): markdown support and image update (#181)
1 parent e134012 commit a897fbc

File tree

6 files changed

+314
-10
lines changed

6 files changed

+314
-10
lines changed

docs/images/inspector.png

649 KB
Loading

frontend/packages/inspector/index.html

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1970,6 +1970,84 @@
19701970
white-space: pre-wrap;
19711971
}
19721972

1973+
.markdown-body p {
1974+
margin: 0;
1975+
}
1976+
1977+
.markdown-body p + p {
1978+
margin-top: 8px;
1979+
}
1980+
1981+
.markdown-body h1,
1982+
.markdown-body h2,
1983+
.markdown-body h3,
1984+
.markdown-body h4,
1985+
.markdown-body h5,
1986+
.markdown-body h6 {
1987+
margin: 0 0 8px;
1988+
line-height: 1.35;
1989+
font-weight: 700;
1990+
}
1991+
1992+
.markdown-body h1 { font-size: 1.35em; }
1993+
.markdown-body h2 { font-size: 1.22em; }
1994+
.markdown-body h3 { font-size: 1.12em; }
1995+
.markdown-body h4,
1996+
.markdown-body h5,
1997+
.markdown-body h6 { font-size: 1em; }
1998+
1999+
.markdown-body ul,
2000+
.markdown-body ol {
2001+
margin: 8px 0 0 18px;
2002+
padding: 0;
2003+
}
2004+
2005+
.markdown-body li + li {
2006+
margin-top: 4px;
2007+
}
2008+
2009+
.markdown-body blockquote {
2010+
margin: 8px 0 0;
2011+
padding: 6px 10px;
2012+
border-left: 3px solid var(--border-2);
2013+
background: var(--surface-2);
2014+
color: var(--muted);
2015+
border-radius: 4px;
2016+
}
2017+
2018+
.markdown-body code {
2019+
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Consolas, monospace;
2020+
font-size: 0.9em;
2021+
background: rgba(255, 255, 255, 0.08);
2022+
border: 1px solid var(--border-2);
2023+
border-radius: 4px;
2024+
padding: 1px 5px;
2025+
}
2026+
2027+
.message.user .markdown-body code {
2028+
background: rgba(0, 0, 0, 0.08);
2029+
border-color: rgba(0, 0, 0, 0.12);
2030+
}
2031+
2032+
.markdown-body .md-pre {
2033+
margin: 8px 0 0;
2034+
padding: 8px 10px;
2035+
border-radius: 6px;
2036+
border: 1px solid var(--border);
2037+
background: var(--surface-2);
2038+
overflow-x: auto;
2039+
}
2040+
2041+
.markdown-body .md-pre code {
2042+
background: transparent;
2043+
border: none;
2044+
border-radius: 0;
2045+
padding: 0;
2046+
font-size: 11px;
2047+
line-height: 1.5;
2048+
display: block;
2049+
}
2050+
19732051
.message-error {
19742052
background: rgba(255, 59, 48, 0.1);
19752053
border: 1px solid rgba(255, 59, 48, 0.3);

frontend/packages/inspector/src/components/chat/ChatMessages.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useState } from "react";
22
import { getMessageClass } from "./messageUtils";
33
import type { TimelineEntry } from "./types";
44
import { AlertTriangle, ChevronRight, ChevronDown, Wrench, Brain, Info, ExternalLink, PlayCircle } from "lucide-react";
5+
import MarkdownText from "./MarkdownText";
56

67
const ToolItem = ({
78
entry,
@@ -253,7 +254,7 @@ const ChatMessages = ({
253254
<div key={entry.id} className={`message ${messageClass} no-avatar`}>
254255
<div className="message-content">
255256
{entry.text ? (
256-
<div className="part-body">{entry.text}</div>
257+
<MarkdownText text={entry.text} />
257258
) : (
258259
<span className="thinking-indicator">
259260
<span className="thinking-dot" />
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import type { ReactNode } from "react";
2+
3+
const SAFE_URL_RE = /^(https?:\/\/|mailto:)/i;
4+
5+
const isSafeUrl = (url: string): boolean => SAFE_URL_RE.test(url.trim());
6+
7+
const inlineTokenRe = /(`[^`\n]+`|\[[^\]\n]+\]\(([^)\s]+)(?:\s+"[^"]*")?\)|\*\*[^*\n]+\*\*|__[^_\n]+__|\*[^*\n]+\*|_[^_\n]+_|~~[^~\n]+~~)/g;
8+
9+
const parseInline = (text: string, keyPrefix: string): ReactNode[] => {
10+
const out: ReactNode[] = [];
11+
let lastIndex = 0;
12+
let tokenIndex = 0;
13+
14+
for (const match of text.matchAll(inlineTokenRe)) {
15+
const token = match[0];
16+
const idx = match.index ?? 0;
17+
18+
if (idx > lastIndex) {
19+
out.push(text.slice(lastIndex, idx));
20+
}
21+
22+
const key = `${keyPrefix}-t-${tokenIndex++}`;
23+
24+
if (token.startsWith("`") && token.endsWith("`")) {
25+
out.push(<code key={key}>{token.slice(1, -1)}</code>);
26+
} else if (token.startsWith("**") && token.endsWith("**")) {
27+
out.push(<strong key={key}>{token.slice(2, -2)}</strong>);
28+
} else if (token.startsWith("__") && token.endsWith("__")) {
29+
out.push(<strong key={key}>{token.slice(2, -2)}</strong>);
30+
} else if (token.startsWith("*") && token.endsWith("*")) {
31+
out.push(<em key={key}>{token.slice(1, -1)}</em>);
32+
} else if (token.startsWith("_") && token.endsWith("_")) {
33+
out.push(<em key={key}>{token.slice(1, -1)}</em>);
34+
} else if (token.startsWith("~~") && token.endsWith("~~")) {
35+
out.push(<del key={key}>{token.slice(2, -2)}</del>);
36+
} else if (token.startsWith("[") && token.includes("](") && token.endsWith(")")) {
37+
const linkMatch = token.match(/^\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)$/);
38+
if (!linkMatch) {
39+
out.push(token);
40+
} else {
41+
const label = linkMatch[1];
42+
const href = linkMatch[2];
43+
if (isSafeUrl(href)) {
44+
out.push(
45+
<a key={key} href={href} target="_blank" rel="noreferrer">
46+
{label}
47+
</a>,
48+
);
49+
} else {
50+
out.push(label);
51+
}
52+
}
53+
} else {
54+
out.push(token);
55+
}
56+
57+
lastIndex = idx + token.length;
58+
}
59+
60+
if (lastIndex < text.length) {
61+
out.push(text.slice(lastIndex));
62+
}
63+
64+
return out;
65+
};
66+
67+
const renderInlineLines = (text: string, keyPrefix: string): ReactNode[] => {
68+
const lines = text.split("\n");
69+
const out: ReactNode[] = [];
70+
lines.forEach((line, idx) => {
71+
if (idx > 0) out.push(<br key={`${keyPrefix}-br-${idx}`} />);
72+
out.push(...parseInline(line, `${keyPrefix}-l-${idx}`));
73+
});
74+
return out;
75+
};
76+
77+
const isUnorderedListItem = (line: string): boolean => /^\s*[-*+]\s+/.test(line);
78+
const isOrderedListItem = (line: string): boolean => /^\s*\d+\.\s+/.test(line);
79+
80+
const MarkdownText = ({ text }: { text: string }) => {
81+
const source = text.replace(/\r\n?/g, "\n");
82+
const lines = source.split("\n");
83+
const nodes: ReactNode[] = [];
84+
85+
let i = 0;
86+
while (i < lines.length) {
87+
const line = lines[i];
88+
const trimmed = line.trim();
89+
90+
if (!trimmed) {
91+
i += 1;
92+
continue;
93+
}
94+
95+
if (trimmed.startsWith("```")) {
96+
const lang = trimmed.slice(3).trim();
97+
const codeLines: string[] = [];
98+
i += 1;
99+
while (i < lines.length && !lines[i].trim().startsWith("```")) {
100+
codeLines.push(lines[i]);
101+
i += 1;
102+
}
103+
if (i < lines.length && lines[i].trim().startsWith("```")) i += 1;
104+
nodes.push(
105+
<pre key={`code-${nodes.length}`} className="md-pre">
106+
<code className={lang ? `language-${lang}` : undefined}>{codeLines.join("\n")}</code>
107+
</pre>,
108+
);
109+
continue;
110+
}
111+
112+
const headingMatch = trimmed.match(/^(#{1,6})\s+(.+)$/);
113+
if (headingMatch) {
114+
const level = headingMatch[1].length;
115+
const content = headingMatch[2];
116+
const key = `h-${nodes.length}`;
117+
if (level === 1) nodes.push(<h1 key={key}>{renderInlineLines(content, key)}</h1>);
118+
else if (level === 2) nodes.push(<h2 key={key}>{renderInlineLines(content, key)}</h2>);
119+
else if (level === 3) nodes.push(<h3 key={key}>{renderInlineLines(content, key)}</h3>);
120+
else if (level === 4) nodes.push(<h4 key={key}>{renderInlineLines(content, key)}</h4>);
121+
else if (level === 5) nodes.push(<h5 key={key}>{renderInlineLines(content, key)}</h5>);
122+
else nodes.push(<h6 key={key}>{renderInlineLines(content, key)}</h6>);
123+
i += 1;
124+
continue;
125+
}
126+
127+
if (trimmed.startsWith(">")) {
128+
const quoteLines: string[] = [];
129+
while (i < lines.length && lines[i].trim().startsWith(">")) {
130+
quoteLines.push(lines[i].trim().replace(/^>\s?/, ""));
131+
i += 1;
132+
}
133+
const content = quoteLines.join("\n");
134+
const key = `q-${nodes.length}`;
135+
nodes.push(<blockquote key={key}>{renderInlineLines(content, key)}</blockquote>);
136+
continue;
137+
}
138+
139+
if (isUnorderedListItem(line) || isOrderedListItem(line)) {
140+
const ordered = isOrderedListItem(line);
141+
const items: string[] = [];
142+
while (i < lines.length) {
143+
const candidate = lines[i];
144+
if (ordered && isOrderedListItem(candidate)) {
145+
items.push(candidate.replace(/^\s*\d+\.\s+/, ""));
146+
i += 1;
147+
continue;
148+
}
149+
if (!ordered && isUnorderedListItem(candidate)) {
150+
items.push(candidate.replace(/^\s*[-*+]\s+/, ""));
151+
i += 1;
152+
continue;
153+
}
154+
if (!candidate.trim()) {
155+
i += 1;
156+
break;
157+
}
158+
break;
159+
}
160+
const key = `list-${nodes.length}`;
161+
if (ordered) {
162+
nodes.push(
163+
<ol key={key}>
164+
{items.map((item, idx) => (
165+
<li key={`${key}-i-${idx}`}>{renderInlineLines(item, `${key}-i-${idx}`)}</li>
166+
))}
167+
</ol>,
168+
);
169+
} else {
170+
nodes.push(
171+
<ul key={key}>
172+
{items.map((item, idx) => (
173+
<li key={`${key}-i-${idx}`}>{renderInlineLines(item, `${key}-i-${idx}`)}</li>
174+
))}
175+
</ul>,
176+
);
177+
}
178+
continue;
179+
}
180+
181+
const paragraphLines: string[] = [];
182+
while (i < lines.length) {
183+
const current = lines[i];
184+
const currentTrimmed = current.trim();
185+
if (!currentTrimmed) break;
186+
if (
187+
currentTrimmed.startsWith("```") ||
188+
currentTrimmed.startsWith(">") ||
189+
/^(#{1,6})\s+/.test(currentTrimmed) ||
190+
isUnorderedListItem(current) ||
191+
isOrderedListItem(current)
192+
) {
193+
break;
194+
}
195+
paragraphLines.push(current);
196+
i += 1;
197+
}
198+
const content = paragraphLines.join("\n");
199+
const key = `p-${nodes.length}`;
200+
nodes.push(<p key={key}>{renderInlineLines(content, key)}</p>);
201+
}
202+
203+
return <div className="markdown-body">{nodes}</div>;
204+
};
205+
206+
export default MarkdownText;
649 KB
Loading

frontend/packages/website/src/components/Hero.tsx

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ function UniversalAPIDiagram() {
154154
const CopyInstallButton = () => {
155155
const [copied, setCopied] = useState(false);
156156
const installCommand = 'npx skills add rivet-dev/skills -s sandbox-agent';
157+
const shortCommand = 'npx skills add rivet-dev/skills';
157158

158159
const handleCopy = async () => {
159160
try {
@@ -171,8 +172,9 @@ const CopyInstallButton = () => {
171172
onClick={handleCopy}
172173
className="w-full sm:w-auto inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md border border-white/10 px-4 py-2 text-sm text-zinc-300 transition-colors hover:border-white/20 hover:text-white font-mono"
173174
>
174-
{copied ? <Check className="h-4 w-4 text-green-400" /> : <Terminal className="h-4 w-4" />}
175-
{installCommand}
175+
{copied ? <Check className="h-4 w-4 text-green-400" /> : <Terminal className="h-4 w-4 flex-shrink-0" />}
176+
<span className="hidden sm:inline">{installCommand}</span>
177+
<span className="sm:hidden">{shortCommand}</span>
176178
</button>
177179
<div className="absolute left-1/2 -translate-x-1/2 top-full mt-3 opacity-0 translate-y-2 group-hover:opacity-100 group-hover:translate-y-0 transition-all duration-200 ease-out text-xs text-zinc-500 whitespace-nowrap pointer-events-none font-mono">
178180
Give this to your coding agent
@@ -183,32 +185,49 @@ const CopyInstallButton = () => {
183185

184186
export function Hero() {
185187
const [scrollOpacity, setScrollOpacity] = useState(1);
188+
const [isMobile, setIsMobile] = useState(false);
186189

187190
useEffect(() => {
191+
const updateViewportMode = () => {
192+
const mobile = window.innerWidth < 1024;
193+
setIsMobile(mobile);
194+
if (mobile) {
195+
setScrollOpacity(1);
196+
}
197+
};
198+
188199
const handleScroll = () => {
200+
if (window.innerWidth < 1024) {
201+
setScrollOpacity(1);
202+
return;
203+
}
189204
const scrollY = window.scrollY;
190205
const windowHeight = window.innerHeight;
191-
const isMobile = window.innerWidth < 1024;
192-
193-
const fadeStart = windowHeight * (isMobile ? 0.3 : 0.15);
194-
const fadeEnd = windowHeight * (isMobile ? 0.7 : 0.5);
206+
const fadeStart = windowHeight * 0.15;
207+
const fadeEnd = windowHeight * 0.5;
195208
const opacity = 1 - Math.min(1, Math.max(0, (scrollY - fadeStart) / (fadeEnd - fadeStart)));
196209
setScrollOpacity(opacity);
197210
};
198211

212+
updateViewportMode();
213+
handleScroll();
214+
window.addEventListener('resize', updateViewportMode);
199215
window.addEventListener('scroll', handleScroll);
200-
return () => window.removeEventListener('scroll', handleScroll);
216+
return () => {
217+
window.removeEventListener('resize', updateViewportMode);
218+
window.removeEventListener('scroll', handleScroll);
219+
};
201220
}, []);
202221

203222
return (
204-
<section className="relative flex min-h-screen flex-col overflow-hidden">
223+
<section className="relative flex min-h-screen flex-col overflow-hidden pb-24 lg:pb-0">
205224
{/* Background gradient */}
206225
<div className="absolute inset-0 bg-gradient-to-b from-zinc-900/20 via-transparent to-transparent pointer-events-none" />
207226

208227
{/* Main content */}
209228
<div
210229
className="flex flex-1 flex-col justify-start pt-32 lg:justify-center lg:pt-0 lg:pb-20 px-6"
211-
style={{ opacity: scrollOpacity, filter: `blur(${(1 - scrollOpacity) * 8}px)` }}
230+
style={isMobile ? undefined : { opacity: scrollOpacity, filter: `blur(${(1 - scrollOpacity) * 8}px)` }}
212231
>
213232
<div className="mx-auto w-full max-w-7xl">
214233
<div className="flex flex-col gap-12 lg:flex-row lg:items-center lg:justify-between lg:gap-16 xl:gap-24">

0 commit comments

Comments
 (0)