@@ -8,42 +8,94 @@ import { Alert, AlertDescription } from '@/components/ui/alert'
88import { CheckCircle2 , X , KeyRound } from 'lucide-react'
99
1010const STORAGE_KEY = 'byok_anthropic_key'
11+ const MOONSHOT_STORAGE_KEY = 'byok_moonshot_key'
1112
1213interface ByokPanelProps {
1314 onClose : ( ) => void
1415}
1516
16- export function ByokPanel ( { onClose } : ByokPanelProps ) {
17+ function KeySection ( {
18+ label,
19+ storageKey,
20+ placeholder,
21+ } : {
22+ label : string
23+ storageKey : string
24+ placeholder ?: string
25+ } ) {
1726 const [ key , setKey ] = useState ( '' )
1827 const [ savedKey , setSavedKey ] = useState < string | null > ( null )
19- const [ usage , setUsage ] = useState < { sessionsUsed : number ; sessionLimit : number ; hoursUsed : number ; hoursLimit : number } | null > ( null )
2028
2129 useEffect ( ( ) => {
22- const stored = localStorage . getItem ( STORAGE_KEY )
30+ const stored = localStorage . getItem ( storageKey )
2331 if ( stored ) setSavedKey ( stored )
24- } , [ ] )
25-
26- useEffect ( ( ) => {
27- if ( ! savedKey ) return
28- fetch ( '/api/byok/usage' )
29- . then ( r => r . json ( ) )
30- . then ( data => setUsage ( data ) )
31- . catch ( ( ) => { } )
32- } , [ savedKey ] )
32+ } , [ storageKey ] )
3333
3434 function handleSave ( ) {
3535 if ( ! key . trim ( ) ) return
36- localStorage . setItem ( STORAGE_KEY , key . trim ( ) )
36+ localStorage . setItem ( storageKey , key . trim ( ) )
3737 setSavedKey ( key . trim ( ) )
3838 setKey ( '' )
3939 }
4040
4141 function handleRemove ( ) {
42- localStorage . removeItem ( STORAGE_KEY )
42+ localStorage . removeItem ( storageKey )
4343 setSavedKey ( null )
44- setUsage ( null )
44+ }
45+
46+ if ( savedKey ) {
47+ return (
48+ < div className = "space-y-2" >
49+ < Label > { label } </ Label >
50+ < Alert className = "border-green-400/50 bg-green-50 dark:bg-green-900/20 justify-center" >
51+ < AlertDescription className = "text-green-700 dark:text-green-300 items-center flex flex-row" >
52+ < CheckCircle2 className = "h-4 w-4 text-green-600 mr-2 shrink-0" />
53+ Key is active
54+ </ AlertDescription >
55+ </ Alert >
56+ < Button variant = "outline" size = "sm" onClick = { handleRemove } className = "w-full" >
57+ < X className = "h-4 w-4 mr-2" />
58+ Remove Key
59+ </ Button >
60+ </ div >
61+ )
4562 }
4663
64+ return (
65+ < div className = "space-y-2" >
66+ < Label htmlFor = { storageKey } > { label } </ Label >
67+ < Input
68+ id = { storageKey }
69+ type = "password"
70+ autoComplete = "off"
71+ placeholder = { placeholder }
72+ value = { key }
73+ onChange = { e => setKey ( e . target . value ) }
74+ onKeyDown = { e => e . key === 'Enter' && handleSave ( ) }
75+ />
76+ < Button onClick = { handleSave } disabled = { ! key . trim ( ) } size = "sm" className = "w-full" >
77+ Save Key
78+ </ Button >
79+ </ div >
80+ )
81+ }
82+
83+ export function ByokPanel ( { onClose } : ByokPanelProps ) {
84+ const [ usage , setUsage ] = useState < { sessionsUsed : number ; sessionLimit : number ; hoursUsed : number ; hoursLimit : number } | null > ( null )
85+ const [ hasAnthropicKey , setHasAnthropicKey ] = useState ( false )
86+
87+ useEffect ( ( ) => {
88+ setHasAnthropicKey ( ! ! localStorage . getItem ( STORAGE_KEY ) )
89+ } , [ ] )
90+
91+ useEffect ( ( ) => {
92+ if ( ! hasAnthropicKey ) return
93+ fetch ( '/api/byok/usage' )
94+ . then ( r => r . json ( ) )
95+ . then ( data => setUsage ( data ) )
96+ . catch ( ( ) => { } )
97+ } , [ hasAnthropicKey ] )
98+
4799 return (
48100 < div className = "flex flex-col h-full p-4 gap-4" >
49101 < div className = "flex items-center gap-2 pt-2" >
@@ -52,62 +104,39 @@ export function ByokPanel({ onClose }: ByokPanelProps) {
52104 </ div >
53105
54106 < p className = "text-sm text-muted-foreground" >
55- Add your Anthropic API key to use the service for free without a subscription.
56- </ p >
57- < p className = "text-sm text-muted-foreground" >
58- Your key is stored locally in your browser. We validate it to check it works and then whenever you send a message it is sent with the API key to the sandbox where the code agent will use it directly. Keys are never stored on database.
107+ Add your API keys to use the service without a subscription. Keys are stored locally in your browser and never saved on our servers.
59108 </ p >
60109
61- { savedKey ? (
62- < div className = "space-y-3" >
63- < Alert className = "border-green-400/50 bg-green-50 dark:bg-green-900/20 justify-center" >
64- < AlertDescription className = "text-green-700 dark:text-green-300 items-center flex flex-row" >
65- < CheckCircle2 className = "h-4 w-4 text-green-600 mr-2" />
66- API key is active. Messages are using your key
67- </ AlertDescription >
68- </ Alert >
69-
70- { usage && (
71- < div className = "rounded-lg border p-3 space-y-2" >
72- < p className = "text-sm font-medium" > Free Sandbox Usage</ p >
73- < div className = "flex justify-between text-sm text-muted-foreground" >
74- < span > Sessions used</ span >
75- < span > { usage . sessionsUsed } / { usage . sessionLimit } </ span >
76- </ div >
77- < div className = "w-full bg-muted rounded-full h-2" >
78- < div
79- className = "bg-primary h-2 rounded-full transition-all"
80- style = { { width : `${ Math . min ( ( usage . sessionsUsed / usage . sessionLimit ) * 100 , 100 ) } %` } }
81- />
82- </ div >
83- < p className = "text-xs text-muted-foreground" >
84- { usage . hoursUsed } h / { usage . hoursLimit } h used ({ usage . sessionLimit - usage . sessionsUsed } sessions remaining)
85- </ p >
86- </ div >
87- ) }
88-
89- < Button variant = "outline" onClick = { handleRemove } className = "w-full" >
90- < X className = "h-4 w-4 mr-2" />
91- Remove Key
92- </ Button >
93- </ div >
94- ) : (
95- < div className = "space-y-3" >
96- < div className = "space-y-1" >
97- < Label htmlFor = "byok-key" > Anthropic API Key</ Label >
98- < Input
99- id = "byok-key"
100- type = "password"
101- autoComplete = "off"
102- value = { key }
103- onChange = { e => setKey ( e . target . value ) }
104- onKeyDown = { e => e . key === 'Enter' && handleSave ( ) }
110+ < div className = "space-y-4" >
111+ < KeySection
112+ label = "Anthropic API Key"
113+ storageKey = { STORAGE_KEY }
114+ placeholder = "sk-ant-..."
115+ />
116+
117+ < KeySection
118+ label = "Moonshot API Key (Kimi K2)"
119+ storageKey = { MOONSHOT_STORAGE_KEY }
120+ placeholder = "sk-..."
121+ />
122+ </ div >
123+
124+ { usage && hasAnthropicKey && (
125+ < div className = "rounded-lg border p-3 space-y-2" >
126+ < p className = "text-sm font-medium" > Free Sandbox Usage</ p >
127+ < div className = "flex justify-between text-sm text-muted-foreground" >
128+ < span > Sessions used</ span >
129+ < span > { usage . sessionsUsed } / { usage . sessionLimit } </ span >
130+ </ div >
131+ < div className = "w-full bg-muted rounded-full h-2" >
132+ < div
133+ className = "bg-primary h-2 rounded-full transition-all"
134+ style = { { width : `${ Math . min ( ( usage . sessionsUsed / usage . sessionLimit ) * 100 , 100 ) } %` } }
105135 />
106136 </ div >
107-
108- < Button onClick = { handleSave } disabled = { ! key . trim ( ) } className = "w-full" >
109- Save Key
110- </ Button >
137+ < p className = "text-xs text-muted-foreground" >
138+ { usage . hoursUsed } h / { usage . hoursLimit } h used ({ usage . sessionLimit - usage . sessionsUsed } sessions remaining)
139+ </ p >
111140 </ div >
112141 ) }
113142 </ div >
0 commit comments