1- import { useEffect , useState , useCallback } from "react" ;
1+ import { useEffect , useState , useCallback , useMemo } from "react" ;
22import { Button } from "./ui/button" ;
33import { Input } from "./ui/input" ;
44import { Label } from "./ui/label" ;
55import { SettingsManager } from "@core/utilities/Settings" ;
66import * as AppMetadataAPI from "@core/chorus/api/AppMetadataAPI" ;
77import { useQueryClient } from "@tanstack/react-query" ;
88
9+ type OnboardingProvider = "openrouter" | "google" | "openai" | "anthropic" ;
10+
11+ const ONBOARDING_API_KEY_FIELDS : Array < {
12+ provider : OnboardingProvider ;
13+ label : string ;
14+ placeholder : string ;
15+ } > = [
16+ {
17+ provider : "openrouter" ,
18+ label : "OpenRouter API key" ,
19+ placeholder : "sk-or-v1-..." ,
20+ } ,
21+ {
22+ provider : "google" ,
23+ label : "Google AI API key" ,
24+ placeholder : "AIza..." ,
25+ } ,
26+ {
27+ provider : "openai" ,
28+ label : "OpenAI API key" ,
29+ placeholder : "sk-..." ,
30+ } ,
31+ {
32+ provider : "anthropic" ,
33+ label : "Anthropic API key" ,
34+ placeholder : "sk-ant-..." ,
35+ } ,
36+ ] ;
37+
38+ const EMPTY_API_KEY_INPUTS : Record < OnboardingProvider , string > = {
39+ openrouter : "" ,
40+ google : "" ,
41+ openai : "" ,
42+ anthropic : "" ,
43+ } ;
44+
945export default function Onboarding ( { onComplete } : { onComplete : ( ) => void } ) {
1046 const onboardingStep = AppMetadataAPI . useOnboardingStep ( ) ;
1147 const setOnboardingStep = AppMetadataAPI . useSetOnboardingStep ( ) ;
12- const [ openRouterKey , setOpenRouterKey ] = useState ( "" ) ;
48+ const [ apiKeyInputs , setApiKeyInputs ] = useState ( EMPTY_API_KEY_INPUTS ) ;
1349 const [ isSaving , setIsSaving ] = useState ( false ) ;
1450 const queryClient = useQueryClient ( ) ;
1551
52+ const hasAnyApiKey = useMemo (
53+ ( ) =>
54+ Object . values ( apiKeyInputs ) . some (
55+ ( apiKey ) => apiKey . trim ( ) . length > 0 ,
56+ ) ,
57+ [ apiKeyInputs ] ,
58+ ) ;
59+
1660 const handleNextStep = useCallback ( ( ) => {
1761 setOnboardingStep . mutate ( { step : 1 } ) ;
1862 } , [ setOnboardingStep ] ) ;
1963
2064 const handleSaveAndComplete = useCallback ( async ( ) => {
21- if ( openRouterKey . trim ( ) ) {
22- setIsSaving ( true ) ;
65+ setIsSaving ( true ) ;
66+ try {
2367 const settingsManager = SettingsManager . getInstance ( ) ;
2468 const currentSettings = await settingsManager . get ( ) ;
25- const newApiKeys = {
26- ...currentSettings . apiKeys ,
27- openrouter : openRouterKey . trim ( ) ,
28- } ;
29- await settingsManager . set ( {
69+
70+ const trimmedApiKeys : Partial < Record < OnboardingProvider , string > > =
71+ { } ;
72+ for ( const field of ONBOARDING_API_KEY_FIELDS ) {
73+ const value = apiKeyInputs [ field . provider ] . trim ( ) ;
74+ if ( value . length > 0 ) {
75+ trimmedApiKeys [ field . provider ] = value ;
76+ }
77+ }
78+
79+ const updatedSettings = {
3080 ...currentSettings ,
31- apiKeys : newApiKeys ,
32- } ) ;
81+ apiKeys : {
82+ ...currentSettings . apiKeys ,
83+ ...trimmedApiKeys ,
84+ } ,
85+ } ;
86+
87+ await settingsManager . set ( updatedSettings ) ;
3388 await queryClient . invalidateQueries ( { queryKey : [ "apiKeys" ] } ) ;
89+ } finally {
3490 setIsSaving ( false ) ;
3591 }
92+
3693 onComplete ( ) ;
37- } , [ openRouterKey , queryClient , onComplete ] ) ;
94+ } , [ apiKeyInputs , queryClient , onComplete ] ) ;
95+
96+ const handleApiKeyChange = useCallback (
97+ ( provider : OnboardingProvider , value : string ) => {
98+ setApiKeyInputs ( ( previous ) => ( {
99+ ...previous ,
100+ [ provider ] : value ,
101+ } ) ) ;
102+ } ,
103+ [ ] ,
104+ ) ;
38105
39- // Allow pressing Enter to continue quickly
40106 useEffect ( ( ) => {
41107 const handleKeyDown = ( e : KeyboardEvent ) => {
42108 if ( e . key === "Enter" ) {
43- e . preventDefault ( ) ;
44109 if ( onboardingStep === 0 ) {
45110 handleNextStep ( ) ;
46- } else {
111+ } else if ( onboardingStep === 1 && ! isSaving ) {
47112 void handleSaveAndComplete ( ) ;
48113 }
49114 }
50115 } ;
51116
52117 document . addEventListener ( "keydown" , handleKeyDown ) ;
53- return ( ) => document . removeEventListener ( "keydown" , handleKeyDown ) ;
54- } , [ onboardingStep , handleNextStep , handleSaveAndComplete ] ) ;
118+ return ( ) => {
119+ document . removeEventListener ( "keydown" , handleKeyDown ) ;
120+ } ;
121+ } , [ onboardingStep , handleNextStep , handleSaveAndComplete , isSaving ] ) ;
55122
56123 if ( onboardingStep === 0 ) {
57124 return (
58- < div
59- data-tauri-drag-region
60- className = "fixed inset-0 z-50 flex flex-col items-center justify-center min-h-screen bg-background/95 backdrop-blur-sm px-4"
61- >
125+ < div className = "min-h-screen flex items-center justify-center px-4" >
62126 < div className = "text-center space-y-6 max-w-3xl w-full" >
63127 < div className = "space-y-2" >
64128 < h1 className = "text-2xl font-semibold tracking-tight" >
@@ -82,44 +146,38 @@ export default function Onboarding({ onComplete }: { onComplete: () => void }) {
82146 ) ;
83147 }
84148
85- // Step 2: OpenRouter API key
86149 return (
87- < div
88- data-tauri-drag-region
89- className = "fixed inset-0 z-50 flex flex-col items-center justify-center min-h-screen bg-background/95 backdrop-blur-sm px-4"
90- >
91- < div className = "text-center space-y-6 max-w-md w-full" >
92- < div className = "space-y-2" >
93- < h1 className = "text-2xl font-semibold tracking-tight" >
94- Add an API Key
95- </ h1 >
96- < p className = "text-muted-foreground" >
97- Chorus runs on API keys. We recommend{ " " }
98- < a
99- href = "https://openrouter.ai/keys"
100- target = "_blank"
101- rel = "noopener noreferrer"
102- className = "text-primary underline underline-offset-4"
103- >
104- OpenRouter
105- </ a > { " " }
106- to get started.
150+ < div className = "min-h-screen flex items-center justify-center px-4" >
151+ < div className = "w-full max-w-md space-y-6" >
152+ < div className = "space-y-2 text-center" >
153+ < h2 className = "text-xl font-semibold tracking-tight" >
154+ Optional API keys
155+ </ h2 >
156+ < p className = "text-sm text-muted-foreground" >
157+ Add keys now or skip and configure them later in
158+ Settings.
107159 </ p >
108160 </ div >
109161
110- < div className = "space-y-2 text-left" >
111- < Label htmlFor = "openrouter-key" > OpenRouter API Key</ Label >
112- < Input
113- id = "openrouter-key"
114- type = "password"
115- placeholder = "sk-or-..."
116- value = { openRouterKey }
117- onChange = { ( e ) => setOpenRouterKey ( e . target . value ) }
118- autoFocus
119- />
120- < p className = "text-xs text-muted-foreground" >
121- You can add more API keys later in Settings.
122- </ p >
162+ < div className = "space-y-4" >
163+ { ONBOARDING_API_KEY_FIELDS . map ( ( field ) => (
164+ < div key = { field . provider } className = "space-y-2" >
165+ < Label htmlFor = { `${ field . provider } -api-key` } >
166+ { field . label }
167+ </ Label >
168+ < Input
169+ id = { `${ field . provider } -api-key` }
170+ placeholder = { field . placeholder }
171+ value = { apiKeyInputs [ field . provider ] }
172+ onChange = { ( event ) =>
173+ handleApiKeyChange (
174+ field . provider ,
175+ event . target . value ,
176+ )
177+ }
178+ />
179+ </ div >
180+ ) ) }
123181 </ div >
124182
125183 < div className = "flex flex-col gap-2" >
@@ -128,9 +186,7 @@ export default function Onboarding({ onComplete }: { onComplete: () => void }) {
128186 onClick = { ( ) => void handleSaveAndComplete ( ) }
129187 disabled = { isSaving }
130188 >
131- { openRouterKey . trim ( )
132- ? "Save and continue"
133- : "Skip for now" } { " " }
189+ { hasAnyApiKey ? "Save and continue" : "Skip for now" } { " " }
134190 < span className = "text-sm" > ↵</ span >
135191 </ Button >
136192 </ div >
0 commit comments