Skip to content

Commit 4bd0731

Browse files
authored
v0.6.2: mothership stability, chat iframe embedding, KB upserts, new blog post
2 parents 4f3bc37 + 60bb942 commit 4bd0731

File tree

93 files changed

+3669
-774
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

93 files changed

+3669
-774
lines changed

apps/sim/app/(home)/components/enterprise/enterprise.tsx

Lines changed: 473 additions & 3 deletions
Large diffs are not rendered by default.

apps/sim/app/(home)/components/features/components/features-preview.tsx

Lines changed: 230 additions & 82 deletions
Large diffs are not rendered by default.

apps/sim/app/(home)/components/pricing/pricing.tsx

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@ const PRICING_TIERS: PricingTier[] = [
2222
features: [
2323
'1,000 credits (trial)',
2424
'5GB file storage',
25+
'3 tables · 1,000 rows each',
2526
'5 min execution limit',
26-
'Limited log retention',
27-
'CLI/SDK Access',
27+
'7-day log retention',
28+
'CLI/SDK/MCP Access',
2829
],
2930
cta: { label: 'Get started', href: '/signup' },
3031
},
@@ -36,11 +37,12 @@ const PRICING_TIERS: PricingTier[] = [
3637
billingPeriod: 'per month',
3738
color: '#00F701',
3839
features: [
39-
'6,000 credits/mo',
40-
'+50 daily refresh credits',
41-
'150 runs/min (sync)',
42-
'50 min sync execution limit',
40+
'6,000 credits/mo · +50/day',
4341
'50GB file storage',
42+
'25 tables · 5,000 rows each',
43+
'50 min execution · 150 runs/min',
44+
'Unlimited log retention',
45+
'CLI/SDK/MCP Access',
4446
],
4547
cta: { label: 'Get started', href: '/signup' },
4648
},
@@ -52,11 +54,12 @@ const PRICING_TIERS: PricingTier[] = [
5254
billingPeriod: 'per month',
5355
color: '#FA4EDF',
5456
features: [
55-
'25,000 credits/mo',
56-
'+200 daily refresh credits',
57-
'300 runs/min (sync)',
58-
'50 min sync execution limit',
57+
'25,000 credits/mo · +200/day',
5958
'500GB file storage',
59+
'25 tables · 5,000 rows each',
60+
'50 min execution · 300 runs/min',
61+
'Unlimited log retention',
62+
'CLI/SDK/MCP Access',
6063
],
6164
cta: { label: 'Get started', href: '/signup' },
6265
},
@@ -66,7 +69,15 @@ const PRICING_TIERS: PricingTier[] = [
6669
description: 'For organizations needing security and scale',
6770
price: 'Custom',
6871
color: '#FFCC02',
69-
features: ['Custom infra limits', 'SSO', 'SOC2', 'Self hosting', 'Dedicated support'],
72+
features: [
73+
'Custom credits & infra limits',
74+
'Custom file storage',
75+
'10,000 tables · 1M rows each',
76+
'Custom execution limits',
77+
'Unlimited log retention',
78+
'SSO & SCIM · SOC2 & HIPAA',
79+
'Self hosting · Dedicated support',
80+
],
7081
cta: { label: 'Book a demo', href: '/contact' },
7182
},
7283
]
@@ -114,12 +125,12 @@ function PricingCard({ tier }: PricingCardProps) {
114125
</p>
115126
<div className='mt-4'>
116127
{isEnterprise ? (
117-
<a
128+
<Link
118129
href={tier.cta.href}
119130
className='flex h-[32px] w-full items-center justify-center rounded-[5px] border border-[#E5E5E5] px-[10px] font-[430] font-season text-[#1C1C1C] text-[14px] transition-colors hover:bg-[#F0F0F0]'
120131
>
121132
{tier.cta.label}
122-
</a>
133+
</Link>
123134
) : isPro ? (
124135
<Link
125136
href={tier.cta.href}

apps/sim/app/(home)/landing.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ import {
2828
* for immediate availability to AI crawlers.
2929
* - Section `id` attributes serve as fragment anchors for precise AI citations.
3030
* - Content ordering prioritizes answer-first patterns: definition (Hero) ->
31-
* examples (Templates) -> capabilities (Features) -> social proof (Collaboration, Testimonials) ->
32-
* pricing (Pricing) -> enterprise (Enterprise).
31+
* examples (Templates) -> capabilities (Features) -> social proof (Collaboration) ->
32+
* enterprise (Enterprise) -> pricing (Pricing) -> testimonials (Testimonials).
3333
*/
3434
export default async function Landing() {
3535
return (
@@ -43,8 +43,8 @@ export default async function Landing() {
4343
<Templates />
4444
<Features />
4545
<Collaboration />
46-
<Pricing />
4746
<Enterprise />
47+
<Pricing />
4848
<Testimonials />
4949
</main>
5050
<Footer />
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
import { randomUUID } from 'crypto'
2+
import { db } from '@sim/db'
3+
import { document } from '@sim/db/schema'
4+
import { createLogger } from '@sim/logger'
5+
import { and, eq, isNull } from 'drizzle-orm'
6+
import { type NextRequest, NextResponse } from 'next/server'
7+
import { z } from 'zod'
8+
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
9+
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
10+
import {
11+
createDocumentRecords,
12+
deleteDocument,
13+
getProcessingConfig,
14+
processDocumentsWithQueue,
15+
} from '@/lib/knowledge/documents/service'
16+
import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils'
17+
import { checkKnowledgeBaseWriteAccess } from '@/app/api/knowledge/utils'
18+
19+
const logger = createLogger('DocumentUpsertAPI')
20+
21+
const UpsertDocumentSchema = z.object({
22+
documentId: z.string().optional(),
23+
filename: z.string().min(1, 'Filename is required'),
24+
fileUrl: z.string().min(1, 'File URL is required'),
25+
fileSize: z.number().min(1, 'File size must be greater than 0'),
26+
mimeType: z.string().min(1, 'MIME type is required'),
27+
documentTagsData: z.string().optional(),
28+
processingOptions: z.object({
29+
chunkSize: z.number().min(100).max(4000),
30+
minCharactersPerChunk: z.number().min(1).max(2000),
31+
recipe: z.string(),
32+
lang: z.string(),
33+
chunkOverlap: z.number().min(0).max(500),
34+
}),
35+
workflowId: z.string().optional(),
36+
})
37+
38+
export async function POST(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
39+
const requestId = randomUUID().slice(0, 8)
40+
const { id: knowledgeBaseId } = await params
41+
42+
try {
43+
const body = await req.json()
44+
45+
logger.info(`[${requestId}] Knowledge base document upsert request`, {
46+
knowledgeBaseId,
47+
hasDocumentId: !!body.documentId,
48+
filename: body.filename,
49+
})
50+
51+
const auth = await checkSessionOrInternalAuth(req, { requireWorkflowId: false })
52+
if (!auth.success || !auth.userId) {
53+
logger.warn(`[${requestId}] Authentication failed: ${auth.error || 'Unauthorized'}`)
54+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
55+
}
56+
const userId = auth.userId
57+
58+
const validatedData = UpsertDocumentSchema.parse(body)
59+
60+
if (validatedData.workflowId) {
61+
const authorization = await authorizeWorkflowByWorkspacePermission({
62+
workflowId: validatedData.workflowId,
63+
userId,
64+
action: 'write',
65+
})
66+
if (!authorization.allowed) {
67+
return NextResponse.json(
68+
{ error: authorization.message || 'Access denied' },
69+
{ status: authorization.status }
70+
)
71+
}
72+
}
73+
74+
const accessCheck = await checkKnowledgeBaseWriteAccess(knowledgeBaseId, userId)
75+
76+
if (!accessCheck.hasAccess) {
77+
if ('notFound' in accessCheck && accessCheck.notFound) {
78+
logger.warn(`[${requestId}] Knowledge base not found: ${knowledgeBaseId}`)
79+
return NextResponse.json({ error: 'Knowledge base not found' }, { status: 404 })
80+
}
81+
logger.warn(
82+
`[${requestId}] User ${userId} attempted to upsert document in unauthorized knowledge base ${knowledgeBaseId}`
83+
)
84+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
85+
}
86+
87+
let existingDocumentId: string | null = null
88+
let isUpdate = false
89+
90+
if (validatedData.documentId) {
91+
const existingDoc = await db
92+
.select({ id: document.id })
93+
.from(document)
94+
.where(
95+
and(
96+
eq(document.id, validatedData.documentId),
97+
eq(document.knowledgeBaseId, knowledgeBaseId),
98+
isNull(document.deletedAt)
99+
)
100+
)
101+
.limit(1)
102+
103+
if (existingDoc.length > 0) {
104+
existingDocumentId = existingDoc[0].id
105+
}
106+
} else {
107+
const docsByFilename = await db
108+
.select({ id: document.id })
109+
.from(document)
110+
.where(
111+
and(
112+
eq(document.filename, validatedData.filename),
113+
eq(document.knowledgeBaseId, knowledgeBaseId),
114+
isNull(document.deletedAt)
115+
)
116+
)
117+
.limit(1)
118+
119+
if (docsByFilename.length > 0) {
120+
existingDocumentId = docsByFilename[0].id
121+
}
122+
}
123+
124+
if (existingDocumentId) {
125+
isUpdate = true
126+
logger.info(
127+
`[${requestId}] Found existing document ${existingDocumentId}, creating replacement before deleting old`
128+
)
129+
}
130+
131+
const createdDocuments = await createDocumentRecords(
132+
[
133+
{
134+
filename: validatedData.filename,
135+
fileUrl: validatedData.fileUrl,
136+
fileSize: validatedData.fileSize,
137+
mimeType: validatedData.mimeType,
138+
...(validatedData.documentTagsData && {
139+
documentTagsData: validatedData.documentTagsData,
140+
}),
141+
},
142+
],
143+
knowledgeBaseId,
144+
requestId
145+
)
146+
147+
const firstDocument = createdDocuments[0]
148+
if (!firstDocument) {
149+
logger.error(`[${requestId}] createDocumentRecords returned empty array unexpectedly`)
150+
return NextResponse.json({ error: 'Failed to create document record' }, { status: 500 })
151+
}
152+
153+
if (existingDocumentId) {
154+
try {
155+
await deleteDocument(existingDocumentId, requestId)
156+
} catch (deleteError) {
157+
logger.error(
158+
`[${requestId}] Failed to delete old document ${existingDocumentId}, rolling back new record`,
159+
deleteError
160+
)
161+
await deleteDocument(firstDocument.documentId, requestId).catch(() => {})
162+
return NextResponse.json({ error: 'Failed to replace existing document' }, { status: 500 })
163+
}
164+
}
165+
166+
processDocumentsWithQueue(
167+
createdDocuments,
168+
knowledgeBaseId,
169+
validatedData.processingOptions,
170+
requestId
171+
).catch((error: unknown) => {
172+
logger.error(`[${requestId}] Critical error in document processing pipeline:`, error)
173+
})
174+
175+
try {
176+
const { PlatformEvents } = await import('@/lib/core/telemetry')
177+
PlatformEvents.knowledgeBaseDocumentsUploaded({
178+
knowledgeBaseId,
179+
documentsCount: 1,
180+
uploadType: 'single',
181+
chunkSize: validatedData.processingOptions.chunkSize,
182+
recipe: validatedData.processingOptions.recipe,
183+
})
184+
} catch (_e) {
185+
// Silently fail
186+
}
187+
188+
recordAudit({
189+
workspaceId: accessCheck.knowledgeBase?.workspaceId ?? null,
190+
actorId: userId,
191+
actorName: auth.userName,
192+
actorEmail: auth.userEmail,
193+
action: isUpdate ? AuditAction.DOCUMENT_UPDATED : AuditAction.DOCUMENT_UPLOADED,
194+
resourceType: AuditResourceType.DOCUMENT,
195+
resourceId: knowledgeBaseId,
196+
resourceName: validatedData.filename,
197+
description: isUpdate
198+
? `Upserted (replaced) document "${validatedData.filename}" in knowledge base "${knowledgeBaseId}"`
199+
: `Upserted (created) document "${validatedData.filename}" in knowledge base "${knowledgeBaseId}"`,
200+
metadata: {
201+
fileName: validatedData.filename,
202+
previousDocumentId: existingDocumentId,
203+
isUpdate,
204+
},
205+
request: req,
206+
})
207+
208+
return NextResponse.json({
209+
success: true,
210+
data: {
211+
documentsCreated: [
212+
{
213+
documentId: firstDocument.documentId,
214+
filename: firstDocument.filename,
215+
status: 'pending',
216+
},
217+
],
218+
isUpdate,
219+
previousDocumentId: existingDocumentId,
220+
processingMethod: 'background',
221+
processingConfig: {
222+
maxConcurrentDocuments: getProcessingConfig().maxConcurrentDocuments,
223+
batchSize: getProcessingConfig().batchSize,
224+
},
225+
},
226+
})
227+
} catch (error) {
228+
if (error instanceof z.ZodError) {
229+
logger.warn(`[${requestId}] Invalid upsert request data`, { errors: error.errors })
230+
return NextResponse.json(
231+
{ error: 'Invalid request data', details: error.errors },
232+
{ status: 400 }
233+
)
234+
}
235+
236+
logger.error(`[${requestId}] Error upserting document`, error)
237+
238+
const errorMessage = error instanceof Error ? error.message : 'Failed to upsert document'
239+
const isStorageLimitError =
240+
errorMessage.includes('Storage limit exceeded') || errorMessage.includes('storage limit')
241+
const isMissingKnowledgeBase = errorMessage === 'Knowledge base not found'
242+
243+
return NextResponse.json(
244+
{ error: errorMessage },
245+
{ status: isMissingKnowledgeBase ? 404 : isStorageLimitError ? 413 : 500 }
246+
)
247+
}
248+
}

apps/sim/app/api/mothership/chat/route.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,7 @@ export async function POST(req: NextRequest) {
279279
role: 'assistant' as const,
280280
content: result.content,
281281
timestamp: new Date().toISOString(),
282+
...(result.requestId ? { requestId: result.requestId } : {}),
282283
}
283284
if (result.toolCalls.length > 0) {
284285
assistantMessage.toolCalls = result.toolCalls

apps/sim/app/workspace/[workspaceId]/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export { ErrorState, type ErrorStateProps } from './error'
22
export { InlineRenameInput } from './inline-rename-input'
3+
export { MessageActions } from './message-actions'
34
export { ownerCell } from './resource/components/owner-cell/owner-cell'
45
export type {
56
BreadcrumbEditing,
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { MessageActions } from './message-actions'

0 commit comments

Comments
 (0)