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