Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions apps/docs/app/api/og/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,12 @@ function splitOversizedWord(word: string, maxWidthEm: number): string[] {
* so it sidesteps the bug instead of fighting Satori's own line-wrapping
* (which is also disabled here — lines are pre-split, not auto-wrapped).
*/
function wrapTitleLines(title: string, fontSize: number): string[] {
interface TitleLine {
text: string
sourceOffset: number
}

function wrapTitleLines(title: string, fontSize: number): TitleLine[] {
const maxWidthEm = TITLE_BOX_WIDTH / fontSize
const words = title.split(' ')
const lines: string[] = []
Expand Down Expand Up @@ -130,7 +135,12 @@ function wrapTitleLines(title: string, fontSize: number): string[] {
}
if (current) lines.push(current)

return lines.map((line) => line.replace(/ /g, ' '))
let lineOffset = 0
return lines.map((line) => {
const sourceOffset = lineOffset
lineOffset += line.length + 1
return { text: line.replace(/ /g, ' '), sourceOffset }
})
}

/**
Expand Down Expand Up @@ -211,8 +221,8 @@ export async function GET(request: NextRequest) {
</div>

<div style={getTitleStyle(title)}>
{titleLines.map((line, index) => (
<span key={index}>{line}</span>
{titleLines.map((line) => (
<span key={line.sourceOffset}>{line.text}</span>
))}
</div>
</div>,
Expand Down
9 changes: 6 additions & 3 deletions apps/docs/components/workflow-preview/format-references.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,19 @@ const REFERENCE_PATTERN = /(<[^<>]+>|\{\{[^{}]+\}\})/g
*/
export function formatReferences(text: string): ReactNode[] {
if (!text) return []
return text.split(REFERENCE_PATTERN).map((part, index) => {
let sourceOffset = 0
return text.split(REFERENCE_PATTERN).map((part) => {
const partOffset = sourceOffset
sourceOffset += part.length
if (!part) return null
const isReference =
(part.startsWith('<') && part.endsWith('>')) || (part.startsWith('{{') && part.endsWith('}}'))
return isReference ? (
<span key={index} className='text-[var(--brand-secondary)]'>
<span key={partOffset} className='text-[var(--brand-secondary)]'>
{part}
</span>
) : (
<span key={index}>{part}</span>
<span key={partOffset}>{part}</span>
)
})
}
23 changes: 17 additions & 6 deletions apps/sim/app/(interfaces)/chat/components/input/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ interface AttachedFile {
dataUrl?: string
}

interface UploadError {
id: string
message: string
}

export const ChatInput: React.FC<{
onSubmit?: (value: string, files?: AttachedFile[]) => void
isStreaming?: boolean
Expand All @@ -30,7 +35,7 @@ export const ChatInput: React.FC<{
const textareaRef = useRef<HTMLTextAreaElement>(null)
const [inputValue, setInputValue] = useState('')
const [attachedFiles, setAttachedFiles] = useState<AttachedFile[]>([])
const [uploadErrors, setUploadErrors] = useState<string[]>([])
const [uploadErrors, setUploadErrors] = useState<UploadError[]>([])
const [dragCounter, setDragCounter] = useState(0)
const isDragOver = dragCounter > 0

Expand All @@ -54,15 +59,21 @@ export const ChatInput: React.FC<{
const file = selectedFiles[i]

if (file.size > maxSize) {
setUploadErrors((prev) => [...prev, `${file.name} is too large (max 10MB)`])
setUploadErrors((prev) => [
...prev,
{ id: generateId(), message: `${file.name} is too large (max 10MB)` },
])
continue
}

const isDuplicate = attachedFiles.some(
(existing) => existing.name === file.name && existing.size === file.size
)
if (isDuplicate) {
setUploadErrors((prev) => [...prev, `${file.name} already added`])
setUploadErrors((prev) => [
...prev,
{ id: generateId(), message: `${file.name} already added` },
])
continue
}

Expand Down Expand Up @@ -128,9 +139,9 @@ export const ChatInput: React.FC<{
<div className='w-full max-w-3xl md:max-w-[748px]'>
{uploadErrors.length > 0 && (
<div className='mb-3 flex flex-col gap-2'>
{uploadErrors.map((error, idx) => (
<Badge key={`${error}-${idx}`} variant='red' size='lg' dot className='max-w-full'>
{error}
{uploadErrors.map((error) => (
<Badge key={error.id} variant='red' size='lg' dot className='max-w-full'>
{error.message}
</Badge>
))}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,11 @@ const Caret = () => (
function PromptAtoms({ atoms }: { atoms: PromptAtom[] }) {
return (
<>
{atoms.map((atom, i) =>
{atoms.map((atom) =>
atom.kind === 'char' ? (
<span key={`${i}-${atom.char}`}>{atom.char}</span>
<span key={atom.id}>{atom.char}</span>
) : (
<span key={`${i}-${atom.label}`}>
<span key={atom.id}>
<span className='relative'>
<span className='invisible'>@</span>
<atom.icon className='absolute inset-0 m-auto size-[12px] translate-y-[1.25px] text-[var(--text-icon)]' />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -238,8 +238,8 @@ export const SCENE_JIRA_FOCUS_TRANSLATE = { x: -645, y: 0 } as const
* `@GitHub` / `@Jira` mention.
*/
export type PromptAtom =
| { kind: 'char'; char: string }
| { kind: 'mention'; label: string; icon: IconComponent }
| { id: string; kind: 'char'; char: string }
| { id: string; kind: 'mention'; label: string; icon: IconComponent }

const PROMPT_SEGMENTS: Array<string | { label: string; icon: IconComponent }> = [
'Create me a ',
Expand All @@ -248,11 +248,26 @@ const PROMPT_SEGMENTS: Array<string | { label: string; icon: IconComponent }> =
{ label: 'Jira', icon: JiraIcon },
]

export const PROMPT_ATOMS: PromptAtom[] = PROMPT_SEGMENTS.flatMap((seg) =>
typeof seg === 'string'
? [...seg].map((char): PromptAtom => ({ kind: 'char', char }))
: [{ kind: 'mention', label: seg.label, icon: seg.icon } as PromptAtom]
)
let promptSourceOffset = 0
export const PROMPT_ATOMS: PromptAtom[] = PROMPT_SEGMENTS.flatMap((seg) => {
if (typeof seg === 'string') {
const atoms = [...seg].map((char): PromptAtom => {
const id = `char-${promptSourceOffset}`
promptSourceOffset += char.length
return { id, kind: 'char', char }
})
return atoms
}

const atom: PromptAtom = {
id: `mention-${promptSourceOffset}-${seg.label}`,
kind: 'mention',
label: seg.label,
icon: seg.icon,
}
promptSourceOffset += seg.label.length + 1
return [atom]
})

/** Greeting shown above the input in the home state (matches the Mothership home). */
export const HOME_GREETING = 'What should we get done?'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -287,22 +287,27 @@ export const PreviewBlockNode = memo(function PreviewBlockNode({
* Supports ### headings, **bold**, _italic_, --- rules, and blank-line spacing.
*/
function NoteMarkdown({ content }: { content: string }) {
const lines = content.split('\n')
let sourceOffset = 0
const lines = content.split('\n').map((text) => {
const line = { sourceOffset, text }
sourceOffset += text.length + 1
return line
})

return (
<div className='flex flex-col gap-1'>
{lines.map((line, i) => {
const trimmed = line.trim()
if (!trimmed) return <div key={`${line}-${i}`} className='h-[4px]' />
{lines.map((line) => {
const trimmed = line.text.trim()
if (!trimmed) return <div key={line.sourceOffset} className='h-[4px]' />

if (trimmed === '---') {
return <hr key={`${line}-${i}`} className='my-1 border-[var(--border)] border-t' />
return <hr key={line.sourceOffset} className='my-1 border-[var(--border)] border-t' />
}

if (trimmed.startsWith('### ')) {
return (
<p
key={`${line}-${i}`}
key={line.sourceOffset}
className='font-semibold text-[16px] text-[var(--text-primary)] leading-[1.3]'
>
{trimmed.slice(4)}
Expand All @@ -312,7 +317,7 @@ function NoteMarkdown({ content }: { content: string }) {

return (
<p
key={`${line}-${i}`}
key={line.sourceOffset}
className='font-medium text-[13px] text-[var(--text-primary)] leading-[1.5]'
dangerouslySetInnerHTML={{
__html: trimmed
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { cn } from '@sim/emcn'
import { extractTextContent } from '@/lib/core/utils/react-node-text'
import { PROSE_SPACING, PROSE_TYPE } from '@/app/(landing)/components/prose-page/constants'
import type { LegalBlock } from '@/app/(landing)/components/prose-page/types'

Expand All @@ -21,19 +22,23 @@ export function LegalBlockView({ block }: LegalBlockViewProps) {
return <p className={PROSE_TYPE.body}>{block.content}</p>
case 'subheading':
return <h3 className={PROSE_TYPE.h3}>{block.text}</h3>
case 'list':
case 'list': {
const itemOccurrences = new Map<string, number>()
return (
<ul className={cn('list-disc', PROSE_SPACING.listIndent, PROSE_SPACING.listStack)}>
{block.items.map((item, index) => {
const itemKey = `item-${index}`
{block.items.map((item) => {
const signature = extractTextContent(item)
const occurrence = itemOccurrences.get(signature) ?? 0
itemOccurrences.set(signature, occurrence + 1)
return (
<li key={itemKey} className={PROSE_TYPE.list}>
<li key={`${signature}:${occurrence}`} className={PROSE_TYPE.list}>
{item}
</li>
)
})}
</ul>
)
}
case 'callout':
return <div className={PROSE_TYPE.callout}>{block.content}</div>
case 'table':
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { cn } from '@sim/emcn'
import { extractTextContent } from '@/lib/core/utils/react-node-text'
import { LegalBlockView } from '@/app/(landing)/components/prose-page/components/legal-block-group/components'
import { PROSE_SPACING } from '@/app/(landing)/components/prose-page/constants'
import type { LegalBlock } from '@/app/(landing)/components/prose-page/types'
Expand All @@ -16,11 +17,21 @@ interface LegalBlockGroupProps {
}

export function LegalBlockGroup({ blocks }: LegalBlockGroupProps) {
const blockOccurrences = new Map<string, number>()
return (
<div className={cn('flex flex-col', PROSE_SPACING.blockStack)}>
{blocks.map((block, index) => (
<LegalBlockView key={`${block.kind}-${index}`} block={block} />
))}
{blocks.map((block) => {
const content =
block.kind === 'subheading'
? block.text
: block.kind === 'list'
? block.items.map(extractTextContent).join('\u0000')
: extractTextContent(block.content)
const signature = `${block.kind}:${content}`
const occurrence = blockOccurrences.get(signature) ?? 0
blockOccurrences.set(signature, occurrence + 1)
return <LegalBlockView key={`${signature}:${occurrence}`} block={block} />
})}
</div>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ interface CodeSegment {
}

/** The `support-agent.ts` contents, split into tone-colored typewriter segments. */
const CODE_LINES: CodeSegment[][] = [
const RAW_CODE_LINES: CodeSegment[][] = [
[
{ text: 'import', tone: 'muted' },
{ text: ' ' },
Expand Down Expand Up @@ -52,8 +52,24 @@ const CODE_LINES: CodeSegment[][] = [
[{ text: ' })' }],
]

const codeLineOccurrences = new Map<string, number>()
const CODE_LINES = RAW_CODE_LINES.map((segments) => {
const text = segments.map((segment) => segment.text).join('')
const occurrence = codeLineOccurrences.get(text) ?? 0
codeLineOccurrences.set(text, occurrence + 1)
let sourceOffset = 0
return {
id: `${text}:${occurrence}`,
segments: segments.map((segment) => {
const id = `${sourceOffset}:${segment.text.length}`
sourceOffset += segment.text.length
return { ...segment, id }
}),
}
})

const CODE_LINE_LENGTHS = CODE_LINES.map((line) =>
line.reduce((total, segment) => total + segment.text.length, 0)
line.segments.reduce((total, segment) => total + segment.text.length, 0)
)
const CODE_LINE_STARTS = CODE_LINE_LENGTHS.map((_, index) =>
CODE_LINE_LENGTHS.slice(0, index).reduce((total, length) => total + length, 0)
Expand Down Expand Up @@ -93,13 +109,13 @@ const SEGMENT_TONE_CLASS = {
} as const

/** Renders one code line clipped to the number of characters typed so far. */
function renderCodeLine(segments: CodeSegment[], visibleChars: number) {
function renderCodeLine(segments: Array<CodeSegment & { id: string }>, visibleChars: number) {
const rendered = []
let remaining = visibleChars
for (let index = 0; index < segments.length && remaining > 0; index++) {
const segment = segments[index]
rendered.push(
<span key={index} className={segment.tone && SEGMENT_TONE_CLASS[segment.tone]}>
<span key={segment.id} className={segment.tone && SEGMENT_TONE_CLASS[segment.tone]}>
{segment.text.slice(0, remaining)}
</span>
)
Expand Down Expand Up @@ -255,12 +271,12 @@ export function BuildMethodsGraphic() {
<div className='min-h-[190px] space-y-2 p-4 font-mono text-caption leading-[1.7]'>
{CODE_LINES.map((line, index) =>
typedCodeChars > CODE_LINE_STARTS[index] ? (
<div key={index} className='flex gap-3'>
<div key={line.id} className='flex gap-3'>
<span className='w-3 select-none text-right text-[var(--text-muted)]'>
{index + 1}
</span>
<code>
{renderCodeLine(line, typedCodeChars - CODE_LINE_STARTS[index])}
{renderCodeLine(line.segments, typedCodeChars - CODE_LINE_STARTS[index])}
{codeTypingActive && index === lastStartedLine && (
<span className='ml-px inline-block h-[1.1em] w-px translate-y-[2px] animate-pulse bg-[var(--text-primary)] align-text-bottom' />
)}
Expand Down
Loading
Loading