Mayo commited on
Commit
817bb02
·
unverified ·
1 Parent(s): 6e37e3c

refactor: remove openai-compatible feature

Browse files
ui/components/canvas/CanvasToolbar.tsx CHANGED
@@ -14,11 +14,8 @@ import {
14
  import { Separator } from '@/components/ui/separator'
15
  import { useAppStore } from '@/lib/store'
16
  import { useTextBlocks } from '@/hooks/useTextBlocks'
17
- import { OPENAI_COMPATIBLE_MODEL_ID } from '@/lib/openai'
18
  import { RenderEffect, RgbaColor, TextStyle } from '@/types'
19
  import { Button } from '@/components/ui/button'
20
- import { Input } from '@/components/ui/input'
21
- import { Textarea } from '@/components/ui/textarea'
22
  import { ColorPicker } from '@/components/ui/color-picker'
23
  import {
24
  Tooltip,
@@ -326,20 +323,11 @@ function LlmStatusPopover() {
326
  llmSetSelectedLanguage,
327
  llmToggleLoadUnload,
328
  llmCheckReady,
329
- llmOpenAIEndpoint,
330
- llmOpenAIApiKey,
331
- llmOpenAIPrompt,
332
- llmOpenAIModel,
333
- setLlmOpenAIEndpoint,
334
- setLlmOpenAIApiKey,
335
- setLlmOpenAIPrompt,
336
- setLlmOpenAIModel,
337
  } = useAppStore()
338
  const { t } = useTranslation()
339
 
340
  const activeLanguages =
341
  llmModels.find((model) => model.id === llmSelectedModel)?.languages ?? []
342
- const isOpenAICompatible = llmSelectedModel === OPENAI_COMPATIBLE_MODEL_ID
343
 
344
  useEffect(() => {
345
  llmList()
@@ -386,9 +374,7 @@ function LlmStatusPopover() {
386
  <SelectContent>
387
  {llmModels.map((model) => (
388
  <SelectItem key={model.id} value={model.id}>
389
- {model.id === OPENAI_COMPATIBLE_MODEL_ID
390
- ? t('llm.openaiCompatible')
391
- : model.id}
392
  </SelectItem>
393
  ))}
394
  </SelectContent>
@@ -413,55 +399,19 @@ function LlmStatusPopover() {
413
  </Select>
414
  )}
415
 
416
- {/* OpenAI compatible settings */}
417
- {isOpenAICompatible && (
418
- <div className='space-y-1.5 rounded border p-1.5'>
419
- <Input
420
- type='text'
421
- value={llmOpenAIEndpoint}
422
- placeholder={t('llm.openaiEndpointPlaceholder')}
423
- onChange={(event) => setLlmOpenAIEndpoint(event.target.value)}
424
- className='h-6 text-xs'
425
- />
426
- <Input
427
- type='password'
428
- value={llmOpenAIApiKey}
429
- placeholder={t('llm.openaiApiKeyPlaceholder')}
430
- autoComplete='off'
431
- onChange={(event) => setLlmOpenAIApiKey(event.target.value)}
432
- className='h-6 text-xs'
433
- />
434
- <Input
435
- value={llmOpenAIModel}
436
- placeholder={t('llm.openaiModelPlaceholder')}
437
- onChange={(event) => setLlmOpenAIModel(event.target.value)}
438
- className='h-6 text-xs'
439
- />
440
- <Textarea
441
- value={llmOpenAIPrompt}
442
- placeholder={t('llm.openaiPromptLabel')}
443
- rows={2}
444
- onChange={(event) => setLlmOpenAIPrompt(event.target.value)}
445
- className='min-h-0 px-2 py-1 text-xs'
446
- />
447
- </div>
448
- )}
449
-
450
  {/* Load/Unload button */}
451
- {!isOpenAICompatible && (
452
- <Button
453
- variant='outline'
454
- size='sm'
455
- onClick={llmToggleLoadUnload}
456
- disabled={!llmSelectedModel || llmLoading}
457
- className='w-full gap-1.5 text-xs'
458
- >
459
- {llmLoading && (
460
- <LoaderCircleIcon className='size-3.5 animate-spin' />
461
- )}
462
- {!llmReady ? t('llm.load') : t('llm.unload')}
463
- </Button>
464
- )}
465
  </div>
466
  </PopoverContent>
467
  </Popover>
 
14
  import { Separator } from '@/components/ui/separator'
15
  import { useAppStore } from '@/lib/store'
16
  import { useTextBlocks } from '@/hooks/useTextBlocks'
 
17
  import { RenderEffect, RgbaColor, TextStyle } from '@/types'
18
  import { Button } from '@/components/ui/button'
 
 
19
  import { ColorPicker } from '@/components/ui/color-picker'
20
  import {
21
  Tooltip,
 
323
  llmSetSelectedLanguage,
324
  llmToggleLoadUnload,
325
  llmCheckReady,
 
 
 
 
 
 
 
 
326
  } = useAppStore()
327
  const { t } = useTranslation()
328
 
329
  const activeLanguages =
330
  llmModels.find((model) => model.id === llmSelectedModel)?.languages ?? []
 
331
 
332
  useEffect(() => {
333
  llmList()
 
374
  <SelectContent>
375
  {llmModels.map((model) => (
376
  <SelectItem key={model.id} value={model.id}>
377
+ {model.id}
 
 
378
  </SelectItem>
379
  ))}
380
  </SelectContent>
 
399
  </Select>
400
  )}
401
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
402
  {/* Load/Unload button */}
403
+ <Button
404
+ variant='outline'
405
+ size='sm'
406
+ onClick={llmToggleLoadUnload}
407
+ disabled={!llmSelectedModel || llmLoading}
408
+ className='w-full gap-1.5 text-xs'
409
+ >
410
+ {llmLoading && (
411
+ <LoaderCircleIcon className='size-3.5 animate-spin' />
412
+ )}
413
+ {!llmReady ? t('llm.load') : t('llm.unload')}
414
+ </Button>
 
 
415
  </div>
416
  </PopoverContent>
417
  </Popover>
ui/components/panels/LlmControls.tsx CHANGED
@@ -3,11 +3,8 @@
3
  import { useEffect, useState } from 'react'
4
  import { useTranslation } from 'react-i18next'
5
  import { LoaderCircleIcon, SparklesIcon } from 'lucide-react'
6
- import { OPENAI_COMPATIBLE_MODEL_ID } from '@/lib/openai'
7
  import { useAppStore } from '@/lib/store'
8
  import { Button } from '@/components/ui/button'
9
- import { Input } from '@/components/ui/input'
10
- import { Textarea } from '@/components/ui/textarea'
11
  import {
12
  Tooltip,
13
  TooltipContent,
@@ -34,21 +31,12 @@ export function LlmControls() {
34
  llmToggleLoadUnload,
35
  llmGenerate,
36
  llmCheckReady,
37
- llmOpenAIEndpoint,
38
- llmOpenAIApiKey,
39
- llmOpenAIPrompt,
40
- llmOpenAIModel,
41
- setLlmOpenAIEndpoint,
42
- setLlmOpenAIApiKey,
43
- setLlmOpenAIPrompt,
44
- setLlmOpenAIModel,
45
  } = useAppStore()
46
  const [generating, setGenerating] = useState(false)
47
  const { t } = useTranslation()
48
 
49
  const activeLanguages =
50
  llmModels.find((model) => model.id === llmSelectedModel)?.languages ?? []
51
- const isOpenAICompatible = llmSelectedModel === OPENAI_COMPATIBLE_MODEL_ID
52
 
53
  useEffect(() => {
54
  llmList()
@@ -68,9 +56,7 @@ export function LlmControls() {
68
  <SelectContent>
69
  {llmModels.map((model) => (
70
  <SelectItem key={model.id} value={model.id}>
71
- {model.id === OPENAI_COMPATIBLE_MODEL_ID
72
- ? t('llm.openaiCompatible')
73
- : model.id}
74
  </SelectItem>
75
  ))}
76
  </SelectContent>
@@ -97,63 +83,27 @@ export function LlmControls() {
97
  </Select>
98
  )}
99
 
100
- {/* OpenAI compatible settings */}
101
- {isOpenAICompatible && (
102
- <div className='bg-card/50 space-y-1.5 rounded p-1.5'>
103
- <Input
104
- type='text'
105
- value={llmOpenAIEndpoint}
106
- placeholder={t('llm.openaiEndpointPlaceholder')}
107
- onChange={(event) => setLlmOpenAIEndpoint(event.target.value)}
108
- className='h-7 text-xs'
109
- />
110
- <Input
111
- type='password'
112
- value={llmOpenAIApiKey}
113
- placeholder={t('llm.openaiApiKeyPlaceholder')}
114
- autoComplete='off'
115
- onChange={(event) => setLlmOpenAIApiKey(event.target.value)}
116
- className='h-7 text-xs'
117
- />
118
- <Input
119
- value={llmOpenAIModel}
120
- placeholder={t('llm.openaiModelPlaceholder')}
121
- onChange={(event) => setLlmOpenAIModel(event.target.value)}
122
- className='h-7 text-xs'
123
- />
124
- <Textarea
125
- value={llmOpenAIPrompt}
126
- placeholder={t('llm.openaiPromptLabel')}
127
- rows={2}
128
- onChange={(event) => setLlmOpenAIPrompt(event.target.value)}
129
- className='min-h-0 px-2 py-1.5 text-xs'
130
- />
131
- </div>
132
- )}
133
-
134
  {/* Action buttons */}
135
  <div className='flex gap-1'>
136
- {!isOpenAICompatible && (
137
- <Tooltip>
138
- <TooltipTrigger asChild>
139
- <Button
140
- variant='outline'
141
- size='sm'
142
- onClick={llmToggleLoadUnload}
143
- disabled={!llmSelectedModel || llmLoading}
144
- className='flex-1 gap-1.5 text-xs'
145
- >
146
- {llmLoading && (
147
- <LoaderCircleIcon className='size-3.5 animate-spin' />
148
- )}
149
- {!llmReady ? t('llm.load') : t('llm.unload')}
150
- </Button>
151
- </TooltipTrigger>
152
- <TooltipContent side='bottom' sideOffset={4}>
153
- {!llmReady ? t('llm.loadTooltip') : t('llm.unloadTooltip')}
154
- </TooltipContent>
155
- </Tooltip>
156
- )}
157
  <Tooltip>
158
  <TooltipTrigger asChild>
159
  <Button
 
3
  import { useEffect, useState } from 'react'
4
  import { useTranslation } from 'react-i18next'
5
  import { LoaderCircleIcon, SparklesIcon } from 'lucide-react'
 
6
  import { useAppStore } from '@/lib/store'
7
  import { Button } from '@/components/ui/button'
 
 
8
  import {
9
  Tooltip,
10
  TooltipContent,
 
31
  llmToggleLoadUnload,
32
  llmGenerate,
33
  llmCheckReady,
 
 
 
 
 
 
 
 
34
  } = useAppStore()
35
  const [generating, setGenerating] = useState(false)
36
  const { t } = useTranslation()
37
 
38
  const activeLanguages =
39
  llmModels.find((model) => model.id === llmSelectedModel)?.languages ?? []
 
40
 
41
  useEffect(() => {
42
  llmList()
 
56
  <SelectContent>
57
  {llmModels.map((model) => (
58
  <SelectItem key={model.id} value={model.id}>
59
+ {model.id}
 
 
60
  </SelectItem>
61
  ))}
62
  </SelectContent>
 
83
  </Select>
84
  )}
85
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  {/* Action buttons */}
87
  <div className='flex gap-1'>
88
+ <Tooltip>
89
+ <TooltipTrigger asChild>
90
+ <Button
91
+ variant='outline'
92
+ size='sm'
93
+ onClick={llmToggleLoadUnload}
94
+ disabled={!llmSelectedModel || llmLoading}
95
+ className='flex-1 gap-1.5 text-xs'
96
+ >
97
+ {llmLoading && (
98
+ <LoaderCircleIcon className='size-3.5 animate-spin' />
99
+ )}
100
+ {!llmReady ? t('llm.load') : t('llm.unload')}
101
+ </Button>
102
+ </TooltipTrigger>
103
+ <TooltipContent side='bottom' sideOffset={4}>
104
+ {!llmReady ? t('llm.loadTooltip') : t('llm.unloadTooltip')}
105
+ </TooltipContent>
106
+ </Tooltip>
 
 
107
  <Tooltip>
108
  <TooltipTrigger asChild>
109
  <Button
ui/components/panels/TextBlocksPanel.tsx CHANGED
@@ -6,7 +6,6 @@ import { motion } from 'motion/react'
6
  import { TextBlock } from '@/types'
7
  import { Languages, LoaderCircleIcon } from 'lucide-react'
8
  import { useTextBlocks } from '@/hooks/useTextBlocks'
9
- import { isOpenAIConfigured, OPENAI_COMPATIBLE_MODEL_ID } from '@/lib/openai'
10
  import { useAppStore } from '@/lib/store'
11
  import {
12
  Accordion,
@@ -32,18 +31,8 @@ export function TextBlocksPanel() {
32
  replaceBlock,
33
  } = useTextBlocks()
34
  const { t } = useTranslation()
35
- const {
36
- llmGenerate,
37
- llmReady,
38
- llmSelectedModel,
39
- llmOpenAIEndpoint,
40
- llmOpenAIApiKey,
41
- } = useAppStore()
42
  const [generatingIndex, setGeneratingIndex] = useState<number | null>(null)
43
- const llmAvailable =
44
- llmSelectedModel === OPENAI_COMPATIBLE_MODEL_ID
45
- ? isOpenAIConfigured(llmOpenAIEndpoint, llmOpenAIApiKey)
46
- : llmReady
47
 
48
  if (!document) {
49
  return (
@@ -101,7 +90,7 @@ export function TextBlocksPanel() {
101
  onChange={(updates) => void replaceBlock(index, updates)}
102
  onGenerate={() => void handleGenerate(index)}
103
  generating={generatingIndex === index}
104
- llmReady={llmAvailable}
105
  />
106
  ))}
107
  </Accordion>
 
6
  import { TextBlock } from '@/types'
7
  import { Languages, LoaderCircleIcon } from 'lucide-react'
8
  import { useTextBlocks } from '@/hooks/useTextBlocks'
 
9
  import { useAppStore } from '@/lib/store'
10
  import {
11
  Accordion,
 
31
  replaceBlock,
32
  } = useTextBlocks()
33
  const { t } = useTranslation()
34
+ const { llmGenerate, llmReady } = useAppStore()
 
 
 
 
 
 
35
  const [generatingIndex, setGeneratingIndex] = useState<number | null>(null)
 
 
 
 
36
 
37
  if (!document) {
38
  return (
 
90
  onChange={(updates) => void replaceBlock(index, updates)}
91
  onGenerate={() => void handleGenerate(index)}
92
  generating={generatingIndex === index}
93
+ llmReady={llmReady}
94
  />
95
  ))}
96
  </Accordion>
ui/lib/openai.ts DELETED
@@ -1,97 +0,0 @@
1
- export type LlmModelInfo = {
2
- id: string
3
- languages: string[]
4
- }
5
-
6
- export const OPENAI_COMPATIBLE_MODEL_ID = 'openai-compatible'
7
- export const OPENAI_DEFAULT_MODEL = 'gpt-4o-mini'
8
-
9
- export const OPENAI_COMPATIBLE_MODEL: LlmModelInfo = {
10
- id: OPENAI_COMPATIBLE_MODEL_ID,
11
- languages: [],
12
- }
13
-
14
- export const isOpenAIModel = (modelId?: string) =>
15
- modelId === OPENAI_COMPATIBLE_MODEL_ID
16
-
17
- export const isOpenAIConfigured = (endpoint: string, apiKey: string) =>
18
- endpoint.trim().length > 0 || apiKey.trim().length > 0
19
-
20
- const normalizeOpenAIEndpoint = (endpoint: string) => {
21
- const baseEndpoint = endpoint.trim() || 'https://api.openai.com/v1/'
22
-
23
- const parsed = new URL(baseEndpoint)
24
- if (!parsed.pathname.endsWith('/chat/completions')) {
25
- const normalizedPath = parsed.pathname.replace(/\/$/, '')
26
- parsed.pathname = `${normalizedPath}/chat/completions`
27
- }
28
- return parsed.toString()
29
- }
30
-
31
- type OpenAIChoice = {
32
- message?: { content?: string }
33
- text?: string
34
- }
35
-
36
- const parseOpenAIResponse = (payload: { choices?: OpenAIChoice[] }) => {
37
- const choice = payload?.choices?.[0]
38
- const content = choice?.message?.content ?? choice?.text
39
- if (!content) {
40
- throw new Error('OpenAI compatible response missing content')
41
- }
42
- return content.trim()
43
- }
44
-
45
- export const callOpenAICompletion = async ({
46
- endpoint,
47
- apiKey,
48
- prompt,
49
- content,
50
- model,
51
- }: {
52
- endpoint: string
53
- apiKey: string
54
- prompt: string
55
- content: string
56
- model?: string
57
- }) => {
58
- const resolvedEndpoint = endpoint.trim() || 'https://api.openai.com/v1/'
59
- const url = normalizeOpenAIEndpoint(resolvedEndpoint)
60
- if (resolvedEndpoint === 'https://api.openai.com/v1/' && !apiKey.trim()) {
61
- throw new Error('OpenAI compatible API key is required')
62
- }
63
- const parsed = new URL(url)
64
- const selectedModel =
65
- model?.trim() || parsed.searchParams.get('model') || OPENAI_DEFAULT_MODEL
66
- parsed.searchParams.delete('model')
67
-
68
- const headers: Record<string, string> = {
69
- 'Content-Type': 'application/json',
70
- }
71
- if (apiKey && apiKey.trim()) {
72
- headers.Authorization = `Bearer ${apiKey}`
73
- }
74
-
75
- const response = await fetch(parsed.toString(), {
76
- method: 'POST',
77
- headers,
78
- body: JSON.stringify({
79
- model: selectedModel,
80
- messages: [
81
- { role: 'system', content: prompt },
82
- { role: 'user', content },
83
- ],
84
- temperature: 0.2,
85
- }),
86
- })
87
-
88
- if (!response.ok) {
89
- const message = await response.text()
90
- throw new Error(
91
- `OpenAI compatible request failed (${response.status}): ${message}`,
92
- )
93
- }
94
-
95
- const payload = (await response.json()) as { choices?: OpenAIChoice[] }
96
- return parseOpenAIResponse(payload)
97
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ui/lib/store.ts CHANGED
@@ -10,15 +10,10 @@ import {
10
  ToolMode,
11
  } from '@/types'
12
  import { createOperationSlice, type OperationSlice } from '@/lib/operations'
13
- import {
14
- callOpenAICompletion,
15
- isOpenAIConfigured,
16
- isOpenAIModel,
17
- OPENAI_COMPATIBLE_MODEL,
18
- OPENAI_DEFAULT_MODEL,
19
- type LlmModelInfo,
20
- } from '@/lib/openai'
21
- import { t } from 'i18next'
22
 
23
  type ProcessAction = 'detect' | 'ocr' | 'inpaint' | 'llmGenerate' | 'render'
24
 
@@ -166,10 +161,6 @@ type AppState = OperationSlice & {
166
  llmSelectedLanguage?: string
167
  llmReady: boolean
168
  llmLoading: boolean
169
- llmOpenAIEndpoint: string
170
- llmOpenAIApiKey: string
171
- llmOpenAIPrompt: string
172
- llmOpenAIModel: string
173
  // ui + actions
174
  setTotalPages: (count: number) => void
175
  fetchCurrentDocument: () => Promise<void>
@@ -239,66 +230,9 @@ type AppState = OperationSlice & {
239
  index?: number,
240
  text_block_index?: number,
241
  ) => Promise<void>
242
- setLlmOpenAIEndpoint: (endpoint: string) => void
243
- setLlmOpenAIApiKey: (apiKey: string) => void
244
- setLlmOpenAIPrompt: (prompt: string) => void
245
- setLlmOpenAIModel: (model: string) => void
246
  }
247
 
248
  export const useAppStore = create<AppState>((set, get) => {
249
- const isOpenAICompatible = () => isOpenAIModel(get().llmSelectedModel)
250
-
251
- const syncOpenAIReady = () => {
252
- if (!isOpenAICompatible()) return false
253
- const { llmOpenAIEndpoint, llmOpenAIApiKey } = get()
254
- const ready = isOpenAIConfigured(llmOpenAIEndpoint, llmOpenAIApiKey)
255
- set({ llmReady: ready, llmLoading: false })
256
- return true
257
- }
258
-
259
- const generateWithOpenAI = async (
260
- textBlockIndex?: number,
261
- ): Promise<TextBlock[] | void> => {
262
- const {
263
- currentDocument,
264
- llmOpenAIEndpoint,
265
- llmOpenAIApiKey,
266
- llmOpenAIPrompt,
267
- llmOpenAIModel,
268
- } = get()
269
- if (!currentDocument) return
270
- await textBlockSyncer.flush()
271
-
272
- const blocks = currentDocument.textBlocks ?? []
273
- const hasSingle = typeof textBlockIndex === 'number' && textBlockIndex >= 0
274
- const sourceText = hasSingle
275
- ? (blocks[textBlockIndex!]?.text ?? '')
276
- : blocks.map((block) => block.text ?? '').join('\n')
277
- if (!sourceText.trim()) return
278
-
279
- const completion = await callOpenAICompletion({
280
- endpoint: llmOpenAIEndpoint,
281
- apiKey: llmOpenAIApiKey,
282
- prompt: llmOpenAIPrompt.trim(),
283
- content: sourceText,
284
- model: llmOpenAIModel,
285
- })
286
-
287
- const translatedLines = hasSingle ? null : completion.split(/\r?\n/)
288
- const updatedBlocks = blocks.map((block, i) => {
289
- if (hasSingle) {
290
- return i === textBlockIndex
291
- ? { ...block, translation: completion }
292
- : block
293
- }
294
- const translated = translatedLines?.[i]
295
- if (translated === undefined) return block
296
- return { ...block, translation: translated }
297
- })
298
-
299
- return updatedBlocks
300
- }
301
-
302
  return {
303
  ...createOperationSlice(set),
304
  totalPages: 0,
@@ -322,10 +256,6 @@ export const useAppStore = create<AppState>((set, get) => {
322
  llmSelectedLanguage: undefined,
323
  llmReady: false,
324
  llmLoading: false,
325
- llmOpenAIEndpoint: '',
326
- llmOpenAIApiKey: '',
327
- llmOpenAIPrompt: t('llm.openaiPromptPlaceholder'),
328
- llmOpenAIModel: OPENAI_DEFAULT_MODEL,
329
  operation: undefined,
330
  setTotalPages: (count: number) => {
331
  set((state) => ({
@@ -564,10 +494,7 @@ export const useAppStore = create<AppState>((set, get) => {
564
  },
565
  llmList: async () => {
566
  try {
567
- const models = [
568
- ...(await invoke<LlmModelInfo[]>('llm_list')),
569
- OPENAI_COMPATIBLE_MODEL,
570
- ]
571
  set({ llmModels: models })
572
  const currentModel = get().llmSelectedModel
573
  const currentLanguage = get().llmSelectedLanguage
@@ -584,7 +511,6 @@ export const useAppStore = create<AppState>((set, get) => {
584
  llmSelectedModel: nextModel,
585
  llmSelectedLanguage: nextLanguage,
586
  })
587
- syncOpenAIReady()
588
  } catch (_) {}
589
  },
590
  llmSetSelectedModel: async (id: string) => {
@@ -600,7 +526,6 @@ export const useAppStore = create<AppState>((set, get) => {
600
  llmLoading: false,
601
  llmReady: false,
602
  })
603
- syncOpenAIReady()
604
  },
605
  llmSetSelectedLanguage: (language: string) => {
606
  const languages = findModelLanguages(
@@ -611,11 +536,6 @@ export const useAppStore = create<AppState>((set, get) => {
611
  set({ llmSelectedLanguage: language })
612
  },
613
  llmToggleLoadUnload: async () => {
614
- if (isOpenAICompatible()) {
615
- syncOpenAIReady()
616
- return
617
- }
618
-
619
  // unload
620
  if (get().llmReady) {
621
  await invoke('llm_offload')
@@ -658,7 +578,6 @@ export const useAppStore = create<AppState>((set, get) => {
658
  }
659
  },
660
  llmCheckReady: async () => {
661
- if (syncOpenAIReady()) return
662
  if (get().llmReady) return
663
  if (llmReadyCheckInFlight) {
664
  await llmReadyCheckInFlight
@@ -685,27 +604,11 @@ export const useAppStore = create<AppState>((set, get) => {
685
  : languages[0]
686
  : undefined
687
 
688
- if (isOpenAICompatible()) {
689
- if (!syncOpenAIReady()) {
690
- throw new Error(
691
- 'Provide an OpenAI compatible endpoint and API key to generate translations.',
692
- )
693
- }
694
- const updatedBlocks = await generateWithOpenAI(textBlockIndex)
695
- if (updatedBlocks) {
696
- // Save the updated blocks to the server
697
- await invoke('update_text_blocks', {
698
- index,
699
- textBlocks: updatedBlocks,
700
- })
701
- }
702
- } else {
703
- await invoke('llm_generate', {
704
- index,
705
- textBlockIndex,
706
- language,
707
- })
708
- }
709
 
710
  if (index === get().currentDocumentIndex) {
711
  await get().refreshCurrentDocument()
@@ -716,26 +619,8 @@ export const useAppStore = create<AppState>((set, get) => {
716
  void get().renderTextBlock(undefined, index, textBlockIndex)
717
  }
718
  },
719
- setLlmOpenAIEndpoint: (endpoint: string) => {
720
- set({ llmOpenAIEndpoint: endpoint })
721
- syncOpenAIReady()
722
- },
723
- setLlmOpenAIApiKey: (apiKey: string) => {
724
- set({ llmOpenAIApiKey: apiKey })
725
- syncOpenAIReady()
726
- },
727
- setLlmOpenAIPrompt: (prompt: string) => {
728
- set({ llmOpenAIPrompt: prompt })
729
- syncOpenAIReady()
730
- },
731
- setLlmOpenAIModel: (model: string) => {
732
- set({ llmOpenAIModel: model })
733
- syncOpenAIReady()
734
- },
735
-
736
  // batch proceeses
737
  processImage: async (_, index, options) => {
738
- const openAISelected = isOpenAICompatible()
739
  const normalizedOptions: ProcessImageOptionsObject =
740
  typeof options === 'function'
741
  ? { onProgress: options, skipOperationTracking: false }
@@ -746,16 +631,9 @@ export const useAppStore = create<AppState>((set, get) => {
746
  const operation = get().operation
747
  const isBatchRun = operation?.type === 'process-all'
748
 
749
- if (!get().llmReady && !openAISelected) {
750
  await get().llmList()
751
  await get().llmToggleLoadUnload()
752
- } else if (openAISelected) {
753
- syncOpenAIReady()
754
- if (!get().llmReady) {
755
- throw new Error(
756
- 'OpenAI compatible endpoint and API key are required before processing.',
757
- )
758
- }
759
  }
760
 
761
  index = index ?? get().currentDocumentIndex
@@ -844,18 +722,10 @@ export const useAppStore = create<AppState>((set, get) => {
844
  processAllImages: async () => {
845
  const total = get().totalPages
846
  if (!total) return
847
- const openAISelected = isOpenAICompatible()
848
 
849
- if (!get().llmReady && !openAISelected) {
850
  await get().llmList()
851
  await get().llmToggleLoadUnload()
852
- } else if (openAISelected) {
853
- syncOpenAIReady()
854
- if (!get().llmReady) {
855
- throw new Error(
856
- 'OpenAI compatible endpoint and API key are required before processing.',
857
- )
858
- }
859
  }
860
 
861
  get().startOperation({
 
10
  ToolMode,
11
  } from '@/types'
12
  import { createOperationSlice, type OperationSlice } from '@/lib/operations'
13
+ type LlmModelInfo = {
14
+ id: string
15
+ languages: string[]
16
+ }
 
 
 
 
 
17
 
18
  type ProcessAction = 'detect' | 'ocr' | 'inpaint' | 'llmGenerate' | 'render'
19
 
 
161
  llmSelectedLanguage?: string
162
  llmReady: boolean
163
  llmLoading: boolean
 
 
 
 
164
  // ui + actions
165
  setTotalPages: (count: number) => void
166
  fetchCurrentDocument: () => Promise<void>
 
230
  index?: number,
231
  text_block_index?: number,
232
  ) => Promise<void>
 
 
 
 
233
  }
234
 
235
  export const useAppStore = create<AppState>((set, get) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
  return {
237
  ...createOperationSlice(set),
238
  totalPages: 0,
 
256
  llmSelectedLanguage: undefined,
257
  llmReady: false,
258
  llmLoading: false,
 
 
 
 
259
  operation: undefined,
260
  setTotalPages: (count: number) => {
261
  set((state) => ({
 
494
  },
495
  llmList: async () => {
496
  try {
497
+ const models = await invoke<LlmModelInfo[]>('llm_list')
 
 
 
498
  set({ llmModels: models })
499
  const currentModel = get().llmSelectedModel
500
  const currentLanguage = get().llmSelectedLanguage
 
511
  llmSelectedModel: nextModel,
512
  llmSelectedLanguage: nextLanguage,
513
  })
 
514
  } catch (_) {}
515
  },
516
  llmSetSelectedModel: async (id: string) => {
 
526
  llmLoading: false,
527
  llmReady: false,
528
  })
 
529
  },
530
  llmSetSelectedLanguage: (language: string) => {
531
  const languages = findModelLanguages(
 
536
  set({ llmSelectedLanguage: language })
537
  },
538
  llmToggleLoadUnload: async () => {
 
 
 
 
 
539
  // unload
540
  if (get().llmReady) {
541
  await invoke('llm_offload')
 
578
  }
579
  },
580
  llmCheckReady: async () => {
 
581
  if (get().llmReady) return
582
  if (llmReadyCheckInFlight) {
583
  await llmReadyCheckInFlight
 
604
  : languages[0]
605
  : undefined
606
 
607
+ await invoke('llm_generate', {
608
+ index,
609
+ textBlockIndex,
610
+ language,
611
+ })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
612
 
613
  if (index === get().currentDocumentIndex) {
614
  await get().refreshCurrentDocument()
 
619
  void get().renderTextBlock(undefined, index, textBlockIndex)
620
  }
621
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
622
  // batch proceeses
623
  processImage: async (_, index, options) => {
 
624
  const normalizedOptions: ProcessImageOptionsObject =
625
  typeof options === 'function'
626
  ? { onProgress: options, skipOperationTracking: false }
 
631
  const operation = get().operation
632
  const isBatchRun = operation?.type === 'process-all'
633
 
634
+ if (!get().llmReady) {
635
  await get().llmList()
636
  await get().llmToggleLoadUnload()
 
 
 
 
 
 
 
637
  }
638
 
639
  index = index ?? get().currentDocumentIndex
 
722
  processAllImages: async () => {
723
  const total = get().totalPages
724
  if (!total) return
 
725
 
726
+ if (!get().llmReady) {
727
  await get().llmList()
728
  await get().llmToggleLoadUnload()
 
 
 
 
 
 
 
729
  }
730
 
731
  get().startOperation({
ui/public/locales/en-US/translation.json CHANGED
@@ -85,15 +85,6 @@
85
  "idle": "Idle",
86
  "selectPlaceholder": "Select model",
87
  "modelLabel": "Model",
88
- "openaiCompatible": "OpenAI Compatible",
89
- "openaiEndpointLabel": "OpenAI compatible endpoint",
90
- "openaiEndpointPlaceholder": "https://api.openai.com/v1",
91
- "openaiApiKeyLabel": "API key",
92
- "openaiApiKeyPlaceholder": "sk-...",
93
- "openaiModelLabel": "Model name",
94
- "openaiModelPlaceholder": "gpt-4o-mini",
95
- "openaiPromptLabel": "Prompt",
96
- "openaiPromptPlaceholder": "Translate the following text to Chinese, retain formatting, do not add extra explanations:\n",
97
  "languageLabel": "Target language",
98
  "languagePlaceholder": "Select language",
99
  "load": "Load",
 
85
  "idle": "Idle",
86
  "selectPlaceholder": "Select model",
87
  "modelLabel": "Model",
 
 
 
 
 
 
 
 
 
88
  "languageLabel": "Target language",
89
  "languagePlaceholder": "Select language",
90
  "load": "Load",
ui/public/locales/ja-JP/translation.json CHANGED
@@ -85,15 +85,6 @@
85
  "idle": "待機中",
86
  "selectPlaceholder": "モデルを選択",
87
  "modelLabel": "モデル",
88
- "openaiCompatible": "OpenAI 互換サービス",
89
- "openaiEndpointLabel": "OpenAI 端点",
90
- "openaiEndpointPlaceholder": "https://api.openai.com/v1",
91
- "openaiApiKeyLabel": "API キー",
92
- "openaiApiKeyPlaceholder": "sk-...",
93
- "openaiModelLabel": "モデル名",
94
- "openaiModelPlaceholder": "gpt-4o-mini",
95
- "openaiPromptLabel": "プロンプト",
96
- "openaiPromptPlaceholder": "以下のテキストを日本語に翻訳し、フォーマットを保持し、追加の説明を加えないでください:\n",
97
  "languageLabel": "翻訳先の言語",
98
  "languagePlaceholder": "言語を選択",
99
  "load": "読み込み",
 
85
  "idle": "待機中",
86
  "selectPlaceholder": "モデルを選択",
87
  "modelLabel": "モデル",
 
 
 
 
 
 
 
 
 
88
  "languageLabel": "翻訳先の言語",
89
  "languagePlaceholder": "言語を選択",
90
  "load": "読み込み",
ui/public/locales/zh-CN/translation.json CHANGED
@@ -85,15 +85,6 @@
85
  "idle": "空闲",
86
  "selectPlaceholder": "选择模型",
87
  "modelLabel": "模型",
88
- "openaiCompatible": "OpenAI 兼容服务",
89
- "openaiEndpointLabel": "OpenAI 端点",
90
- "openaiEndpointPlaceholder": "https://api.openai.com/v1",
91
- "openaiApiKeyLabel": "API 密钥",
92
- "openaiApiKeyPlaceholder": "sk-...",
93
- "openaiModelLabel": "模型名称",
94
- "openaiModelPlaceholder": "gpt-4o-mini",
95
- "openaiPromptLabel": "提示词",
96
- "openaiPromptPlaceholder": "将下列文本翻译为中文,保留格式,不要添加额外说明:\n",
97
  "languageLabel": "目标语言",
98
  "languagePlaceholder": "选择语言",
99
  "load": "加载",
 
85
  "idle": "空闲",
86
  "selectPlaceholder": "选择模型",
87
  "modelLabel": "模型",
 
 
 
 
 
 
 
 
 
88
  "languageLabel": "目标语言",
89
  "languagePlaceholder": "选择语言",
90
  "load": "加载",
ui/public/locales/zh-TW/translation.json CHANGED
@@ -85,15 +85,6 @@
85
  "idle": "閒置",
86
  "selectPlaceholder": "選擇模型",
87
  "modelLabel": "模型",
88
- "openaiCompatible": "OpenAI 兼容服務",
89
- "openaiEndpointLabel": "OpenAI 端點",
90
- "openaiEndpointPlaceholder": "https://api.openai.com/v1",
91
- "openaiApiKeyLabel": "API 密鑰",
92
- "openaiApiKeyPlaceholder": "sk-...",
93
- "openaiModelLabel": "模型名稱",
94
- "openaiModelPlaceholder": "gpt-4o-mini",
95
- "openaiPromptLabel": "提示詞",
96
- "openaiPromptPlaceholder": "將下列文本翻譯為繁體中文,保留格式,不要添加額外說明:\n",
97
  "languageLabel": "目標語言",
98
  "languagePlaceholder": "選擇語言",
99
  "load": "載入",
 
85
  "idle": "閒置",
86
  "selectPlaceholder": "選擇模型",
87
  "modelLabel": "模型",
 
 
 
 
 
 
 
 
 
88
  "languageLabel": "目標語言",
89
  "languagePlaceholder": "選擇語言",
90
  "load": "載入",