profileShare

rasmusjy / voicetask

Read-only snapshot

No repository description.

main default branch 105 files Expires Sep 13, 2026, 9:06 AM
ProviderSetup.tsx 4,893 bytes
1 import { useState } from 'react'
2 import type { ConfigStatus } from 'shared/types'
3 import * as api from '../api'
4 import { connectionDetail } from '../labels'
5
6 interface ProviderSetupProps {
7 status: ConfigStatus
8 onSaved: (status: ConfigStatus) => void
9 onClose?: () => void
10 }
11
12 export function ProviderSetup({ status, onSaved, onClose }: ProviderSetupProps) {
13 const [anthropicKey, setAnthropicKey] = useState('')
14 const [openaiKey, setOpenaiKey] = useState('')
15 const [saving, setSaving] = useState(false)
16 const [error, setError] = useState<string | null>(null)
17 const [note, setNote] = useState<string | null>(null)
18
19 const anthropicReady = anthropicKey.trim().length > 0 || status.anthropicKeySet
20 const openaiReady = openaiKey.trim().length > 0 || status.openaiKeySet
21
22 async function save(mode: 'demo' | 'real') {
23 setError(null)
24 setNote(null)
25 setSaving(true)
26 try {
27 const result =
28 mode === 'demo'
29 ? await api.saveConfig({ mode: 'demo' })
30 : await api.saveConfig({
31 mode: 'real',
32 ...(anthropicKey.trim() ? { anthropicKey: anthropicKey.trim() } : {}),
33 ...(openaiKey.trim() ? { openaiKey: openaiKey.trim() } : {}),
34 })
35 setAnthropicKey('')
36 setOpenaiKey('')
37 if (result.restartRequired) {
38 setNote('Keys saved. Stop this server and run "npm run dev" to use them.')
39 } else {
40 onClose?.()
41 }
42 onSaved(result.status)
43 } catch (err) {
44 setError(err instanceof Error ? err.message : String(err))
45 } finally {
46 setSaving(false)
47 }
48 }
49
50 return (
51 <section className="provider-setup" aria-labelledby="provider-setup-heading">
52 <p className="card-kicker">{status.mode === 'unconfigured' ? 'One-time setup' : 'Connection'}</p>
53 <h2 id="provider-setup-heading">
54 {status.mode === 'unconfigured' ? 'Connect VoiceTask' : 'How VoiceTask is connected'}
55 </h2>
56 <p className="card-intro">{connectionDetail(status)}</p>
57
58 <form
59 className="provider-form"
60 onSubmit={(event) => {
61 event.preventDefault()
62 void save('real')
63 }}
64 >
65 <div className="form-field">
66 <label htmlFor="anthropic-key">Anthropic key, for the interview</label>
67 <p id="anthropic-key-help">
68 Create one at console.anthropic.com. It starts with sk-ant-.
69 {status.anthropicKeySet ? ' A key is already saved; leave this empty to keep it.' : ''}
70 </p>
71 <input
72 id="anthropic-key"
73 type="password"
74 value={anthropicKey}
75 onChange={(event) => setAnthropicKey(event.target.value)}
76 placeholder={status.anthropicKeySet ? 'Saved. Paste a new key to replace it' : 'sk-ant-...'}
77 autoComplete="off"
78 spellCheck={false}
79 aria-describedby="anthropic-key-help"
80 disabled={saving}
81 />
82 </div>
83
84 <div className="form-field">
85 <label htmlFor="openai-key">OpenAI key, for turning speech into text</label>
86 <p id="openai-key-help">
87 Create one at platform.openai.com. It starts with sk-.
88 {status.openaiKeySet ? ' A key is already saved; leave this empty to keep it.' : ''}
89 </p>
90 <input
91 id="openai-key"
92 type="password"
93 value={openaiKey}
94 onChange={(event) => setOpenaiKey(event.target.value)}
95 placeholder={status.openaiKeySet ? 'Saved. Paste a new key to replace it' : 'sk-...'}
96 autoComplete="off"
97 spellCheck={false}
98 aria-describedby="openai-key-help"
99 disabled={saving}
100 />
101 </div>
102
103 <button type="submit" className="btn-primary start-button" disabled={saving || !anthropicReady || !openaiReady}>
104 {saving ? 'Saving...' : 'Save keys'}
105 </button>
106 <p className="form-footnote">
107 Keys are written to a local .env file on this computer, and never into your interview or build brief.
108 </p>
109 </form>
110
111 <div className="provider-alt">
112 <span>or</span>
113 </div>
114
115 <button type="button" className="choose-folder-button" disabled={saving} onClick={() => void save('demo')}>
116 Try the demo without keys
117 </button>
118 <p className="form-footnote">
119 The demo runs the whole flow with built-in sample answers, offline. You can switch to real keys later.
120 </p>
121
122 {note && (
123 <p className="provider-note" role="status">
124 {note}
125 </p>
126 )}
127 {error && (
128 <p className="error" role="alert">
129 {error}
130 </p>
131 )}
132
133 {onClose && (
134 <button type="button" className="provider-close" onClick={onClose} disabled={saving}>
135 Back
136 </button>
137 )}
138 </section>
139 )
140 }
141