22
33import { useMemo, useState } from 'react'
44import { Check, Copy, Plus, Search } from 'lucide-react'
5- import { Button } from '@/components/emcn'
5+ import { Button, Input as EmcnInput } from '@/components/emcn'
66import {
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>
0 commit comments