@@ -8,6 +8,8 @@ import { transition } from '../../helpers/transition';
8
8
import { useSettings } from '../../helpers/AppSettings' ;
9
9
import { useEffect , useState } from 'react' ;
10
10
import { OpenRouterLoginButton } from './OpenRouterLoginButton' ;
11
+ import { TabPanel , Tabs } from '../Tabs' ;
12
+ import { effectFetch } from '../../helpers/effectFetch' ;
11
13
12
14
interface CreditUsage {
13
15
total : number ;
@@ -16,6 +18,17 @@ interface CreditUsage {
16
18
17
19
const CREDITS_ENDPOINT = 'https://openrouter.ai/api/v1/credits' ;
18
20
21
+ const PROVIDER_TABS = [
22
+ {
23
+ label : 'OpenRouter' ,
24
+ value : 'openrouter' ,
25
+ } ,
26
+ {
27
+ label : 'Ollama' ,
28
+ value : 'ollama' ,
29
+ } ,
30
+ ] ;
31
+
19
32
const AISettings : React . FC = ( ) => {
20
33
const {
21
34
enableAI,
@@ -26,6 +39,8 @@ const AISettings: React.FC = () => {
26
39
setMcpServers,
27
40
showTokenUsage,
28
41
setShowTokenUsage,
42
+ ollamaUrl,
43
+ setOllamaUrl,
29
44
} = useSettings ( ) ;
30
45
31
46
const [ creditUsage , setCreditUsage ] = useState < CreditUsage | undefined > ( ) ;
@@ -37,18 +52,16 @@ const AISettings: React.FC = () => {
37
52
return ;
38
53
}
39
54
40
- fetch ( CREDITS_ENDPOINT , {
55
+ return effectFetch ( CREDITS_ENDPOINT , {
41
56
headers : {
42
57
Authorization : `Bearer ${ openRouterApiKey } ` ,
43
58
} ,
44
- } )
45
- . then ( res => res . json ( ) )
46
- . then ( data => {
47
- setCreditUsage ( {
48
- total : data . data . total_credits ,
49
- used : data . data . total_usage ,
50
- } ) ;
59
+ } ) ( data => {
60
+ setCreditUsage ( {
61
+ total : data . data . total_credits ,
62
+ used : data . data . total_usage ,
51
63
} ) ;
64
+ } ) ;
52
65
} , [ openRouterApiKey ] ) ;
53
66
54
67
return (
@@ -59,45 +72,74 @@ const AISettings: React.FC = () => {
59
72
Features
60
73
</ CheckboxLabel >
61
74
< ConditionalSettings enabled = { enableAI } inert = { ! enableAI } >
62
- < label htmlFor = 'openrouter-api-key' >
63
- < Column gap = '0.5rem' >
64
- OpenRouter API Key
65
- < Row center >
66
- { ! openRouterApiKey && (
67
- < >
68
- < OpenRouterLoginButton />
69
- or
70
- </ >
71
- ) }
72
- < InputWrapper >
73
- < InputStyled
74
- id = 'openrouter-api-key'
75
- type = 'password'
76
- value = { openRouterApiKey || '' }
77
- onChange = { e =>
78
- setOpenRouterApiKey ( e . target . value || undefined )
79
- }
80
- placeholder = 'Enter your OpenRouter API key'
81
- />
82
- </ InputWrapper >
83
- </ Row >
84
- { creditUsage && (
85
- < CreditUsage >
86
- Credits used: { creditUsage . used } / Total: { creditUsage . total }
87
- </ CreditUsage >
88
- ) }
89
- { ! openRouterApiKey && (
90
- < CreditUsage >
91
- < p >
92
- OpenRouter provides a unified API that gives you access to
93
- hundreds of AI models from all major vendors, while
94
- automatically handling fallbacks and selecting the most
95
- cost-effective options.
96
- </ p >
97
- </ CreditUsage >
98
- ) }
99
- </ Column >
100
- </ label >
75
+ < Heading > AI Provider</ Heading >
76
+ < TabWrapper >
77
+ < Tabs tabs = { PROVIDER_TABS } label = 'AI Provider' rounded >
78
+ < StyledTabPanel value = 'openrouter' >
79
+ < Column gap = '0.5rem' >
80
+ < label htmlFor = 'openrouter-api-key' > OpenRouter API Key</ label >
81
+ < Row center >
82
+ { ! openRouterApiKey && (
83
+ < >
84
+ < OpenRouterLoginButton />
85
+ or
86
+ </ >
87
+ ) }
88
+ < InputWrapper >
89
+ < InputStyled
90
+ id = 'openrouter-api-key'
91
+ type = 'password'
92
+ value = { openRouterApiKey || '' }
93
+ onChange = { e =>
94
+ setOpenRouterApiKey ( e . target . value || undefined )
95
+ }
96
+ placeholder = 'Enter your OpenRouter API key'
97
+ />
98
+ </ InputWrapper >
99
+ </ Row >
100
+ { creditUsage && (
101
+ < Subtle as = 'p' >
102
+ Credits used: { creditUsage . used } / Total:{ ' ' }
103
+ { creditUsage . total }
104
+ </ Subtle >
105
+ ) }
106
+ { ! openRouterApiKey && (
107
+ < Subtle as = 'p' >
108
+ OpenRouter provides a unified API that gives you access to
109
+ hundreds of AI models from all major vendors, while
110
+ automatically handling fallbacks and selecting the most
111
+ cost-effective options.
112
+ </ Subtle >
113
+ ) }
114
+ </ Column >
115
+ </ StyledTabPanel >
116
+ < StyledTabPanel value = 'ollama' >
117
+ < Column gap = '0.5rem' >
118
+ < label htmlFor = 'ollama-url' > Ollama API Url</ label >
119
+ < InputWrapper >
120
+ < InputStyled
121
+ id = 'ollama-url'
122
+ value = { ollamaUrl || '' }
123
+ onChange = { e => setOllamaUrl ( e . target . value || undefined ) }
124
+ type = 'url'
125
+ placeholder = 'http://localhost:11434/api'
126
+ />
127
+ </ InputWrapper >
128
+ < Subtle as = 'p' >
129
+ Host your own AI models locally using{ ' ' }
130
+ < a
131
+ href = 'https://ollama.com/'
132
+ target = '_blank'
133
+ rel = 'noreferrer'
134
+ >
135
+ Ollama
136
+ </ a >
137
+ .
138
+ </ Subtle >
139
+ </ Column >
140
+ </ StyledTabPanel >
141
+ </ Tabs >
142
+ </ TabWrapper >
101
143
< CheckboxLabel >
102
144
< Checkbox checked = { showTokenUsage } onChange = { setShowTokenUsage } />
103
145
Show token usage in chats
@@ -122,9 +164,23 @@ const ConditionalSettings = styled(Column)<{ enabled: boolean }>`
122
164
${ transition ( 'opacity' ) }
123
165
` ;
124
166
125
- const CreditUsage = styled . div `
167
+ const Subtle = styled . div `
126
168
font-size: 0.8rem;
127
169
color: ${ p => p . theme . colors . textLight } ;
128
170
` ;
129
171
172
+ const TabWrapper = styled . div `
173
+ border: 1px solid ${ p => p . theme . colors . bg2 } ;
174
+ border-radius: ${ p => p . theme . radius } ;
175
+ ` ;
176
+
177
+ const StyledTabPanel = styled ( TabPanel ) `
178
+ padding: ${ p => p . theme . size ( ) } ;
179
+ padding-top: 0;
180
+
181
+ ${ InputWrapper } :has(input:user-invalid) {
182
+ border-color: ${ p => p . theme . colors . alert } ;
183
+ }
184
+ ` ;
185
+
130
186
export default AISettings ;
0 commit comments