Skip to content

Commit c8098d3

Browse files
committed
improvement: chat
1 parent 00eb812 commit c8098d3

File tree

17 files changed

+443
-112
lines changed

17 files changed

+443
-112
lines changed

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx

Lines changed: 41 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,14 @@ export function isPreviewable(file: { type: string; name: string }): boolean {
5454
return resolvePreviewType(file.type, file.name) !== null
5555
}
5656

57+
export type PreviewMode = 'editor' | 'split' | 'preview'
58+
5759
interface FileViewerProps {
5860
file: WorkspaceFileRecord
5961
workspaceId: string
6062
canEdit: boolean
6163
showPreview?: boolean
64+
previewMode?: PreviewMode
6265
autoFocus?: boolean
6366
onDirtyChange?: (isDirty: boolean) => void
6467
onSaveStatusChange?: (status: 'idle' | 'saving' | 'saved' | 'error') => void
@@ -70,6 +73,7 @@ export function FileViewer({
7073
workspaceId,
7174
canEdit,
7275
showPreview,
76+
previewMode,
7377
autoFocus,
7478
onDirtyChange,
7579
onSaveStatusChange,
@@ -83,7 +87,7 @@ export function FileViewer({
8387
file={file}
8488
workspaceId={workspaceId}
8589
canEdit={canEdit}
86-
showPreview={showPreview}
90+
previewMode={previewMode ?? (showPreview ? 'split' : 'editor')}
8791
autoFocus={autoFocus}
8892
onDirtyChange={onDirtyChange}
8993
onSaveStatusChange={onSaveStatusChange}
@@ -103,7 +107,7 @@ interface TextEditorProps {
103107
file: WorkspaceFileRecord
104108
workspaceId: string
105109
canEdit: boolean
106-
showPreview?: boolean
110+
previewMode: PreviewMode
107111
autoFocus?: boolean
108112
onDirtyChange?: (isDirty: boolean) => void
109113
onSaveStatusChange?: (status: 'idle' | 'saving' | 'saved' | 'error') => void
@@ -114,7 +118,7 @@ function TextEditor({
114118
file,
115119
workspaceId,
116120
canEdit,
117-
showPreview,
121+
previewMode,
118122
autoFocus,
119123
onDirtyChange,
120124
onSaveStatusChange,
@@ -256,36 +260,43 @@ function TextEditor({
256260
)
257261
}
258262

263+
const showEditor = previewMode !== 'preview'
264+
const showPreviewPane = previewMode !== 'editor'
265+
259266
return (
260267
<div ref={containerRef} className='relative flex flex-1 overflow-hidden'>
261-
<textarea
262-
ref={textareaRef}
263-
value={content}
264-
onChange={(e) => handleContentChange(e.target.value)}
265-
readOnly={!canEdit}
266-
spellCheck={false}
267-
style={showPreview ? { width: `${splitPct}%`, flexShrink: 0 } : undefined}
268-
className={cn(
269-
'h-full resize-none border-0 bg-transparent p-[24px] font-mono text-[14px] text-[var(--text-body)] outline-none placeholder:text-[var(--text-subtle)]',
270-
!showPreview && 'w-full',
271-
isResizing && 'pointer-events-none'
272-
)}
273-
/>
274-
{showPreview && (
268+
{showEditor && (
269+
<textarea
270+
ref={textareaRef}
271+
value={content}
272+
onChange={(e) => handleContentChange(e.target.value)}
273+
readOnly={!canEdit}
274+
spellCheck={false}
275+
style={showPreviewPane ? { width: `${splitPct}%`, flexShrink: 0 } : undefined}
276+
className={cn(
277+
'h-full resize-none border-0 bg-transparent p-[24px] font-mono text-[14px] text-[var(--text-body)] outline-none placeholder:text-[var(--text-subtle)]',
278+
!showPreviewPane && 'w-full',
279+
isResizing && 'pointer-events-none'
280+
)}
281+
/>
282+
)}
283+
{showPreviewPane && (
275284
<>
276-
<div className='relative shrink-0'>
277-
<div className='h-full w-px bg-[var(--border)]' />
278-
<div
279-
className='-left-[3px] absolute top-0 z-10 h-full w-[6px] cursor-col-resize'
280-
onMouseDown={() => setIsResizing(true)}
281-
role='separator'
282-
aria-orientation='vertical'
283-
aria-label='Resize split'
284-
/>
285-
{isResizing && (
286-
<div className='-translate-x-[0.5px] pointer-events-none absolute top-0 z-20 h-full w-[2px] bg-[var(--selection)]' />
287-
)}
288-
</div>
285+
{showEditor && (
286+
<div className='relative shrink-0'>
287+
<div className='h-full w-px bg-[var(--border)]' />
288+
<div
289+
className='-left-[3px] absolute top-0 z-10 h-full w-[6px] cursor-col-resize'
290+
onMouseDown={() => setIsResizing(true)}
291+
role='separator'
292+
aria-orientation='vertical'
293+
aria-label='Resize split'
294+
/>
295+
{isResizing && (
296+
<div className='-translate-x-[0.5px] pointer-events-none absolute top-0 z-20 h-full w-[2px] bg-[var(--selection)]' />
297+
)}
298+
</div>
299+
)}
289300
<div
290301
className={cn('min-w-0 flex-1 overflow-hidden', isResizing && 'pointer-events-none')}
291302
>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
export type { PreviewMode } from './file-viewer'
12
export { FileViewer, isPreviewable, isTextEditable } from './file-viewer'

apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx

Lines changed: 48 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
'use client'
22

3-
import { ArrowUpRight } from 'lucide-react'
3+
import { createElement } from 'react'
44
import { useParams } from 'next/navigation'
55
import { ArrowRight } from '@/components/emcn'
66
import { cn } from '@/lib/core/utils/cn'
7+
import { OAUTH_PROVIDERS } from '@/lib/oauth/oauth'
78

89
export interface OptionsItemData {
910
title: string
@@ -177,6 +178,8 @@ function OptionsDisplay({ data, onSelect }: OptionsDisplayProps) {
177178
const entries = Object.entries(data)
178179
if (entries.length === 0) return null
179180

181+
const disabled = !onSelect
182+
180183
return (
181184
<div className='animate-stream-fade-in'>
182185
<span className='font-[var(--sidebar-font-weight)] text-[14px] text-[var(--text-body)]'>
@@ -190,9 +193,11 @@ function OptionsDisplay({ data, onSelect }: OptionsDisplayProps) {
190193
<button
191194
key={key}
192195
type='button'
193-
onClick={() => onSelect?.(key)}
196+
disabled={disabled}
197+
onClick={() => onSelect?.(title)}
194198
className={cn(
195-
'flex items-center gap-[8px] border-[var(--divider)] px-[8px] py-[8px] text-left transition-colors hover:bg-[var(--surface-5)]',
199+
'flex items-center gap-[8px] border-[var(--divider)] px-[8px] py-[8px] text-left transition-colors',
200+
disabled ? 'cursor-not-allowed' : 'hover:bg-[var(--surface-5)]',
196201
i > 0 && 'border-t'
197202
)}
198203
>
@@ -213,33 +218,56 @@ function OptionsDisplay({ data, onSelect }: OptionsDisplayProps) {
213218
)
214219
}
215220

221+
function getCredentialIcon(provider: string): React.ComponentType<{ className?: string }> | null {
222+
const lower = provider.toLowerCase()
223+
224+
const directMatch = OAUTH_PROVIDERS[lower]
225+
if (directMatch) return directMatch.icon
226+
227+
for (const config of Object.values(OAUTH_PROVIDERS)) {
228+
if (config.name.toLowerCase() === lower) return config.icon
229+
for (const service of Object.values(config.services)) {
230+
if (service.name.toLowerCase() === lower) return service.icon
231+
if (service.providerId.toLowerCase() === lower) return service.icon
232+
}
233+
}
234+
235+
return null
236+
}
237+
238+
const LockIcon = (props: { className?: string }) => (
239+
<svg
240+
className={props.className}
241+
viewBox='0 0 16 16'
242+
fill='none'
243+
xmlns='http://www.w3.org/2000/svg'
244+
>
245+
<rect x='2' y='5' width='12' height='8' rx='1.5' stroke='currentColor' strokeWidth='1.3' />
246+
<path
247+
d='M5 5V3.5a3 3 0 1 1 6 0V5'
248+
stroke='currentColor'
249+
strokeWidth='1.3'
250+
strokeLinecap='round'
251+
/>
252+
<circle cx='8' cy='9.5' r='1.25' fill='currentColor' />
253+
</svg>
254+
)
255+
216256
function CredentialDisplay({ data }: { data: CredentialTagData }) {
257+
const Icon = getCredentialIcon(data.provider) ?? LockIcon
258+
217259
return (
218260
<a
219261
href={data.link}
220262
target='_blank'
221263
rel='noopener noreferrer'
222264
className='flex animate-stream-fade-in items-center gap-[8px] rounded-lg border border-[var(--divider)] px-3 py-2.5 transition-colors hover:bg-[var(--surface-5)]'
223265
>
224-
<svg
225-
className='h-[16px] w-[16px] shrink-0 text-[var(--text-icon)]'
226-
viewBox='0 0 16 16'
227-
fill='none'
228-
xmlns='http://www.w3.org/2000/svg'
229-
>
230-
<rect x='2' y='5' width='12' height='8' rx='1.5' stroke='currentColor' strokeWidth='1.3' />
231-
<path
232-
d='M5 5V3.5a3 3 0 1 1 6 0V5'
233-
stroke='currentColor'
234-
strokeWidth='1.3'
235-
strokeLinecap='round'
236-
/>
237-
<circle cx='8' cy='9.5' r='1.25' fill='currentColor' />
238-
</svg>
266+
{createElement(Icon, { className: 'h-[16px] w-[16px] shrink-0' })}
239267
<span className='flex-1 font-[var(--sidebar-font-weight)] text-[14px] text-[var(--text-body)]'>
240268
Connect {data.provider}
241269
</span>
242-
<ArrowUpRight className='h-[16px] w-[16px] shrink-0 text-[var(--text-icon)]' />
270+
<ArrowRight className='h-[16px] w-[16px] shrink-0 text-[var(--text-icon)]' />
243271
</a>
244272
)
245273
}
@@ -279,7 +307,7 @@ function UsageUpgradeDisplay({ data }: { data: UsageUpgradeTagData }) {
279307
className='mt-2 inline-flex items-center gap-1 font-[500] text-[13px] text-amber-700 underline decoration-dashed underline-offset-2 transition-colors hover:text-amber-900 dark:text-amber-300 dark:hover:text-amber-200'
280308
>
281309
{buttonLabel}
282-
<ArrowUpRight className='h-3 w-3' />
310+
<ArrowRight className='h-3 w-3' />
283311
</a>
284312
</div>
285313
)

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,12 @@ export function MessageContent({
202202
switch (segment.type) {
203203
case 'text':
204204
return (
205-
<ChatContent key={`text-${i}`} content={segment.content} isStreaming={isStreaming} />
205+
<ChatContent
206+
key={`text-${i}`}
207+
content={segment.content}
208+
isStreaming={isStreaming}
209+
onOptionSelect={onOptionSelect}
210+
/>
206211
)
207212
case 'agent_group': {
208213
const allToolsDone =

apps/sim/app/workspace/[workspaceId]/home/components/message-content/utils.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,23 @@ import {
33
Asterisk,
44
Blimp,
55
BubbleChatPreview,
6+
Bug,
7+
Calendar,
8+
ClipboardList,
69
Connections,
710
Database,
8-
Eye,
911
File,
1012
FolderCode,
11-
Key,
13+
Hammer,
14+
Integration,
1215
Library,
13-
ListFilter,
14-
Loader,
1516
Pencil,
1617
Play,
1718
Rocket,
1819
Search,
1920
Settings,
2021
TerminalWindow,
22+
Wrench,
2123
} from '@/components/emcn'
2224
import { Table as TableIcon } from '@/components/emcn/icons'
2325
import type { MothershipToolName, SubagentName } from '../../types'
@@ -42,18 +44,18 @@ const TOOL_ICONS: Record<MothershipToolName | SubagentName | 'mothership', IconC
4244
workspace_file: File,
4345
create_workflow: Connections,
4446
edit_workflow: Pencil,
45-
build: Connections,
47+
build: Hammer,
4648
run: Play,
4749
deploy: Rocket,
48-
auth: Key,
50+
auth: Integration,
4951
knowledge: Database,
5052
table: TableIcon,
51-
job: Loader,
53+
job: Calendar,
5254
agent: BubbleChatPreview,
53-
custom_tool: Settings,
55+
custom_tool: Wrench,
5456
research: Search,
55-
plan: ListFilter,
56-
debug: Eye,
57+
plan: ClipboardList,
58+
debug: Bug,
5759
edit: Pencil,
5860
}
5961

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

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { lazy, Suspense, useMemo } from 'react'
44
import { Skeleton } from '@/components/emcn'
55
import {
66
FileViewer,
7-
isPreviewable,
7+
type PreviewMode,
88
} from '@/app/workspace/[workspaceId]/files/components/file-viewer'
99
import type { MothershipResource } from '@/app/workspace/[workspaceId]/home/types'
1010
import { Table } from '@/app/workspace/[workspaceId]/tables/[tableId]/components'
@@ -23,20 +23,28 @@ const LOADING_SKELETON = (
2323
interface ResourceContentProps {
2424
workspaceId: string
2525
resource: MothershipResource
26+
previewMode?: PreviewMode
2627
}
2728

2829
/**
2930
* Renders the content for the currently active mothership resource.
3031
* Handles table, file, and workflow resource types with appropriate
3132
* embedded rendering for each.
3233
*/
33-
export function ResourceContent({ workspaceId, resource }: ResourceContentProps) {
34+
export function ResourceContent({ workspaceId, resource, previewMode }: ResourceContentProps) {
3435
switch (resource.type) {
3536
case 'table':
3637
return <Table key={resource.id} workspaceId={workspaceId} tableId={resource.id} embedded />
3738

3839
case 'file':
39-
return <EmbeddedFile key={resource.id} workspaceId={workspaceId} fileId={resource.id} />
40+
return (
41+
<EmbeddedFile
42+
key={resource.id}
43+
workspaceId={workspaceId}
44+
fileId={resource.id}
45+
previewMode={previewMode}
46+
/>
47+
)
4048

4149
case 'workflow':
4250
return (
@@ -53,9 +61,10 @@ export function ResourceContent({ workspaceId, resource }: ResourceContentProps)
5361
interface EmbeddedFileProps {
5462
workspaceId: string
5563
fileId: string
64+
previewMode?: PreviewMode
5665
}
5766

58-
function EmbeddedFile({ workspaceId, fileId }: EmbeddedFileProps) {
67+
function EmbeddedFile({ workspaceId, fileId, previewMode }: EmbeddedFileProps) {
5968
const { data: files = [], isLoading } = useWorkspaceFiles(workspaceId)
6069
const file = useMemo(() => files.find((f) => f.id === fileId), [files, fileId])
6170

@@ -76,7 +85,7 @@ function EmbeddedFile({ workspaceId, fileId }: EmbeddedFileProps) {
7685
file={file}
7786
workspaceId={workspaceId}
7887
canEdit={true}
79-
showPreview={isPreviewable(file)}
88+
previewMode={previewMode}
8089
/>
8190
</div>
8291
)

0 commit comments

Comments
 (0)