Skip to content

Commit 70fc328

Browse files
committed
Merge remote-tracking branch 'origin/staging' into waleedlatif1/logs-traces-fix
# Conflicts: # apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx
2 parents c92ea70 + 2f90e41 commit 70fc328

21 files changed

Lines changed: 309 additions & 52 deletions

File tree

apps/sim/.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ API_ENCRYPTION_KEY=your_api_encryption_key # Use `openssl rand -hex 32` to gener
3535
# AZURE_ANTHROPIC_API_KEY= # Azure Anthropic API key
3636
# AZURE_ANTHROPIC_API_VERSION= # Azure Anthropic API version (e.g., 2023-06-01)
3737
# NEXT_PUBLIC_AZURE_CONFIGURED=true # Set when Azure credentials are pre-configured above. Hides endpoint/key/version fields in Agent block UI.
38+
# COHERE_API_KEY= # Cohere API key for the Knowledge block reranker (rerank-v4.0-pro/-fast, rerank-v3.5). Alternatively set COHERE_API_KEY_1/2/3 for rotation.
39+
# NEXT_PUBLIC_COHERE_CONFIGURED=true # Set when COHERE_API_KEY (or rotation keys) are pre-configured above. Hides the Cohere API Key field on the Knowledge block UI.
3840

3941
# Admin API (Optional - for self-hosted GitOps)
4042
# ADMIN_API_KEY= # Use `openssl rand -hex 32` to generate. Enables admin API for workflow export/import.

apps/sim/app/api/knowledge/search/route.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -247,9 +247,21 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
247247

248248
const hasFilters = structuredFilters && structuredFilters.length > 0
249249

250-
/** Oversample candidates when reranking so the reranker has more to choose from.
251-
* Cap at 100 to bound Cohere request cost (1 search unit = ≤100 docs). */
252-
const candidateTopK = useReranker ? Math.min(100, validatedData.topK * 4) : validatedData.topK
250+
/** Oversample vector results when reranking so the reranker has more to choose from.
251+
* Cap at 100 to bound Cohere request cost (1 search unit = ≤100 docs). When the caller
252+
* supplies `rerankerInputCount`, honor it but never let it drop below `topK`
253+
* (which would defeat the purpose) or exceed 100 (which would split into >1 search units). */
254+
const rawInputCount = validatedData.rerankerInputCount
255+
if (useReranker && rawInputCount !== undefined && rawInputCount < validatedData.topK) {
256+
logger.warn(
257+
`[${requestId}] rerankerInputCount (${rawInputCount}) is below topK (${validatedData.topK}); raising to topK`
258+
)
259+
}
260+
const candidateTopK = useReranker
261+
? rawInputCount !== undefined
262+
? Math.min(100, Math.max(validatedData.topK, rawInputCount))
263+
: Math.min(100, validatedData.topK * 4)
264+
: validatedData.topK
253265

