Skip to content

Commit a81ec74

Browse files
committed
feat(copilot): refactor, adjusted welcome and user-input UI/UX
1 parent 4f409a6 commit a81ec74

File tree

66 files changed

+6840
-5125
lines changed

Some content is hidden

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

66 files changed

+6840
-5125
lines changed

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/diff-controls.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { memo, useCallback } from 'react'
22
import { Eye, EyeOff } from 'lucide-react'
33
import { Button } from '@/components/ui/button'
44
import { createLogger } from '@/lib/logs/console/logger'
5-
import { useCopilotStore } from '@/stores/copilot/store'
5+
import { useCopilotStore } from '@/stores/panel-new/copilot/store'
66
import { useWorkflowDiffStore } from '@/stores/workflow-diff'
77
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
88
import { mergeSubblockState } from '@/stores/workflows/utils'
Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,14 @@ import { useEffect } from 'react'
44
import { formatDistanceToNow } from 'date-fns'
55
import { AlertCircle, History, RotateCcw } from 'lucide-react'
66
import { Button, ScrollArea, Separator } from '@/components/ui'
7-
import { useCopilotStore } from '@/stores/copilot/store'
7+
import { useCopilotStore } from '@/stores/panel-new/copilot/store'
88

9+
/**
10+
* CheckpointPanel component displays and manages workflow checkpoints
11+
* Allows users to view checkpoint history and revert to previous workflow states
12+
*
13+
* @returns Checkpoint panel UI with list of available checkpoints
14+
*/
915
export function CheckpointPanel() {
1016
const {
1117
currentChat,
@@ -18,7 +24,9 @@ export function CheckpointPanel() {
1824
clearCheckpointError,
1925
} = useCopilotStore()
2026

21-
// Load checkpoints when chat changes
27+
/**
28+
* Load checkpoints when chat changes
29+
*/
2230
useEffect(() => {
2331
if (currentChat?.id) {
2432
loadCheckpoints(currentChat.id)
@@ -77,6 +85,11 @@ export function CheckpointPanel() {
7785
)
7886
}
7987

88+
/**
89+
* Handles reverting to a specific checkpoint
90+
* Prompts user for confirmation before reverting
91+
* @param checkpointId - ID of the checkpoint to revert to
92+
*/
8093
const handleRevert = async (checkpointId: string) => {
8194
if (
8295
window.confirm(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
'use client'
2+
3+
import { memo } from 'react'
4+
import { Plus } from 'lucide-react'
5+
import { cn } from '@/lib/utils'
6+
7+
/**
8+
* Threshold for high context usage warning (75%)
9+
*/
10+
const HIGH_USAGE_THRESHOLD = 75
11+
12+
/**
13+
* Color thresholds for context usage indicator
14+
*/
15+
const COLOR_THRESHOLDS = {
16+
CRITICAL: 90,
17+
WARNING: 75,
18+
MODERATE: 50,
19+
} as const
20+
21+
/**
22+
* Props for the ContextUsagePill component
23+
*/
24+
interface ContextUsagePillProps {
25+
/** Current context usage percentage (0-100) */
26+
percentage: number
27+
/** Additional CSS classes to apply */
28+
className?: string
29+
/** Callback to create a new chat when usage is high */
30+
onCreateNewChat?: () => void
31+
}
32+
33+
/**
34+
* Context usage indicator pill showing percentage of context window used
35+
* Displays color-coded percentage with option to start new chat when usage is high
36+
*
37+
* Color scheme:
38+
* - Red (≥90%): Critical usage
39+
* - Orange (≥75%): Warning usage
40+
* - Yellow (≥50%): Moderate usage
41+
* - Gray (<50%): Normal usage
42+
*
43+
* @param props - Component props
44+
* @returns Context usage pill with percentage and optional new chat button
45+
*/
46+
export const ContextUsagePill = memo(
47+
({ percentage, className, onCreateNewChat }: ContextUsagePillProps) => {
48+
if (percentage === null || percentage === undefined || Number.isNaN(percentage)) {
49+
return null
50+
}
51+
52+
const isHighUsage = percentage >= HIGH_USAGE_THRESHOLD
53+
54+
/**
55+
* Determines the color class based on usage percentage
56+
* @returns Tailwind classes for background and text color
57+
*/
58+
const getColorClass = () => {
59+
if (percentage >= COLOR_THRESHOLDS.CRITICAL) {
60+
return 'bg-red-500/10 text-red-600 dark:text-red-400'
61+
}
62+
if (percentage >= COLOR_THRESHOLDS.WARNING) {
63+
return 'bg-orange-500/10 text-orange-600 dark:text-orange-400'
64+
}
65+
if (percentage >= COLOR_THRESHOLDS.MODERATE) {
66+
return 'bg-yellow-500/10 text-yellow-600 dark:text-yellow-400'
67+
}
68+
return 'bg-gray-500/10 text-gray-600 dark:text-gray-400'
69+
}
70+
71+
/**
72+
* Formats percentage for display
73+
* Shows 1 decimal place for values <1%, 0 decimals otherwise
74+
*/
75+
const formattedPercentage = percentage < 1 ? percentage.toFixed(1) : percentage.toFixed(0)
76+
77+
return (
78+
<div
79+
className={cn(
80+
'inline-flex items-center gap-1 rounded-full px-2 py-0.5 font-medium text-[11px] tabular-nums transition-colors',
81+
getColorClass(),
82+
isHighUsage && 'border border-red-500/50',
83+
className
84+
)}
85+
title={`Context used in this chat: ${percentage.toFixed(2)}%`}
86+
>
87+
<span>{formattedPercentage}%</span>
88+
{isHighUsage && onCreateNewChat && (
89+
<button
90+
onClick={(e) => {
91+
e.stopPropagation()
92+
onCreateNewChat()
93+
}}
94+
className='inline-flex items-center justify-center transition-opacity hover:opacity-70'
95+
title='Recommended: Start a new chat for better quality'
96+
type='button'
97+
>
98+
<Plus className='h-3 w-3' />
99+
</button>
100+
)}
101+
</div>
102+
)
103+
}
104+
)
105+
106+
ContextUsagePill.displayName = 'ContextUsagePill'
Lines changed: 59 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,52 @@
11
import { memo, useState } from 'react'
22
import { FileText, Image } from 'lucide-react'
3-
import type { MessageFileAttachment } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/user-input'
3+
import type { MessageFileAttachment } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/copilot/components/user-input/user-input'
44

5+
/**
6+
* File size units for formatting
7+
*/
8+
const FILE_SIZE_UNITS = ['B', 'KB', 'MB', 'GB'] as const
9+
10+
/**
11+
* Kilobyte multiplier
12+
*/
13+
const KILOBYTE = 1024
14+
15+
/**
16+
* Props for the FileAttachmentDisplay component
17+
*/
518
interface FileAttachmentDisplayProps {
19+
/** Array of file attachments to display */
620
fileAttachments: MessageFileAttachment[]
721
}
822

23+
/**
24+
* FileAttachmentDisplay shows thumbnails or icons for attached files
25+
* Displays image previews or appropriate icons based on file type
26+
*
27+
* @param props - Component props
28+
* @returns Grid of file attachment thumbnails
29+
*/
930
export const FileAttachmentDisplay = memo(({ fileAttachments }: FileAttachmentDisplayProps) => {
1031
const [fileUrls, setFileUrls] = useState<Record<string, string>>({})
32+
const [failedImages, setFailedImages] = useState<Set<string>>(new Set())
1133

34+
/**
35+
* Formats file size in bytes to human-readable format
36+
* @param bytes - File size in bytes
37+
* @returns Formatted string (e.g., "2.5 MB")
38+
*/
1239
const formatFileSize = (bytes: number) => {
1340
if (bytes === 0) return '0 B'
14-
const k = 1024
15-
const sizes = ['B', 'KB', 'MB', 'GB']
16-
const i = Math.floor(Math.log(bytes) / Math.log(k))
17-
return `${Math.round((bytes / k ** i) * 10) / 10} ${sizes[i]}`
41+
const i = Math.floor(Math.log(bytes) / Math.log(KILOBYTE))
42+
return `${Math.round((bytes / KILOBYTE ** i) * 10) / 10} ${FILE_SIZE_UNITS[i]}`
1843
}
1944

45+
/**
46+
* Returns appropriate icon based on file media type
47+
* @param mediaType - MIME type of the file
48+
* @returns Icon component
49+
*/
2050
const getFileIcon = (mediaType: string) => {
2151
if (mediaType.startsWith('image/')) {
2252
return <Image className='h-5 w-5 text-muted-foreground' />
@@ -30,6 +60,11 @@ export const FileAttachmentDisplay = memo(({ fileAttachments }: FileAttachmentDi
3060
return <FileText className='h-5 w-5 text-muted-foreground' />
3161
}
3262

63+
/**
64+
* Gets or generates the file URL from cache
65+
* @param file - File attachment object
66+
* @returns URL to serve the file
67+
*/
3368
const getFileUrl = (file: MessageFileAttachment) => {
3469
const cacheKey = file.key
3570
if (fileUrls[cacheKey]) {
@@ -41,15 +76,32 @@ export const FileAttachmentDisplay = memo(({ fileAttachments }: FileAttachmentDi
4176
return url
4277
}
4378

79+
/**
80+
* Handles click on a file attachment - opens in new tab
81+
* @param file - File attachment object
82+
*/
4483
const handleFileClick = (file: MessageFileAttachment) => {
4584
const serveUrl = getFileUrl(file)
4685
window.open(serveUrl, '_blank')
4786
}
4887

88+
/**
89+
* Checks if a file is an image based on media type
90+
* @param mediaType - MIME type of the file
91+
* @returns True if file is an image
92+
*/
4993
const isImageFile = (mediaType: string) => {
5094
return mediaType.startsWith('image/')
5195
}
5296

97+
/**
98+
* Handles image loading errors
99+
* @param fileId - ID of the file that failed to load
100+
*/
101+
const handleImageError = (fileId: string) => {
102+
setFailedImages((prev) => new Set(prev).add(fileId))
103+
}
104+
53105
return (
54106
<>
55107
{fileAttachments.map((file) => (
@@ -59,29 +111,14 @@ export const FileAttachmentDisplay = memo(({ fileAttachments }: FileAttachmentDi
59111
onClick={() => handleFileClick(file)}
60112
title={`${file.filename} (${formatFileSize(file.size)})`}
61113
>
62-
{isImageFile(file.media_type) ? (
63-
// For images, show actual thumbnail
114+
{isImageFile(file.media_type) && !failedImages.has(file.id) ? (
64115
<img
65116
src={getFileUrl(file)}
66117
alt={file.filename}
67118
className='h-full w-full object-cover'
68-
onError={(e) => {
69-
// If image fails to load, replace with icon
70-
const target = e.target as HTMLImageElement
71-
target.style.display = 'none'
72-
const parent = target.parentElement
73-
if (parent) {
74-
const iconContainer = document.createElement('div')
75-
iconContainer.className =
76-
'flex items-center justify-center w-full h-full bg-background/50'
77-
iconContainer.innerHTML =
78-
'<svg class="h-5 w-5 text-muted-foreground" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/></svg>'
79-
parent.appendChild(iconContainer)
80-
}
81-
}}
119+
onError={() => handleImageError(file.id)}
82120
/>
83121
) : (
84-
// For other files, show icon centered
85122
<div className='flex h-full w-full items-center justify-center bg-background/50'>
86123
{getFileIcon(file.media_type)}
87124
</div>

0 commit comments

Comments
 (0)