Skip to content

Commit f021795

Browse files
committed
fix(chat): restore Inter and refine table overflow
1 parent 06983cd commit f021795

16 files changed

Lines changed: 154 additions & 60 deletions

File tree

‎apps/sim/app/o/[organizationId]/home/components/composer/composer.tsx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
isAssistantImageType,
1111
} from '@/lib/uploads/shared/assistant-images'
1212
import { MOTHERSHIP_ACCEPT_ATTRIBUTE } from '@/lib/uploads/utils/validation'
13+
import { inter } from '@/app/_styles/fonts/inter/inter'
1314
import { SearchInputBar } from '@/app/o/[organizationId]/components/search-input-bar'
1415
import { useOrganizationContext } from '@/app/o/[organizationId]/providers/organization-provider'
1516
import { AttachedFilesList } from '@/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list/attached-files-list'
@@ -268,6 +269,7 @@ export function Composer({
268269
onDragOver={files.handleDragOver}
269270
onDrop={files.handleDrop}
270271
className={cn(
272+
inter.className,
271273
'relative z-10 mx-auto w-full max-w-chat',
272274
!imagesOnly &&
273275
'rounded-2xl border border-[var(--border-1)] bg-[var(--white)] px-2.5 py-2 dark:bg-[var(--surface-4)]',

‎apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.tsx‎

Lines changed: 31 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
useState,
1212
} from 'react'
1313
import type { Nodes } from 'hast'
14-
import { defaultRehypePlugins, defaultRemarkPlugins, type ExtraProps, Streamdown } from 'streamdown'
14+
import { defaultRehypePlugins, defaultRemarkPlugins, Streamdown } from 'streamdown'
1515
import 'streamdown/styles.css'
1616
// prismjs core must load before its language components — they register on the
1717
// global `Prism` it installs (on `window`/`global`); fixes SSR + client order.
@@ -28,14 +28,17 @@ import {
2828
cn,
2929
Lightbox,
3030
languages,
31-
OverflowText,
3231
highlight as prismHighlight,
32+
scrollFadeAttributes,
33+
scrollFadeXClass,
34+
useScrollEdges,
3335
} from '@sim/emcn'
3436
import { extractTextContent } from '@/lib/core/utils/react-node-text'
3537
import {
3638
inlineChatImageUrl,
3739
isInlineFileReference,
3840
} from '@/lib/mothership/chat/inline-image-reference'
41+
import { inter } from '@/app/_styles/fonts/inter/inter'
3942
import { useChatSurface } from '@/app/workspace/[workspaceId]/home/components/chat-surface-context'
4043
import { sanitizeChatDisplayContent } from '@/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-sanitize'
4144
import {
@@ -267,69 +270,56 @@ function highlight(code: string, language: string): string {
267270
return html
268271
}
269272

270-
function hasInteractiveTableContent(node?: Nodes): boolean {
271-
return (
272-
node?.type === 'element' &&
273-
(node.tagName === 'a' ||
274-
node.tagName === 'img' ||
275-
node.children.some(hasInteractiveTableContent))
276-
)
277-
}
278-
279-
interface TableCellContentProps extends ExtraProps {
273+
interface MarkdownTableProps {
280274
children?: React.ReactNode
281275
}
282276

283-
function TableCellContent({ children, node }: TableCellContentProps) {
284-
if (hasInteractiveTableContent(node)) {
285-
return (
286-
<div className='min-w-[160px] max-w-[320px] whitespace-normal [overflow-wrap:anywhere]'>
287-
{children}
288-
</div>
289-
)
290-
}
277+
function MarkdownTable({ children }: MarkdownTableProps) {
278+
const scrollRef = useRef<HTMLDivElement>(null)
279+
const edges = useScrollEdges(scrollRef, { axis: 'x' })
280+
const isOverflowing = edges.left || edges.right
281+
291282
return (
292-
<span className='inline-block max-w-full align-middle'>
293-
<OverflowText
294-
label={extractTextContent(children)}
295-
className='[&_code]:whitespace-nowrap [@media(hover:hover)]:max-w-[320px]'
296-
>
283+
<div
284+
ref={scrollRef}
285+
role={isOverflowing ? 'region' : undefined}
286+
aria-label={isOverflowing ? 'Scrollable table' : undefined}
287+
tabIndex={isOverflowing ? 0 : undefined}
288+
className={cn(
289+
'not-prose my-4 w-full overflow-x-auto [&_strong]:font-semibold',
290+
scrollFadeXClass
291+
)}
292+
{...scrollFadeAttributes(edges)}
293+
>
294+
<table className='min-w-full border-collapse [&_tbody_tr:last-child_td]:border-b-0'>
297295
{children}
298-
</OverflowText>
299-
</span>
296+
</table>
297+
</div>
300298
)
301299
}
302300

303301
const MARKDOWN_COMPONENTS = {
304-
table({ children }: { children?: React.ReactNode }) {
305-
return (
306-
<div className='not-prose my-4 w-full overflow-x-auto [&_strong]:font-semibold'>
307-
<table className='min-w-full table-auto border-collapse [&_tbody_tr:last-child_td]:border-b-0'>
308-
{children}
309-
</table>
310-
</div>
311-
)
312-
},
302+
table: MarkdownTable,
313303
thead({ children }: { children?: React.ReactNode }) {
314304
return <thead>{children}</thead>
315305
},
316-
th({ children, style, node }: ThProps & ExtraProps) {
306+
th({ children, style }: ThProps) {
317307
return (
318308
<th
319309
style={{ textAlign: style?.textAlign }}
320310
className='whitespace-nowrap border-[var(--border)] border-b px-3 py-2 text-left font-semibold text-[var(--text-primary)] text-sm leading-6'
321311
>
322-
<TableCellContent node={node}>{children}</TableCellContent>
312+
{children}
323313
</th>
324314
)
325315
},
326-
td({ children, style, node }: TdProps & ExtraProps) {
316+
td({ children, style }: TdProps) {
327317
return (
328318
<td
329319
style={{ textAlign: style?.textAlign }}
330320
className='whitespace-nowrap border-[var(--border)] border-b px-3 py-2 text-[var(--text-primary)] text-sm leading-6'
331321
>
332-
<TableCellContent node={node}>{children}</TableCellContent>
322+
{children}
333323
</td>
334324
)
335325
},
@@ -764,7 +754,7 @@ function ChatContentInner({
764754
<WorkspaceRefsContext.Provider
765755
value={{ resources: workspaceRefs, onSelect: onWorkspaceResourceSelect }}
766756
>
767-
<div className='space-y-3'>
757+
<div className={cn('space-y-3', inter.className)}>
768758
{groups.map((group, i) => {
769759
if (group.kind === 'inline') {
770760
return (

‎apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-presentation.test.tsx‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ async function render(content: string) {
4040
}
4141

4242
describe('mixed chat presentation', () => {
43-
it('preserves table formatting and gives interactive cells their own preview without an overflow tooltip', async () => {
43+
it('preserves full table content and gives links their own preview without competing tooltips', async () => {
4444
const source = JSON.stringify({
4545
url: 'https://example.com/review',
4646
title: 'Release review',
@@ -56,9 +56,10 @@ describe('mixed chat presentation', () => {
5656
'center',
5757
'right',
5858
])
59-
expect(
60-
container.querySelector('td [data-overflow-text] [data-streamdown="strong"]')?.textContent
61-
).toBe('A long document title')
59+
expect(container.querySelector('td [data-streamdown="strong"]')?.textContent).toBe(
60+
'A long document title'
61+
)
62+
expect(container.querySelector('td')?.querySelector('[data-overflow-text]')).toBeNull()
6263
expect(container.querySelector('td code')?.textContent).toBe('search workspace')
6364
expect(container.querySelector('td a[href="https://example.com/guide"] em')?.textContent).toBe(
6465
'the guide'

‎apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/message-sources/message-sources.tsx‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
'use client'
22

3-
import { Popover, PopoverContent, PopoverTrigger, Tooltip } from '@sim/emcn'
3+
import { cn, Popover, PopoverContent, PopoverTrigger, Tooltip } from '@sim/emcn'
44
import { BookOpen } from '@sim/emcn/icons'
5+
import { inter } from '@/app/_styles/fonts/inter/inter'
56
import { SourceCard } from '@/app/workspace/[workspaceId]/home/components/message-content/components/source-card'
67
import type { SourceTagData } from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags'
78

@@ -36,7 +37,12 @@ export function MessageSources({ sources }: MessageSourcesProps) {
3637
</Tooltip.Trigger>
3738
<Tooltip.Content side='top'>{label}</Tooltip.Content>
3839
</Tooltip.Root>
39-
<PopoverContent align='start' side='top' sideOffset={4} className='w-[420px] p-0'>
40+
<PopoverContent
41+
align='start'
42+
side='top'
43+
sideOffset={4}
44+
className={cn('w-[420px] p-0', inter.className)}
45+
>
4046
<div className='flex flex-col py-1'>
4147
{sources.map((source) => (
4248
<SourceCard key={source.url} source={source} dense />

‎apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/source-preview.tsx‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
'use client'
22

33
import { type ReactElement, useEffect, useRef, useState } from 'react'
4-
import { OverflowText, Popover, PopoverAnchor, PopoverContent } from '@sim/emcn'
4+
import { cn, OverflowText, Popover, PopoverAnchor, PopoverContent } from '@sim/emcn'
55
import { ArrowUpRight } from '@sim/emcn/icons'
6+
import { inter } from '@/app/_styles/fonts/inter/inter'
67
import { SourceIcon } from '@/app/workspace/[workspaceId]/home/components/message-content/components/source-chip/source-icon'
78
import {
89
handleExternalLinkClick,
@@ -76,7 +77,7 @@ export function SourcePreview({ source, children }: SourcePreviewProps) {
7677
ref={content}
7778
appearance='tooltip'
7879
maxWidth='min(320px, calc(100vw - 2rem))'
79-
className='w-[320px]'
80+
className={cn('w-[320px]', inter.className)}
8081
sideOffset={8}
8182
aria-label='Source preview'
8283
onFocusOutside={(event) => {

‎apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { useQueryClient } from '@tanstack/react-query'
1717
import { defaultRangeExtractor, type Range, useVirtualizer } from '@tanstack/react-virtual'
1818
import { SMOOTH_CHASE_RATE } from '@/lib/core/utils/smooth-bottom-chase'
1919
import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace'
20+
import { inter } from '@/app/_styles/fonts/inter/inter'
2021
import { MessageActions } from '@/app/workspace/[workspaceId]/components/message-actions'
2122
import { ChatMessageAttachments } from '@/app/workspace/[workspaceId]/home/components/chat-message-attachments'
2223
import { ChatSurfaceProvider } from '@/app/workspace/[workspaceId]/home/components/chat-surface-context'
@@ -814,7 +815,7 @@ export function MothershipChat({
814815
onContextRemove={onContextRemove}
815816
onWorkspaceResourceSelect={onWorkspaceResourceSelect}
816817
>
817-
<div className={cn('flex h-full min-h-0 flex-col', className)}>
818+
<div className={cn('flex h-full min-h-0 flex-col', inter.className, className)}>
818819
<div ref={setScrollElement} className={styles.scrollContainer} onCopy={handleCopy}>
819820
{isLoading && !hasMessages ? (
820821
<MothershipChatSkeleton layout={layout} />

‎apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { getMothershipAttachmentPreviewUrl } from '@/lib/mothership/chat/attachm
1818
import { MOTHERSHIP_ADD_CONTEXT_EVENT } from '@/lib/mothership/events'
1919
import { SIM_RESOURCE_DRAG_TYPE, SIM_RESOURCES_DRAG_TYPE } from '@/lib/mothership/resource-types'
2020
import { MOTHERSHIP_ACCEPT_ATTRIBUTE } from '@/lib/uploads/utils/validation'
21+
import { inter } from '@/app/_styles/fonts/inter/inter'
2122
import { useChatSurface } from '@/app/workspace/[workspaceId]/home/components/chat-surface-context'
2223
import {
2324
AnimatedPlaceholderEffect,
@@ -558,6 +559,7 @@ const UserInputImpl = forwardRef<UserInputHandle, UserInputProps>(function UserI
558559
}}
559560
className={cn(
560561
'relative z-10 mx-auto w-full max-w-chat cursor-text rounded-2xl border border-[var(--border-1)] bg-[var(--white)] px-2.5 py-2 dark:bg-[var(--surface-4)]',
562+
inter.className,
561563
isInitialView && 'shadow-ambient'
562564
)}
563565
onDragEnter={handleDragEnter}

‎apps/sim/app/workspace/[workspaceId]/home/components/user-message-content/user-message-content.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { useMemo } from 'react'
44
import { cn } from '@sim/emcn'
55
import { escapeRegExp } from '@sim/utils/string'
6+
import { inter } from '@/app/_styles/fonts/inter/inter'
67
import { ContextMentionIcon } from '@/app/workspace/[workspaceId]/home/components/context-mention-icon'
78
import { ResourceMention } from '@/app/workspace/[workspaceId]/home/components/message-content/components/resource-mention'
89
import type { ChatMessageContext } from '@/app/workspace/[workspaceId]/home/types'
@@ -126,7 +127,7 @@ export function UserMessageContent({
126127
compact = false,
127128
}: UserMessageContentProps) {
128129
const trimmed = content.trim()
129-
const classes = cn(compact ? COMPACT_CLASSES : USER_MESSAGE_CLASSES, className)
130+
const classes = cn(inter.className, compact ? COMPACT_CLASSES : USER_MESSAGE_CLASSES, className)
130131

131132
const ranges = useMemo(() => computeMentionRanges(content, contexts ?? []), [content, contexts])
132133

‎apps/sim/lib/core/errors/retryable-infrastructure.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const RETRYABLE_NETWORK_ERROR_CODES = new Set([
2222
'ECONNRESET',
2323
'ECONNREFUSED',
2424
'EPIPE',
25+
'ERR_STREAM_PREMATURE_CLOSE',
2526
'ENETDOWN',
2627
'ENETRESET',
2728
'ENETUNREACH',

‎apps/sim/lib/core/security/input-validation.server.ts‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@ import {
3131
} from '@/lib/core/security/egress/validate'
3232
import type { HttpRedirectPolicy } from '@/lib/core/security/http-redirect-policy'
3333
import type { ValidationResult } from '@/lib/core/security/input-validation'
34-
import { nodeReadableToWebStream } from '@/lib/core/utils/node-stream'
34+
import {
35+
createPrematureStreamCloseError,
36+
nodeReadableToWebStream,
37+
} from '@/lib/core/utils/node-stream'
3538
import { PayloadSizeLimitError } from '@/lib/core/utils/stream-limits'
3639

3740
const logger = createLogger('InputValidation')
@@ -918,7 +921,7 @@ async function undiciRequestAsResponse(
918921
signal?.addEventListener('abort', onAbort, { once: true })
919922
body.once('error', (error) => decoder.destroy(error))
920923
body.once('close', () => {
921-
if (!body.readableEnded) decoder.destroy(new Error('Response body closed before completing'))
924+
if (!body.readableEnded) decoder.destroy(createPrematureStreamCloseError())
922925
})
923926
decoder.once('close', () => {
924927
signal?.removeEventListener('abort', onAbort)
@@ -1421,13 +1424,12 @@ export async function secureFetchWithPinnedIP(
14211424
})
14221425
nodeRes.once('error', fail)
14231426
nodeRes.once('close', () => {
1424-
if (!bodySettled) fail(new Error('Response body closed before completing'))
1427+
if (!bodySettled) fail(createPrematureStreamCloseError())
14251428
})
14261429
if (decoder) {
14271430
res.once('error', (error) => decoder.destroy(error))
14281431
res.once('close', () => {
1429-
if (!res.readableEnded)
1430-
decoder.destroy(new Error('Response body closed before completing'))
1432+
if (!res.readableEnded) decoder.destroy(createPrematureStreamCloseError())
14311433
})
14321434
res.pipe(decoder)
14331435
}

0 commit comments

Comments
 (0)