LlmSetting.tsx raw
1 import { Input } from '@/components/ui/input'
2 import { Label } from '@/components/ui/label'
3 import {
4 Select,
5 SelectContent,
6 SelectItem,
7 SelectTrigger,
8 SelectValue
9 } from '@/components/ui/select'
10 import { Switch } from '@/components/ui/switch'
11 import { Textarea } from '@/components/ui/textarea'
12 import { useNostr } from '@/providers/NostrProvider'
13 import { DEFAULT_MODEL, fetchModels, TAnthropicModel } from '@/services/llm.service'
14 import storage, { dispatchSettingsChanged } from '@/services/local-storage.service'
15 import { TLlmConfig } from '@/types'
16 import { LoaderCircle } from 'lucide-react'
17 import { useCallback, useEffect, useState } from 'react'
18 import { useTranslation } from 'react-i18next'
19
20 const DEFAULT_CONFIG: TLlmConfig = {
21 apiKey: '',
22 model: '',
23 systemPrompt: '',
24 autoRewrite: false
25 }
26
27 export default function LlmSetting() {
28 const { t } = useTranslation()
29 const { pubkey } = useNostr()
30 const [config, setConfig] = useState<TLlmConfig>(DEFAULT_CONFIG)
31 const [models, setModels] = useState<TAnthropicModel[]>([])
32 const [loadingModels, setLoadingModels] = useState(false)
33
34 useEffect(() => {
35 if (pubkey) {
36 setConfig(storage.getLlmConfig(pubkey) ?? DEFAULT_CONFIG)
37 }
38 }, [pubkey])
39
40 // Fetch models when API key changes
41 useEffect(() => {
42 if (!config.apiKey || config.apiKey.length < 10) {
43 setModels([])
44 return
45 }
46
47 let cancelled = false
48 const timer = setTimeout(async () => {
49 setLoadingModels(true)
50 try {
51 const result = await fetchModels(config.apiKey)
52 if (!cancelled) setModels(result)
53 } catch (err) {
54 console.error('[LLM] Failed to fetch models:', err)
55 if (!cancelled) setModels([])
56 } finally {
57 if (!cancelled) setLoadingModels(false)
58 }
59 }, 500) // debounce while typing the key
60
61 return () => {
62 cancelled = true
63 clearTimeout(timer)
64 }
65 }, [config.apiKey])
66
67 const save = useCallback(
68 (updated: TLlmConfig) => {
69 setConfig(updated)
70 if (pubkey) {
71 storage.setLlmConfig(pubkey, updated)
72 dispatchSettingsChanged()
73 }
74 },
75 [pubkey]
76 )
77
78 const selectedModel = config.model || DEFAULT_MODEL
79
80 return (
81 <div className="space-y-2">
82 <Label className="text-base font-semibold">{t('LLM Settings')}</Label>
83
84 <div className="space-y-1">
85 <Label htmlFor="llm-api-key">{t('Anthropic API key')}</Label>
86 <Input
87 id="llm-api-key"
88 type="password"
89 placeholder="sk-ant-..."
90 value={config.apiKey}
91 onChange={(e) => save({ ...config, apiKey: e.target.value })}
92 />
93 </div>
94
95 <div className="space-y-1">
96 <Label>{t('Model')}</Label>
97 {loadingModels ? (
98 <div className="flex items-center gap-2 h-10 px-3 text-sm text-muted-foreground">
99 <LoaderCircle className="size-4 animate-spin" />
100 {t('Loading models...')}
101 </div>
102 ) : models.length > 0 ? (
103 <Select value={selectedModel} onValueChange={(v) => save({ ...config, model: v })}>
104 <SelectTrigger>
105 <SelectValue />
106 </SelectTrigger>
107 <SelectContent>
108 {models.map((m) => (
109 <SelectItem key={m.id} value={m.id}>
110 {m.display_name}
111 </SelectItem>
112 ))}
113 </SelectContent>
114 </Select>
115 ) : (
116 <div className="flex items-center h-10 px-3 text-sm text-muted-foreground rounded-lg border border-input">
117 {config.apiKey
118 ? t('Enter a valid API key to load models')
119 : t('Enter API key first')}
120 </div>
121 )}
122 </div>
123
124 <div className="space-y-1">
125 <Label htmlFor="llm-system-prompt">{t('Rewrite instructions')}</Label>
126 <Textarea
127 id="llm-system-prompt"
128 rows={6}
129 placeholder={t(
130 'e.g. Rewrite the following note to be concise and clear. Preserve the original meaning and tone.'
131 )}
132 value={config.systemPrompt}
133 onChange={(e) => save({ ...config, systemPrompt: e.target.value })}
134 />
135 </div>
136
137 <div className="flex items-center justify-between min-h-9">
138 <Label htmlFor="llm-auto-rewrite" className="text-base font-normal">
139 {t('Auto-rewrite on publish')}
140 </Label>
141 <Switch
142 id="llm-auto-rewrite"
143 checked={config.autoRewrite}
144 onCheckedChange={(checked) => save({ ...config, autoRewrite: checked })}
145 />
146 </div>
147
148 <p className="text-xs text-muted-foreground">
149 {t('Only Anthropic Claude is supported.')}{' '}
150 <a
151 href="https://git.mleku.dev/mleku/smesh"
152 target="_blank"
153 rel="noopener noreferrer"
154 className="underline"
155 >
156 {t('Create an issue or PR for other APIs')}
157 </a>
158 </p>
159 </div>
160 )
161 }
162