Skip to content

Commit dfcb4a9

Browse files
committed
improvement(chat): unify message styling and source previews
1 parent 99b1702 commit dfcb4a9

30 files changed

Lines changed: 860 additions & 426 deletions

‎apps/sim/app/(landing)/components/hero/components/hero-chat-loop/hero-chat-reply.tsx‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,7 @@ export function HeroChatReply({ content, onOpenWorkflowResource }: HeroChatReply
2424
<>
2525
{paragraph.slice(0, resourceIndex)}
2626
<ResourceMention
27-
icon={
28-
<Workflow className='relative top-0.5 size-[12px] shrink-0 text-[var(--text-icon)]' />
29-
}
27+
icon={<Workflow className='size-[12px] shrink-0 text-[var(--text-icon)]' />}
3028
title={WORKFLOW_TITLE}
3129
onSelect={onOpenWorkflowResource}
3230
/>

‎apps/sim/app/api/link-preview/route.ts‎

Lines changed: 4 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import { createHash } from 'crypto'
22
import { createLogger } from '@sim/logger'
33
import { getErrorMessage } from '@sim/utils/errors'
4-
import { truncate } from '@sim/utils/string'
5-
import * as cheerio from 'cheerio'
64
import type { NextRequest } from 'next/server'
75
import { NextResponse } from 'next/server'
86
import type { LinkPreview } from '@/lib/api/contracts/link-preview'
@@ -12,66 +10,14 @@ import { getSession } from '@/lib/auth'
1210
import { getRedisClient } from '@/lib/core/config/redis'
1311
import { runWithOutboundOrganization } from '@/lib/core/network/context.server'
1412
import { enforceUserRateLimit } from '@/lib/core/rate-limiter/route-helpers'
15-
import { secureFetchWithValidation } from '@/lib/core/security/input-validation.server'
1613
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
14+
import { fetchLinkPreview } from '@/lib/link-preview/fetch-preview'
1715

1816
const logger = createLogger('LinkPreviewAPI')
1917

20-
const FETCH_TIMEOUT_MS = 5000
21-
const MAX_RESPONSE_BYTES = 256 * 1024
22-
const MAX_REDIRECTS = 3
23-
const TITLE_MAX_CHARS = 200
24-
const DESCRIPTION_MAX_CHARS = 300
2518
const CACHE_TTL_SECONDS = 24 * 60 * 60
2619
const NEGATIVE_CACHE_TTL_SECONDS = 60 * 60
27-
const CACHE_KEY_PREFIX = 'link-preview:v1:'
28-
29-
/**
30-
* Parses preview metadata from the fetched document (already capped at
31-
* MAX_RESPONSE_BYTES); cheerio handles attribute order, quoting, and entity
32-
* decoding.
33-
*/
34-
function parsePreview(html: string): LinkPreview {
35-
const $ = cheerio.load(html)
36-
37-
const meta = (key: string): string | null => {
38-
const value = $(`meta[property="${key}"], meta[name="${key}"]`).first().attr('content')
39-
return value?.trim() || null
40-
}
41-
42-
const title =
43-
meta('og:title') ?? meta('twitter:title') ?? ($('title').first().text().trim() || null)
44-
const description = meta('og:description') ?? meta('twitter:description') ?? meta('description')
45-
const siteName = meta('og:site_name')
46-
47-
if (!title && !description && !siteName) return null
48-
return {
49-
title: title ? truncate(title, TITLE_MAX_CHARS) : null,
50-
description: description ? truncate(description, DESCRIPTION_MAX_CHARS) : null,
51-
siteName: siteName ? truncate(siteName, TITLE_MAX_CHARS) : null,
52-
}
53-
}
54-
55-
async function fetchPreview(url: string): Promise<LinkPreview> {
56-
const response = await secureFetchWithValidation(url, {
57-
// The URL is harvested from a rendered link rather than authored as a
58-
// destination, so it gets no reach into a private network.
59-
profile: 'contentFetch',
60-
timeout: FETCH_TIMEOUT_MS,
61-
maxRedirects: MAX_REDIRECTS,
62-
maxResponseBytes: MAX_RESPONSE_BYTES,
63-
headers: {
64-
'User-Agent': 'Simbot/1.0 (+https://sim.ai)',
65-
Accept: 'text/html,application/xhtml+xml',
66-
},
67-
})
68-
if (response.status < 200 || response.status >= 300) return null
69-
const contentType = response.headers.get('content-type') ?? ''
70-
if (!contentType.includes('text/html') && !contentType.includes('application/xhtml+xml')) {
71-
return null
72-
}
73-
return parsePreview(await response.text())
74-
}
20+
const CACHE_KEY_PREFIX = 'link-preview:v2:'
7521

7622
export const GET = withRouteHandler(async (request: NextRequest) => {
7723
const session = await getSession()
@@ -106,8 +52,9 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
10652
let preview: LinkPreview = null
10753
try {
10854
/** Link previews have no organization owner and use a shared URL cache. */
109-
preview = await runWithOutboundOrganization(null, () => fetchPreview(url))
55+
preview = await runWithOutboundOrganization(null, () => fetchLinkPreview(url, request.signal))
11056
} catch (error) {
57+
if (request.signal.aborted) return new NextResponse(null, { status: 499 })
11158
logger.info('Link preview fetch failed; returning null preview', {
11259
host: new URL(url).hostname,
11360
error: getErrorMessage(error, 'unknown error').replaceAll(url, '[url]'),

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

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
1-
import { cn, Lightbox } from '@sim/emcn'
1+
import { ChipTag, cn, Lightbox, OverflowText } from '@sim/emcn'
22
import { getDocumentIcon } from '@/components/icons/document-icons'
33
import type { ChatMessageAttachment } from '@/app/workspace/[workspaceId]/home/types'
44

55
function FileAttachmentPill(props: { mediaType: string; filename: string }) {
66
const Icon = getDocumentIcon(props.mediaType, props.filename)
77
return (
8-
<div className='flex max-w-[140px] items-center gap-[5px] rounded-lg bg-[var(--surface-5)] px-[6px] py-[3px]'>
9-
<Icon className='size-[14px] shrink-0 text-[var(--text-icon)]' />
10-
<span className='truncate text-[var(--text-body)] text-xs'>{props.filename}</span>
11-
</div>
8+
<ChipTag variant='mono' leftIcon={Icon} className='max-w-[140px]'>
9+
<OverflowText label={props.filename} />
10+
</ChipTag>
1211
)
1312
}
1413

‎apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/activity-disclosure.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export function ActivityDisclosure({
5959
{collapsible && (
6060
<Expandable expanded={expanded}>
6161
<ExpandableContent id={contentId}>
62-
<div className='pt-1.5'>
62+
<div className='pt-2'>
6363
<ActivityViewport isStreaming={isStreaming} unbounded={unbounded}>
6464
{children}
6565
</ActivityViewport>

‎apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/activity-layout.test.tsx‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ describe('flat expanded activity layout', () => {
8080
expect(iconSlot(row).classList).toContain(ICON_SLOT)
8181
}
8282
const list = rows[0].closest('.flex-col')!
83-
expect(list.classList).toContain('gap-1.5')
83+
expect(list.classList).toContain('gap-2')
8484
let node: Element | null = rows[0]
8585
while (node && node !== container) {
8686
expect(hasIndent(node), node.className).toBe(false)
@@ -134,16 +134,15 @@ describe('flat expanded activity layout', () => {
134134
},
135135
},
136136
])
137-
const blocks = container.querySelector('.flex-col.gap-3')!
137+
const blocks = container.querySelector('.flex-col.gap-2')!
138138
expect(blocks.contains(statuses()[0])).toBe(true)
139-
expect(blocks.classList).toContain('gap-3')
140-
expect(blocks.classList).not.toContain('gap-1.5')
139+
expect(blocks.classList).toContain('gap-2')
141140
expect(statuses()).toHaveLength(1)
142141
expand()
143142
const rows = statuses().slice(1)
144143
expect(rows).toHaveLength(3)
145144
for (const row of rows) {
146-
expect(row.closest('.flex-col')!.classList).toContain('gap-1.5')
145+
expect(row.closest('.flex-col')!.classList).toContain('gap-2')
147146
expect(iconSlot(row).classList).toContain(ICON_SLOT)
148147
}
149148
})

‎apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/activity-viewport.tsx‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,7 @@ export function ActivityViewport({
8585
className={cn(
8686
'pr-2',
8787
!unbounded && 'scrollbar-hide max-h-[110px] overflow-y-auto',
88-
scrollFadeClass,
89-
(edges.top || edges.bottom) && 'py-1'
88+
scrollFadeClass
9089
)}
9190
{...scrollFadeAttributes(edges)}
9291
>

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ export function AgentGroupView({
280280
liveToolId={liveCall?.id}
281281
/>
282282
) : (
283-
<div className='flex min-w-0 flex-col gap-1.5 py-0.5'>{items.map(renderItem)}</div>
283+
<div className='flex min-w-0 flex-col gap-2'>{items.map(renderItem)}</div>
284284
)
285285
const headerText = error
286286
? agentLabel
@@ -308,7 +308,7 @@ export function AgentGroupView({
308308
)
309309

310310
return (
311-
<div className='flex min-w-0 flex-col gap-1.5'>
311+
<div className='flex min-w-0 flex-col gap-2'>
312312
{isMainAgent ? (
313313
activity
314314
) : (

‎apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/main-agent-activity.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,5 +49,5 @@ export function MainAgentActivity({
4949
)
5050
})
5151

52-
return <div className='flex min-w-0 flex-col gap-3'>{activity}</div>
52+
return <div className='flex min-w-0 flex-col gap-2'>{activity}</div>
5353
}

‎apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/search-activity-results.tsx‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@ import {
99
scrollFadeClass,
1010
useScrollEdges,
1111
} from '@sim/emcn'
12-
import {
13-
externalLinkHostname,
14-
handleExternalLinkClick,
15-
} from '@/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/external-link'
1612
import {
1713
SourceIcon,
1814
sourceLabel,
1915
sourceSiteName,
2016
} from '@/app/workspace/[workspaceId]/home/components/message-content/components/source-chip'
17+
import {
18+
externalLinkHostname,
19+
handleExternalLinkClick,
20+
} from '@/app/workspace/[workspaceId]/home/components/message-content/components/source-link'
2121
import type { SourceTagData } from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags'
2222

2323
interface SearchActivityResultsProps {

‎apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-activity-group.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ export function ToolActivityGroup({
242242
isStreaming={working && autoScrollActivity}
243243
unbounded={entries.some(({ sources }) => (sources?.length ?? 0) > 0)}
244244
>
245-
<div className='flex min-w-0 flex-col gap-1.5 py-0.5'>
245+
<div className='flex min-w-0 flex-col gap-2'>
246246
{entries.map(({ tool, sources }, index) => (
247247
<Fragment key={tool.id}>
248248
{tools.length === 1 ? null : tool.id === headerTool.id ? (

0 commit comments

Comments
 (0)