@@ -8,6 +8,8 @@ import { transition } from '../../helpers/transition';
88import { useSettings } from '../../helpers/AppSettings' ;
99import { useEffect , useState } from 'react' ;
1010import { OpenRouterLoginButton } from './OpenRouterLoginButton' ;
11+ import { TabPanel , Tabs } from '../Tabs' ;
12+ import { effectFetch } from '../../helpers/effectFetch' ;
1113
1214interface CreditUsage {
1315 total : number ;
@@ -16,6 +18,17 @@ interface CreditUsage {
1618
1719const CREDITS_ENDPOINT = 'https://openrouter.ai/api/v1/credits' ;
1820
21+ const PROVIDER_TABS = [
22+ {
23+ label : 'OpenRouter' ,
24+ value : 'openrouter' ,
25+ } ,
26+ {
27+ label : 'Ollama' ,
28+ value : 'ollama' ,
29+ } ,
30+ ] ;
31+
1932const AISettings : React . FC = ( ) => {
2033 const {
2134 enableAI,
@@ -26,6 +39,8 @@ const AISettings: React.FC = () => {
2639 setMcpServers,
2740 showTokenUsage,
2841 setShowTokenUsage,
42+ ollamaUrl,
43+ setOllamaUrl,
2944 } = useSettings ( ) ;
3045
3146 const [ creditUsage , setCreditUsage ] = useState < CreditUsage | undefined > ( ) ;
@@ -37,18 +52,16 @@ const AISettings: React.FC = () => {
3752 return ;
3853 }
3954
40- fetch ( CREDITS_ENDPOINT , {
55+ return effectFetch ( CREDITS_ENDPOINT , {
4156 headers : {
4257 Authorization : `Bearer ${ openRouterApiKey } ` ,
4358 } ,
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 ,
5163 } ) ;
64+ } ) ;
5265 } , [ openRouterApiKey ] ) ;
5366
5467 return (
@@ -59,45 +72,74 @@ const AISettings: React.FC = () => {
5972 Features
6073 </ CheckboxLabel >
6174 < 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 >
101143 < CheckboxLabel >
102144 < Checkbox checked = { showTokenUsage } onChange = { setShowTokenUsage } />
103145 Show token usage in chats
@@ -122,9 +164,23 @@ const ConditionalSettings = styled(Column)<{ enabled: boolean }>`
122164 ${ transition ( 'opacity' ) }
123165` ;
124166
125- const CreditUsage = styled . div `
167+ const Subtle = styled . div `
126168 font-size: 0.8rem;
127169 color: ${ p => p . theme . colors . textLight } ;
128170` ;
129171
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+
130186export default AISettings ;
0 commit comments