254266
if (!hasQuery && hasFilters) {
255267
results = await handleTagOnlySearch({
@@ -300,7 +312,12 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
300312
const { results: ranked, isBYOK } = await rerank(
301313
validatedData.query!,
302314
results.map((r) => ({ id: r.id, text: r.content })),
303-
{ model: rerankerModel, topN: validatedData.topK, workspaceId }
315+
{
316+
model: rerankerModel,
317+
topN: validatedData.topK,
318+
workspaceId,
319+
apiKey: validatedData.rerankerApiKey,
320+
}
304321
)
305322
rerankBilled = true
306323
rerankIsBYOK = isBYOK

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

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,36 @@ export function ChatMessageAttachments(props: {
3030
)}
3131
>
3232
{attachments.map((att) => {
33-
const isImage = att.media_type.startsWith('image/')
34-
return isImage && att.previewUrl ? (
33+
if (!att.previewUrl) {
34+
return (
35+
<FileAttachmentPill key={att.id} mediaType={att.media_type} filename={att.filename} />
36+
)
37+
}
38+
const isVideo = att.media_type.startsWith('video/')
39+
if (isVideo) {
40+
const Icon = getDocumentIcon(att.media_type, att.filename)
41+
return (
42+
<div
43+
key={att.id}
44+
className='relative h-[56px] w-[56px] overflow-hidden rounded-[8px] bg-[var(--surface-5)]'
45+
>
46+
<div className='absolute inset-0 flex items-center justify-center text-[var(--text-icon)]'>
47+
<Icon className='h-[18px] w-[18px]' />
48+
</div>
49+
<video
50+
src={att.previewUrl}
51+
muted
52+
playsInline
53+
preload='metadata'
54+
className='relative h-full w-full object-cover'
55+
/>
56+
</div>
57+
)
58+
}
59+
return (
3560
<div key={att.id} className='h-[56px] w-[56px] overflow-hidden rounded-[8px]'>
3661
<img src={att.previewUrl} alt={att.filename} className='h-full w-full object-cover' />
3762
</div>
38-
) : (
39-
<FileAttachmentPill key={att.id} mediaType={att.media_type} filename={att.filename} />
4063
)
4164
})}
4265
</div>

apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client'
22

3-
import { lazy, memo, Suspense, useEffect, useMemo } from 'react'
3+
import { lazy, memo, Suspense, useEffect, useMemo, useRef } from 'react'
44
import { createLogger } from '@sim/logger'
55
import { Square } from 'lucide-react'
66
import { useRouter } from 'next/navigation'
@@ -13,6 +13,7 @@ import {
1313
SquareArrowUpRight,
1414
WorkflowX,
1515
} from '@/components/emcn/icons'
16+
import { isApiClientError } from '@/lib/api/client/errors'
1617
import type { FilePreviewSession } from '@/lib/copilot/request/session'
1718
import {
1819
cancelRunToolExecution,
@@ -70,6 +71,7 @@ interface ResourceContentProps {
7071
previewSession?: FilePreviewSession | null
7172
genericResourceData?: GenericResourceData
7273
previewContextKey?: string
74+
onNotFound?: (resourceId: string) => void
7375
}
7476

7577
/**
@@ -86,6 +88,7 @@ export const ResourceContent = memo(function ResourceContent({
8688
previewSession,
8789
genericResourceData,
8890
previewContextKey,
91+
onNotFound,
8992
}: ResourceContentProps) {
9093
const streamFileName = previewSession?.fileName || 'file.md'
9194
const syntheticFile = useMemo(() => {
@@ -179,7 +182,14 @@ export const ResourceContent = memo(function ResourceContent({
179182
return <EmbeddedFolder key={resource.id} workspaceId={workspaceId} folderId={resource.id} />
180183

181184
case 'log':
182-
return <EmbeddedLog key={resource.id} workspaceId={workspaceId} logId={resource.id} />
185+
return (
186+
<EmbeddedLog
187+
key={resource.id}
188+
workspaceId={workspaceId}
189+
logId={resource.id}
190+
onNotFound={onNotFound ? () => onNotFound(resource.id) : undefined}
191+
/>
192+
)
183193

184194
case 'generic':
185195
return (
@@ -619,10 +629,20 @@ function EmbeddedFolder({ workspaceId, folderId }: EmbeddedFolderProps) {
619629
interface EmbeddedLogProps {
620630
workspaceId: string
621631
logId: string
632+
onNotFound?: () => void
622633
}
623634

624-
function EmbeddedLog({ workspaceId, logId }: EmbeddedLogProps) {
625-
const { data: log, isLoading } = useLogDetail(logId, workspaceId)
635+
function EmbeddedLog({ workspaceId, logId, onNotFound }: EmbeddedLogProps) {
636+
const { data: log, isLoading, error } = useLogDetail(logId, workspaceId)
637+
638+
const onNotFoundRef = useRef(onNotFound)
639+
onNotFoundRef.current = onNotFound
640+
641+
useEffect(() => {
642+
if (isApiClientError(error) && error.status === 404) {
643+
onNotFoundRef.current?.()
644+
}
645+
}, [error])
626646

627647
if (isLoading) return LOADING_SKELETON
628648

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ export const MothershipView = memo(
128128
previewSession={previewForActive}
129129
genericResourceData={active.type === 'generic' ? genericResourceData : undefined}
130130
previewContextKey={chatId}
131+
onNotFound={(resourceId) => onRemoveResource('log', resourceId)}
131132
/>
132133
) : (
133134
<div className='flex h-full items-center justify-center text-[var(--text-muted)] text-sm'>

apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list.tsx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,32 @@ export const AttachedFilesList = React.memo(function AttachedFilesList({
2222
return (
2323
<div className='mb-1.5 flex flex-wrap gap-1.5'>
2424
{attachedFiles.map((file) => {
25-
const isImage = file.type.startsWith('image/')
25+
const isVideo = file.type.startsWith('video/')
26+
const hasPreview = Boolean(file.previewUrl)
2627
return (
2728
<Tooltip.Root key={file.id}>
2829
<Tooltip.Trigger asChild>
2930
<div
3031
className='group relative h-[56px] w-[56px] flex-shrink-0 cursor-pointer overflow-hidden rounded-[8px] border border-[var(--border-1)] bg-[var(--surface-5)] hover:bg-[var(--surface-4)]'
3132
onClick={() => onFileClick(file)}
3233
>
33-
{isImage && file.previewUrl ? (
34+
{hasPreview && isVideo ? (
35+
<>
36+
<div className='absolute inset-0 flex items-center justify-center text-[var(--text-icon)]'>
37+
{(() => {
38+
const Icon = getDocumentIcon(file.type, file.name)
39+
return <Icon className='h-[18px] w-[18px]' />
40+
})()}
41+
</div>
42+
<video
43+
src={file.previewUrl}
44+
muted
45+
playsInline
46+
preload='metadata'
47+
className='relative h-full w-full object-cover'
48+
/>
49+
</>
50+
) : hasPreview ? (
3451
<img
3552
src={file.previewUrl}
3653
alt={file.name}

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

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ import {
1111
useRef,
1212
useState,
1313
} from 'react'
14+
import { createLogger } from '@sim/logger'
1415
import { Paperclip } from 'lucide-react'
1516
import { useParams } from 'next/navigation'
1617
import { Button, Tooltip } from '@/components/emcn'
1718
import { useSession } from '@/lib/auth/auth-client'
19+
import { getMothershipAttachmentPreviewUrl } from '@/lib/copilot/chat/attachment-preview'
1820
import { SIM_RESOURCE_DRAG_TYPE, SIM_RESOURCES_DRAG_TYPE } from '@/lib/copilot/resource-types'
1921
import { cn } from '@/lib/core/utils/cn'
2022
import { CHAT_ACCEPT_ATTRIBUTE } from '@/lib/uploads/utils/validation'
@@ -58,6 +60,8 @@ import type { ChatContext } from '@/stores/panel'
5860

5961
export type { FileAttachmentForApi } from '@/app/workspace/[workspaceId]/home/types'
6062

63+
const logger = createLogger('UserInput')
64+
6165
function getCaretAnchor(
6266
textarea: HTMLTextAreaElement,
6367
caretPos: number
@@ -148,7 +152,8 @@ export const UserInput = forwardRef<UserInputHandle, UserInputProps>(function Us
148152
const [value, setValue] = useState(() => {
149153
if (defaultValue) return defaultValue
150154
if (!draftScopeKey) return ''
151-
return useMothershipDraftsStore.getState().drafts[draftScopeKey]?.text ?? ''
155+
const text = useMothershipDraftsStore.getState().drafts[draftScopeKey]?.text
156+
return typeof text === 'string' ? text : ''
152157
})
153158
const overlayRef = useRef<HTMLDivElement>(null)
154159
const plusMenuRef = useRef<PlusMenuHandle>(null)
@@ -189,29 +194,42 @@ export const UserInput = forwardRef<UserInputHandle, UserInputProps>(function Us
189194
useEffect(() => {
190195
if (hasRestoredDraftRef.current || !draftScopeKey) return
191196
hasRestoredDraftRef.current = true
192-
const draft = useMothershipDraftsStore.getState().drafts[draftScopeKey]
193-
if (!draft) return
194-
if (draft.contexts?.length) {
195-
contextManagement.setSelectedContexts(draft.contexts)
196-
}
197-
if (draft.fileAttachments?.length) {
198-
files.restoreAttachedFiles(
199-
draft.fileAttachments.map((a) => ({
197+
let restoredContexts: ChatContext[] | null = null
198+
let restoredFiles: AttachedFile[] | null = null
199+
let caretText: string | null = null
200+
try {
201+
const draft = useMothershipDraftsStore.getState().drafts[draftScopeKey]
202+
if (!draft) return
203+
if (draft.contexts?.length) {
204+
restoredContexts = draft.contexts
205+
}
206+
if (draft.fileAttachments?.length) {
207+
restoredFiles = draft.fileAttachments.map((a) => ({
200208
id: a.id,
201209
name: a.filename,
202210
size: a.size,
203211
type: a.media_type,
204212
path: a.path ?? '',
205213
key: a.key,
206214
uploading: false,
215+
previewUrl: getMothershipAttachmentPreviewUrl(a),
207216
}))
208-
)
217+
}
218+
if (typeof draft.text === 'string' && draft.text.length > 0) {
219+
caretText = draft.text
220+
}
221+
} catch (err) {
222+
logger.error('Failed to read draft, clearing', { err })
223+
useMothershipDraftsStore.getState().clearDraft(draftScopeKey)
224+
return
209225
}
210-
if (draft.text) {
226+
if (restoredContexts) contextManagement.setSelectedContexts(restoredContexts)
227+
if (restoredFiles) files.restoreAttachedFiles(restoredFiles)
228+
if (caretText !== null) {
211229
const textarea = textareaRef.current
212230
if (textarea) {
213231
textarea.focus()
214-
textarea.setSelectionRange(draft.text.length, draft.text.length)
232+
textarea.setSelectionRange(caretText.length, caretText.length)
215233
}
216234
}
217235
}, []) // eslint-disable-line react-hooks/exhaustive-deps -- intentional mount-only restore
@@ -369,6 +387,7 @@ export const UserInput = forwardRef<UserInputHandle, UserInputProps>(function Us
369387
path: a.path ?? '',
370388
key: a.key,
371389
uploading: false,
390+
previewUrl: getMothershipAttachmentPreviewUrl(a),
372391
}))
373392
files.restoreAttachedFiles(restored)
374393
contextManagement.setSelectedContexts(msg.contexts ?? [])

apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { sleep } from '@sim/utils/helpers'
55
import { generateId } from '@sim/utils/id'
66
import { useQueryClient } from '@tanstack/react-query'
77
import { usePathname, useRouter } from 'next/navigation'
8+
import { getMothershipAttachmentPreviewUrl } from '@/lib/copilot/chat/attachment-preview'
89
import { toDisplayMessage } from '@/lib/copilot/chat/display-message'
910
import { getLiveAssistantMessageId } from '@/lib/copilot/chat/effective-transcript'
1011
import type {
@@ -1420,6 +1421,18 @@ export function useChat(
14201421
const removeResource = useCallback((resourceType: MothershipResourceType, resourceId: string) => {
14211422
setResources((prev) => prev.filter((r) => !(r.type === resourceType && r.id === resourceId)))
14221423
setActiveResourceId((prev) => (prev === resourceId ? null : prev))
1424+
1425+
const persistChatId = chatIdRef.current ?? selectedChatIdRef.current
1426+
if (persistChatId) {
1427+
// boundary-raw-fetch: fire-and-forget side-effect; intentionally avoids requestJson's response parsing/throw semantics so a transient failure cannot interrupt the caller
1428+
fetch('/api/mothership/chat/resources', {
1429+
method: 'DELETE',
1430+
headers: { 'Content-Type': 'application/json' },
1431+
body: JSON.stringify({ chatId: persistChatId, resourceType, resourceId }),
1432+
}).catch((err) => {
1433+
logger.warn('Failed to persist resource removal', err)
1434+
})
1435+
}
14231436
}, [])
14241437

14251438
const reorderResources = useCallback((newOrder: MothershipResource[]) => {
@@ -3291,9 +3304,7 @@ export function useChat(
32913304
filename: f.filename,
32923305
media_type: f.media_type,
32933306
size: f.size,
3294-
previewUrl: f.media_type.startsWith('image/')
3295-
? `/api/files/serve/${encodeURIComponent(f.key)}?context=mothership`
3296-
: undefined,
3307+
previewUrl: getMothershipAttachmentPreviewUrl(f),
32973308
}))
32983309

32993310
const optimisticUserMessage: ChatMessage = {

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-file-attachments.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,10 @@ export function useFileAttachments(props: UseFileAttachmentsProps) {
126126
type: resolveFileType(file),
127127
path: '',
128128
uploading: true,
129-
previewUrl: file.type.startsWith('image/') ? URL.createObjectURL(file) : undefined,
129+
previewUrl:
130+
file.type.startsWith('image/') || file.type.startsWith('video/')
131+
? URL.createObjectURL(file)
132+
: undefined,
130133
}))
131134

132135
setAttachedFiles((prev) => [...prev, ...placeholders])

apps/sim/blocks/blocks/knowledge.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { PackageSearchIcon } from '@/components/icons'
22
import { DEFAULT_RERANKER_MODEL, SUPPORTED_RERANKER_MODELS } from '@/lib/knowledge/reranker-models'
33
import type { BlockConfig } from '@/blocks/types'
4+
import { getCohereRerankerApiKeyCondition } from '@/blocks/utils'
45

56
export const KnowledgeBlock: BlockConfig = {
67
type: 'knowledge',
@@ -105,6 +106,28 @@ export const KnowledgeBlock: BlockConfig = {
105106
and: { field: 'rerankerEnabled', value: true },
106107
},
107108
},
109+
{
110+
id: 'rerankerInputCount',
111+
title: 'Documents Sent to Reranker',
112+
type: 'short-input',
113+
placeholder: 'Auto (4× results, capped at 100)',
114+
mode: 'advanced',
115+
condition: {
116+
field: 'operation',
117+
value: 'search',
118+
and: { field: 'rerankerEnabled', value: true },
119+
},
120+
},
121+
{
122+
id: 'apiKey',
123+
title: 'Cohere API Key',
124+
type: 'short-input',
125+
placeholder: 'Enter your Cohere API key',
126+
password: true,
127+
connectionDroppable: false,
128+
required: true,
129+
condition: getCohereRerankerApiKeyCondition(),
130+
},
108131

109132
// --- List Documents ---
110133
{
@@ -419,6 +442,11 @@ export const KnowledgeBlock: BlockConfig = {
419442
tagFilters: { type: 'string', description: 'Tag filter criteria' },
420443
rerankerEnabled: { type: 'boolean', description: 'Apply Cohere reranking to search results' },
421444
rerankerModel: { type: 'string', description: 'Cohere rerank model identifier' },
445+
rerankerInputCount: {
446+
type: 'number',
447+
description: 'Number of vector results sent to the Cohere reranker (1–100)',
448+
},
449+
apiKey: { type: 'string', description: 'Cohere API key (self-hosted only)' },
422450
documentTags: { type: 'string', description: 'Document tags' },
423451
chunkSearch: { type: 'string', description: 'Search filter for chunks' },
424452
chunkEnabledFilter: { type: 'string', description: 'Filter chunks by enabled status' },

0 commit comments

Comments
 (0)