Skip to content

Commit 17a084c

Browse files
authored
feat(copilot): updated copilot keys to have names, full parity with API keys page (#2260)
1 parent dafd2f5 commit 17a084c

File tree

4 files changed

+145
-18
lines changed

4 files changed

+145
-18
lines changed

apps/sim/app/api/copilot/api-keys/generate/route.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import { getSession } from '@/lib/auth'
44
import { SIM_AGENT_API_URL_DEFAULT } from '@/lib/copilot/constants'
55
import { env } from '@/lib/core/config/env'
66

7-
const GenerateApiKeySchema = z.object({}).optional()
7+
const GenerateApiKeySchema = z.object({
8+
name: z.string().min(1, 'Name is required').max(255, 'Name is too long'),
9+
})
810

911
export async function POST(req: NextRequest) {
1012
try {
@@ -31,13 +33,15 @@ export async function POST(req: NextRequest) {
3133
)
3234
}
3335

36+
const { name } = validationResult.data
37+
3438
const res = await fetch(`${SIM_AGENT_API_URL}/api/validate-key/generate`, {
3539
method: 'POST',
3640
headers: {
3741
'Content-Type': 'application/json',
3842
...(env.COPILOT_API_KEY ? { 'x-api-key': env.COPILOT_API_KEY } : {}),
3943
},
40-
body: JSON.stringify({ userId }),
44+
body: JSON.stringify({ userId, name }),
4145
})
4246

4347
if (!res.ok) {

apps/sim/app/api/copilot/api-keys/route.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ export async function GET(request: NextRequest) {
2727
return NextResponse.json({ error: 'Failed to get keys' }, { status: res.status || 500 })
2828
}
2929

30-
const apiKeys = (await res.json().catch(() => null)) as { id: string; apiKey: string }[] | null
30+
const apiKeys = (await res.json().catch(() => null)) as
31+
| { id: string; apiKey: string; name?: string; createdAt?: string; lastUsed?: string }[]
32+
| null
3133

3234
if (!Array.isArray(apiKeys)) {
3335
return NextResponse.json({ error: 'Invalid response from Sim Agent' }, { status: 500 })
@@ -37,7 +39,13 @@ export async function GET(request: NextRequest) {
3739
const value = typeof k.apiKey === 'string' ? k.apiKey : ''
3840
const last6 = value.slice(-6)
3941
const displayKey = `•••••${last6}`
40-
return { id: k.id, displayKey }
42+
return {
43+
id: k.id,
44+
displayKey,
45+
name: k.name || null,
46+
createdAt: k.createdAt || null,
47+
lastUsed: k.lastUsed || null,
48+
}
4149
})
4250

4351
return NextResponse.json({ keys }, { status: 200 })

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components-new/settings-modal/components/copilot/copilot.tsx

Lines changed: 117 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { useMemo, useState } from 'react'
44
import { Check, Copy, Plus, Search } from 'lucide-react'
5-
import { Button } from '@/components/emcn'
5+
import { Button, Input as EmcnInput } from '@/components/emcn'
66
import {
77
Modal,
88
ModalBody,
@@ -28,7 +28,11 @@ function CopilotKeySkeleton() {
2828
return (
2929
<div className='flex items-center justify-between gap-[12px]'>
3030
<div className='flex min-w-0 flex-col justify-center gap-[1px]'>
31-
<Skeleton className='h-[13px] w-[120px]' />
31+
<div className='flex items-center gap-[6px]'>
32+
<Skeleton className='h-5 w-[80px]' />
33+
<Skeleton className='h-5 w-[140px]' />
34+
</div>
35+
<Skeleton className='h-5 w-[100px]' />
3236
</div>
3337
<Skeleton className='h-[26px] w-[48px] rounded-[6px]' />
3438
</div>
@@ -44,28 +48,50 @@ export function Copilot() {
4448
const generateKey = useGenerateCopilotKey()
4549
const deleteKeyMutation = useDeleteCopilotKey()
4650

47-
const [showNewKeyDialog, setShowNewKeyDialog] = useState(false)
51+
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false)
52+
const [newKeyName, setNewKeyName] = useState('')
4853
const [newKey, setNewKey] = useState<string | null>(null)
54+
const [showNewKeyDialog, setShowNewKeyDialog] = useState(false)
4955
const [copySuccess, setCopySuccess] = useState(false)
5056
const [deleteKey, setDeleteKey] = useState<CopilotKey | null>(null)
5157
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
5258
const [searchTerm, setSearchTerm] = useState('')
59+
const [createError, setCreateError] = useState<string | null>(null)
5360

5461
const filteredKeys = useMemo(() => {
5562
if (!searchTerm.trim()) return keys
5663
const term = searchTerm.toLowerCase()
57-
return keys.filter((key) => key.displayKey?.toLowerCase().includes(term))
64+
return keys.filter(
65+
(key) =>
66+
key.name?.toLowerCase().includes(term) || key.displayKey?.toLowerCase().includes(term)
67+
)
5868
}, [keys, searchTerm])
5969

60-
const onGenerate = async () => {
70+
const handleCreateKey = async () => {
71+
if (!newKeyName.trim()) return
72+
73+
const trimmedName = newKeyName.trim()
74+
const isDuplicate = keys.some((k) => k.name === trimmedName)
75+
if (isDuplicate) {
76+
setCreateError(
77+
`A Copilot API key named "${trimmedName}" already exists. Please choose a different name.`
78+
)
79+
return
80+
}
81+
82+
setCreateError(null)
6183
try {
62-
const data = await generateKey.mutateAsync()
84+
const data = await generateKey.mutateAsync({ name: trimmedName })
6385
if (data?.key?.apiKey) {
6486
setNewKey(data.key.apiKey)
6587
setShowNewKeyDialog(true)
88+
setNewKeyName('')
89+
setCreateError(null)
90+
setIsCreateDialogOpen(false)
6691
}
6792
} catch (error) {
6893
logger.error('Failed to generate copilot API key', { error })
94+
setCreateError('Failed to create API key. Please check your connection and try again.')
6995
}
7096
}
7197

@@ -88,6 +114,15 @@ export function Copilot() {
88114
}
89115
}
90116

117+
const formatDate = (dateString?: string | null) => {
118+
if (!dateString) return 'Never'
119+
return new Date(dateString).toLocaleDateString('en-US', {
120+
year: 'numeric',
121+
month: 'short',
122+
day: 'numeric',
123+
})
124+
}
125+
91126
const hasKeys = keys.length > 0
92127
const showEmptyState = !hasKeys
93128
const showNoResults = searchTerm.trim() && filteredKeys.length === 0 && keys.length > 0
@@ -103,20 +138,23 @@ export function Copilot() {
103138
strokeWidth={2}
104139
/>
105140
<Input
106-
placeholder='Search keys...'
141+
placeholder='Search API keys...'
107142
value={searchTerm}
108143
onChange={(e) => setSearchTerm(e.target.value)}
109144
className='h-auto flex-1 border-0 bg-transparent p-0 font-base leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0'
110145
/>
111146
</div>
112147
<Button
113-
onClick={onGenerate}
148+
onClick={() => {
149+
setIsCreateDialogOpen(true)
150+
setCreateError(null)
151+
}}
114152
variant='primary'
115-
disabled={isLoading || generateKey.isPending}
153+
disabled={isLoading}
116154
className='!bg-[var(--brand-tertiary-2)] !text-[var(--text-inverse)] hover:!bg-[var(--brand-tertiary-2)]/90 disabled:cursor-not-allowed disabled:opacity-60'
117155
>
118156
<Plus className='mr-[6px] h-[13px] w-[13px]' />
119-
{generateKey.isPending ? 'Creating...' : 'Create'}
157+
Create
120158
</Button>
121159
</div>
122160

@@ -137,7 +175,15 @@ export function Copilot() {
137175
{filteredKeys.map((key) => (
138176
<div key={key.id} className='flex items-center justify-between gap-[12px]'>
139177
<div className='flex min-w-0 flex-col justify-center gap-[1px]'>
140-
<p className='truncate text-[13px] text-[var(--text-primary)]'>
178+
<div className='flex items-center gap-[6px]'>
179+
<span className='max-w-[280px] truncate font-medium text-[14px]'>
180+
{key.name || 'Unnamed Key'}
181+
</span>
182+
<span className='text-[13px] text-[var(--text-secondary)]'>
183+
(last used: {formatDate(key.lastUsed).toLowerCase()})
184+
</span>
185+
</div>
186+
<p className='truncate text-[13px] text-[var(--text-muted)]'>
141187
{key.displayKey}
142188
</p>
143189
</div>
@@ -155,14 +201,68 @@ export function Copilot() {
155201
))}
156202
{showNoResults && (
157203
<div className='py-[16px] text-center text-[13px] text-[var(--text-muted)]'>
158-
No keys found matching "{searchTerm}"
204+
No API keys found matching "{searchTerm}"
159205
</div>
160206
)}
161207
</div>
162208
)}
163209
</div>
164210
</div>
165211

212+
{/* Create API Key Dialog */}
213+
<Modal open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
214+
<ModalContent className='w-[400px]'>
215+
<ModalHeader>Create new API key</ModalHeader>
216+
<ModalBody>
217+
<p className='text-[12px] text-[var(--text-tertiary)]'>
218+
This key will allow access to Copilot features. Make sure to copy it after creation as
219+
you won't be able to see it again.
220+
</p>
221+
222+
<div className='mt-[16px] flex flex-col gap-[8px]'>
223+
<p className='font-medium text-[13px] text-[var(--text-secondary)]'>
224+
Enter a name for your API key to help you identify it later.
225+
</p>
226+
<EmcnInput
227+
value={newKeyName}
228+
onChange={(e) => {
229+
setNewKeyName(e.target.value)
230+
if (createError) setCreateError(null)
231+
}}
232+
placeholder='e.g., Development, Production'
233+
className='h-9'
234+
autoFocus
235+
/>
236+
{createError && (
237+
<p className='text-[11px] text-[var(--text-error)] leading-tight'>{createError}</p>
238+
)}
239+
</div>
240+
</ModalBody>
241+
242+
<ModalFooter>
243+
<Button
244+
variant='default'
245+
onClick={() => {
246+
setIsCreateDialogOpen(false)
247+
setNewKeyName('')
248+
setCreateError(null)
249+
}}
250+
>
251+
Cancel
252+
</Button>
253+
<Button
254+
type='button'
255+
variant='primary'
256+
onClick={handleCreateKey}
257+
disabled={!newKeyName.trim() || generateKey.isPending}
258+
className='!bg-[var(--brand-tertiary-2)] !text-[var(--text-inverse)] hover:!bg-[var(--brand-tertiary-2)]/90'
259+
>
260+
{generateKey.isPending ? 'Creating...' : 'Create'}
261+
</Button>
262+
</ModalFooter>
263+
</ModalContent>
264+
</Modal>
265+
166266
{/* New API Key Dialog */}
167267
<Modal
168268
open={showNewKeyDialog}
@@ -215,7 +315,11 @@ export function Copilot() {
215315
<ModalHeader>Delete API key</ModalHeader>
216316
<ModalBody>
217317
<p className='text-[12px] text-[var(--text-tertiary)]'>
218-
Deleting this API key will immediately revoke access for any integrations using it.{' '}
318+
Deleting{' '}
319+
<span className='font-medium text-[var(--text-primary)]'>
320+
{deleteKey?.name || 'Unnamed Key'}
321+
</span>{' '}
322+
will immediately revoke access for any integrations using it.{' '}
219323
<span className='text-[var(--text-error)]'>This action cannot be undone.</span>
220324
</p>
221325
</ModalBody>

apps/sim/hooks/queries/copilot-keys.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ export const copilotKeysKeys = {
1818
export interface CopilotKey {
1919
id: string
2020
displayKey: string // "•••••{last6}"
21+
name: string | null
22+
createdAt: string | null
23+
lastUsed: string | null
2124
}
2225

2326
/**
@@ -58,19 +61,27 @@ export function useCopilotKeys() {
5861
})
5962
}
6063

64+
/**
65+
* Generate key params
66+
*/
67+
interface GenerateKeyParams {
68+
name: string
69+
}
70+
6171
/**
6272
* Generate new Copilot API key mutation
6373
*/
6474
export function useGenerateCopilotKey() {
6575
const queryClient = useQueryClient()
6676

6777
return useMutation({
68-
mutationFn: async (): Promise<GenerateKeyResponse> => {
78+
mutationFn: async ({ name }: GenerateKeyParams): Promise<GenerateKeyResponse> => {
6979
const response = await fetch('/api/copilot/api-keys/generate', {
7080
method: 'POST',
7181
headers: {
7282
'Content-Type': 'application/json',
7383
},
84+
body: JSON.stringify({ name }),
7485
})
7586

7687
if (!response.ok) {

0 commit comments

Comments
 (0)