|
1 | 1 | <script setup lang="ts"> |
2 | | -import { Loader2, RefreshCw } from 'lucide-vue-next' |
3 | 2 | import { useSettingsError } from '~/composables/settings/useSettingsError' |
4 | | -import { useSettingsTaskOverview } from '~/composables/settings/useSettingsTaskOverview' |
5 | 3 | import type { CountrySourceSetting, CountrySourceSettings } from '~/server/utils/country-source-settings' |
6 | 4 |
|
7 | | -interface EnrichRunResult { |
8 | | - crawled: number |
9 | | - todo: number |
10 | | - archived: number |
11 | | - enriched: number |
12 | | - photoExtractions: number |
13 | | - photosTotal: number |
14 | | - durationMs: number |
15 | | - warning?: string | null |
16 | | - followUpTasksStarted?: boolean |
17 | | -} |
18 | | -
|
19 | 5 | const { t } = useI18n() |
20 | 6 | const countryLabel = useCountryLabel() |
21 | 7 | const { normalizeSettingsError } = useSettingsError() |
22 | | -const { |
23 | | - llmBatchJobs, |
24 | | - llmBatchJobsPending, |
25 | | - llmBatchJobsError, |
26 | | - formatBatchDate, |
27 | | - loadLlmBatchJobs, |
28 | | - startProgressPolling, |
29 | | - stopProgressPolling, |
30 | | -} = useSettingsTaskOverview() |
| 8 | +const emit = defineEmits<{ countries: [codes: string[]] }>() |
31 | 9 |
|
32 | 10 | const countrySources = ref<CountrySourceSetting[]>([]) |
33 | 11 | const countrySourcesPending = ref(false) |
34 | 12 | const countrySourcesError = ref<string | null>(null) |
35 | 13 | const countrySourcesSaved = ref(false) |
36 | 14 | const countrySourcesLoaded = ref(false) |
37 | | -const countryEnrichPending = ref<string | null>(null) |
38 | | -const countryEnrichError = ref<string | null>(null) |
39 | | -const countryEnrichResult = ref<EnrichRunResult & { country: string } | null>(null) |
40 | | -const forceCountryExtraction = ref(false) |
41 | 15 | const enabledCountrySourceCount = computed( |
42 | 16 | () => countrySources.value.filter((source) => source.enabled).length, |
43 | 17 | ) |
44 | 18 |
|
| 19 | +function publishEnabledCountries(): void { |
| 20 | + emit('countries', countrySources.value.filter((source) => source.enabled).map((source) => source.code)) |
| 21 | +} |
| 22 | +
|
45 | 23 | async function loadCountrySources(): Promise<void> { |
46 | 24 | try { |
47 | 25 | const res = await $fetch<CountrySourceSettings>('/api/settings/countries') |
48 | 26 | countrySources.value = res.countries |
49 | 27 | countrySourcesLoaded.value = true |
50 | 28 | countrySourcesError.value = null |
| 29 | + publishEnabledCountries() |
51 | 30 | } catch (err) { |
52 | 31 | countrySourcesLoaded.value = false |
53 | 32 | countrySourcesError.value = normalizeSettingsError(err, t('settings.sources.loadError')) |
@@ -76,248 +55,49 @@ async function saveCountrySources(): Promise<void> { |
76 | 55 | }) |
77 | 56 | countrySources.value = res.countries |
78 | 57 | countrySourcesSaved.value = true |
| 58 | + publishEnabledCountries() |
79 | 59 | } catch (err) { |
80 | 60 | countrySourcesError.value = normalizeSettingsError(err, t('settings.sources.saveError')) |
81 | 61 | } finally { |
82 | 62 | countrySourcesPending.value = false |
83 | 63 | } |
84 | 64 | } |
85 | 65 |
|
86 | | -async function enrichCountrySource(source: CountrySourceSetting): Promise<void> { |
87 | | - if (!source.enabled || countryEnrichPending.value) return |
88 | | -
|
89 | | - countryEnrichPending.value = source.code |
90 | | - countryEnrichError.value = null |
91 | | - countryEnrichResult.value = null |
92 | | - startProgressPolling() |
93 | | - try { |
94 | | - const res = await $fetch<{ result: EnrichRunResult }>( |
95 | | - `/api/settings/countries/${source.code}/enrich`, |
96 | | - { |
97 | | - method: 'POST', |
98 | | - body: { forceExtraction: forceCountryExtraction.value }, |
99 | | - }, |
100 | | - ) |
101 | | - countryEnrichResult.value = { ...res.result, country: source.code } |
102 | | - await loadLlmBatchJobs() |
103 | | - } catch (err) { |
104 | | - countryEnrichError.value = normalizeSettingsError(err, t('settings.sources.enrichError')) |
105 | | - } finally { |
106 | | - countryEnrichPending.value = null |
107 | | - } |
108 | | -} |
109 | | -
|
110 | | -onMounted(async () => { |
111 | | - await Promise.all([loadCountrySources(), loadLlmBatchJobs()]) |
112 | | -}) |
113 | | -onBeforeUnmount(stopProgressPolling) |
| 66 | +onMounted(loadCountrySources) |
114 | 67 | </script> |
115 | 68 |
|
116 | 69 | <template> |
117 | | - <Card> |
118 | | - <CardHeader> |
119 | | - <CardTitle>{{ $t('settings.sources.title') }}</CardTitle> |
120 | | - <CardAction> |
121 | | - <Button |
122 | | - type="button" |
123 | | - variant="ghost" |
124 | | - size="icon-sm" |
125 | | - :disabled="llmBatchJobsPending" |
126 | | - :title="$t('settings.sources.refreshOverview')" |
127 | | - @click="loadLlmBatchJobs" |
128 | | - > |
129 | | - <Loader2 v-if="llmBatchJobsPending" class="h-4 w-4 animate-spin" /> |
130 | | - <RefreshCw v-else class="h-4 w-4" /> |
131 | | - </Button> |
132 | | - </CardAction> |
133 | | - </CardHeader> |
134 | | - <CardContent class="space-y-4"> |
135 | | - <p class="text-sm text-muted-foreground"> |
136 | | - {{ $t('settings.sources.description') }} |
137 | | - </p> |
138 | | - |
139 | | - <p v-if="countrySourcesError" class="text-sm text-destructive">{{ countrySourcesError }}</p> |
140 | | - <p v-if="countryEnrichError" class="text-sm text-destructive">{{ countryEnrichError }}</p> |
141 | | - <p v-if="llmBatchJobsError" class="text-sm text-destructive">{{ llmBatchJobsError }}</p> |
142 | | - <p v-if="countrySourcesSaved" class="text-sm text-emerald-600 dark:text-emerald-500">{{ $t('settings.sources.saved') }}</p> |
143 | | - <p v-if="countryEnrichResult" class="text-sm text-emerald-600 dark:text-emerald-500"> |
144 | | - {{ $t('settings.sources.enrichDone', { |
145 | | - country: countryLabel(countryEnrichResult.country), |
146 | | - archived: countryEnrichResult.archived, |
147 | | - crawled: countryEnrichResult.crawled, |
148 | | - photos: countryEnrichResult.photosTotal, |
149 | | - duration: Math.round(countryEnrichResult.durationMs / 1000), |
150 | | - }) }} |
151 | | - </p> |
152 | | - <SettingsMessageDetails |
153 | | - v-if="countryEnrichResult?.warning" |
154 | | - :text="countryEnrichResult.warning" |
155 | | - variant="warning" |
156 | | - role="alert" |
157 | | - /> |
158 | | - <p v-if="countryEnrichResult?.followUpTasksStarted" class="text-sm text-muted-foreground"> |
159 | | - {{ $t('settings.sources.followUpTasksStarted') }} |
160 | | - </p> |
161 | | - |
162 | | - <div v-if="llmBatchJobs?.enrichStatus" class="text-sm space-y-1"> |
163 | | - <p v-if="llmBatchJobs.enrichStatus.status === 'running'"> |
164 | | - {{ $t('settings.sources.enrichStatusRunning', { at: formatBatchDate(llmBatchJobs.enrichStatus.startedAt) }) }} |
165 | | - </p> |
166 | | - <p v-else-if="llmBatchJobs.enrichStatus.finishedAt" class="text-muted-foreground"> |
167 | | - {{ $t('settings.sources.enrichStatusLastRun', { |
168 | | - at: formatBatchDate(llmBatchJobs.enrichStatus.finishedAt), |
169 | | - archived: llmBatchJobs.enrichStatus.lastResult?.archived ?? 0, |
170 | | - duration: Math.round((llmBatchJobs.enrichStatus.lastResult?.durationMs ?? 0) / 1000), |
171 | | - }) }} |
172 | | - </p> |
173 | | - <SettingsMessageDetails |
174 | | - v-if="llmBatchJobs.enrichStatus.lastError" |
175 | | - :text="$t('settings.sources.enrichStatusLastError', { message: llmBatchJobs.enrichStatus.lastError })" |
176 | | - variant="error" |
177 | | - /> |
178 | | - <SettingsMessageDetails |
179 | | - v-if="llmBatchJobs.enrichStatus.lastWarning" |
180 | | - :text="llmBatchJobs.enrichStatus.lastWarning" |
181 | | - variant="warning" |
182 | | - /> |
183 | | - </div> |
184 | | - |
185 | | - <div v-if="llmBatchJobs?.reprocessStatus" class="text-sm space-y-1"> |
186 | | - <p v-if="llmBatchJobs.reprocessStatus.status === 'running'"> |
187 | | - {{ $t('settings.sources.llmStatusRunning', { at: formatBatchDate(llmBatchJobs.reprocessStatus.startedAt) }) }} |
188 | | - </p> |
189 | | - <p v-else-if="llmBatchJobs.reprocessStatus.finishedAt" class="text-muted-foreground"> |
190 | | - {{ $t('settings.sources.llmStatusLastRun', { |
191 | | - at: formatBatchDate(llmBatchJobs.reprocessStatus.finishedAt), |
192 | | - processed: llmBatchJobs.reprocessStatus.lastResult?.processed ?? 0, |
193 | | - llmCalls: llmBatchJobs.reprocessStatus.lastResult?.llmCalls ?? 0, |
194 | | - duration: Math.round((llmBatchJobs.reprocessStatus.lastResult?.durationMs ?? 0) / 1000), |
195 | | - }) }} |
196 | | - </p> |
197 | | - <SettingsMessageDetails |
198 | | - v-if="llmBatchJobs.reprocessStatus.lastWarning" |
199 | | - :text="$t('settings.sources.llmStatusLastWarning', { message: llmBatchJobs.reprocessStatus.lastWarning })" |
200 | | - variant="warning" |
201 | | - /> |
202 | | - <SettingsMessageDetails |
203 | | - v-if="llmBatchJobs.reprocessStatus.lastError" |
204 | | - :text="$t('settings.sources.llmStatusLastError', { message: llmBatchJobs.reprocessStatus.lastError })" |
205 | | - variant="error" |
206 | | - /> |
207 | | - <p v-if="llmBatchJobs.reprocessStatus.lastResult?.llmErrors" class="text-destructive"> |
208 | | - {{ $t('settings.sources.llmStatusLlmErrors', { count: llmBatchJobs.reprocessStatus.lastResult.llmErrors }) }} |
209 | | - </p> |
210 | | - <SettingsMessageDetails |
211 | | - v-if="llmBatchJobs.reprocessStatus.lastLlmError" |
212 | | - :text="$t('settings.sources.llmStatusLastLlmError', { message: llmBatchJobs.reprocessStatus.lastLlmError })" |
213 | | - variant="error" |
214 | | - /> |
215 | | - </div> |
216 | | - |
217 | | - <div v-if="llmBatchJobs?.offloadImagesStatus?.finishedAt" class="text-sm space-y-1"> |
218 | | - <p class="text-muted-foreground"> |
219 | | - {{ $t('settings.sources.offloadStatusLastRun', { |
220 | | - at: formatBatchDate(llmBatchJobs.offloadImagesStatus.finishedAt), |
221 | | - removed: llmBatchJobs.offloadImagesStatus.lastResult?.removed ?? 0, |
222 | | - freedMb: Math.round((llmBatchJobs.offloadImagesStatus.lastResult?.freedBytes ?? 0) / 1024 / 1024), |
223 | | - }) }} |
224 | | - </p> |
225 | | - <SettingsMessageDetails |
226 | | - v-if="llmBatchJobs.offloadImagesStatus.lastWarning" |
227 | | - :text="llmBatchJobs.offloadImagesStatus.lastWarning" |
228 | | - variant="warning" |
229 | | - role="alert" |
230 | | - /> |
231 | | - <SettingsMessageDetails |
232 | | - v-if="llmBatchJobs.offloadImagesStatus.lastError" |
233 | | - :text="$t('settings.sources.offloadStatusLastError', { message: llmBatchJobs.offloadImagesStatus.lastError })" |
234 | | - variant="error" |
235 | | - role="alert" |
236 | | - /> |
| 70 | + <section class="space-y-3"> |
| 71 | + <div> |
| 72 | + <h3 class="text-sm font-semibold">{{ $t('settings.sources.title') }}</h3> |
| 73 | + <p class="mt-1 text-sm text-muted-foreground">{{ $t('settings.sources.description') }}</p> |
| 74 | + </div> |
| 75 | + <p v-if="countrySourcesError" class="text-sm text-destructive">{{ countrySourcesError }}</p> |
| 76 | + <p v-if="countrySourcesSaved" class="text-sm text-emerald-600 dark:text-emerald-500">{{ $t('settings.sources.saved') }}</p> |
| 77 | + <form class="space-y-3" @submit.prevent="saveCountrySources"> |
| 78 | + <div class="max-h-80 overflow-y-auto rounded-md border divide-y"> |
| 79 | + <SettingsCountryActionRow v-for="source in countrySources" :key="source.code"> |
| 80 | + <template #leading> |
| 81 | + <Checkbox |
| 82 | + class="mt-0.5" |
| 83 | + :model-value="source.enabled" |
| 84 | + :disabled="countrySourcesPending" |
| 85 | + @update:model-value="toggleCountrySource(source.code)" |
| 86 | + /> |
| 87 | + </template> |
| 88 | + <button type="button" class="min-w-0 w-full text-left disabled:cursor-not-allowed disabled:opacity-60" :disabled="countrySourcesPending" @click="toggleCountrySource(source.code)"> |
| 89 | + <span class="block text-sm font-medium"> |
| 90 | + {{ countryLabel(source.code, source.name) }} |
| 91 | + <span class="ml-1 font-mono text-xs uppercase text-muted-foreground">{{ source.code }}</span> |
| 92 | + </span> |
| 93 | + <span class="block text-xs text-muted-foreground">{{ source.platforms.map((platform) => platform.name).join(', ') }}</span> |
| 94 | + </button> |
| 95 | + </SettingsCountryActionRow> |
237 | 96 | </div> |
238 | | - |
239 | | - <div v-if="llmBatchJobs?.externalEnrichmentStatus" class="text-sm space-y-1"> |
240 | | - <p v-if="llmBatchJobs.externalEnrichmentStatus.status === 'running'"> |
241 | | - {{ $t('settings.sources.externalStatusRunning', { at: formatBatchDate(llmBatchJobs.externalEnrichmentStatus.startedAt) }) }} |
242 | | - </p> |
243 | | - <p v-else-if="llmBatchJobs.externalEnrichmentStatus.finishedAt" class="text-muted-foreground"> |
244 | | - {{ $t('settings.sources.externalStatusLastRun', { |
245 | | - at: formatBatchDate(llmBatchJobs.externalEnrichmentStatus.finishedAt), |
246 | | - processed: llmBatchJobs.externalEnrichmentStatus.lastResult?.processed ?? 0, |
247 | | - written: llmBatchJobs.externalEnrichmentStatus.lastResult?.written ?? 0, |
248 | | - duration: Math.round((llmBatchJobs.externalEnrichmentStatus.lastResult?.durationMs ?? 0) / 1000), |
249 | | - }) }} |
250 | | - </p> |
251 | | - <SettingsMessageDetails |
252 | | - v-if="llmBatchJobs.externalEnrichmentStatus.lastWarning" |
253 | | - :text="llmBatchJobs.externalEnrichmentStatus.lastWarning" |
254 | | - variant="warning" |
255 | | - role="alert" |
256 | | - /> |
257 | | - <SettingsMessageDetails |
258 | | - v-if="llmBatchJobs.externalEnrichmentStatus.lastError" |
259 | | - :text="$t('settings.sources.externalStatusLastError', { message: llmBatchJobs.externalEnrichmentStatus.lastError })" |
260 | | - variant="error" |
261 | | - role="alert" |
262 | | - /> |
263 | | - </div> |
264 | | - |
265 | | - <form class="space-y-3" @submit.prevent="saveCountrySources"> |
266 | | - <label class="flex items-start gap-2 rounded-md border bg-muted/20 p-3 text-sm"> |
267 | | - <Checkbox v-model="forceCountryExtraction" class="mt-0.5" :disabled="countryEnrichPending !== null" /> |
268 | | - <span class="space-y-1"> |
269 | | - <span class="block font-medium">{{ $t('settings.sources.forceExtraction') }}</span> |
270 | | - <span class="block text-xs text-muted-foreground">{{ $t('settings.sources.forceExtractionHelp') }}</span> |
271 | | - </span> |
272 | | - </label> |
273 | | - |
274 | | - <div class="max-h-80 overflow-y-auto rounded-md border divide-y"> |
275 | | - <SettingsCountryActionRow v-for="source in countrySources" :key="source.code"> |
276 | | - <template #leading> |
277 | | - <Checkbox |
278 | | - class="mt-0.5" |
279 | | - :model-value="source.enabled" |
280 | | - :disabled="countrySourcesPending" |
281 | | - @update:model-value="toggleCountrySource(source.code)" |
282 | | - /> |
283 | | - </template> |
284 | | - <button type="button" class="min-w-0 w-full text-left disabled:cursor-not-allowed disabled:opacity-60" :disabled="countrySourcesPending" @click="toggleCountrySource(source.code)"> |
285 | | - <span class="block text-sm font-medium"> |
286 | | - {{ countryLabel(source.code, source.name) }} |
287 | | - <span class="ml-1 font-mono text-xs uppercase text-muted-foreground">{{ source.code }}</span> |
288 | | - </span> |
289 | | - <span class="block text-xs text-muted-foreground"> |
290 | | - {{ source.platforms.map((platform) => platform.name).join(', ') }} |
291 | | - </span> |
292 | | - </button> |
293 | | - <template #action> |
294 | | - <Button |
295 | | - type="button" |
296 | | - variant="outline" |
297 | | - size="sm" |
298 | | - :title="$t('settings.sources.enrichTitle')" |
299 | | - :disabled="!source.enabled || countrySourcesPending || countryEnrichPending !== null" |
300 | | - @click="enrichCountrySource(source)" |
301 | | - > |
302 | | - <Loader2 v-if="countryEnrichPending === source.code" class="h-4 w-4 animate-spin" /> |
303 | | - <RefreshCw v-else class="h-4 w-4" /> |
304 | | - {{ countryEnrichPending === source.code ? $t('settings.sources.enriching') : $t('settings.sources.enrich') }} |
305 | | - </Button> |
306 | | - </template> |
307 | | - </SettingsCountryActionRow> |
308 | | - </div> |
309 | | - |
310 | | - <p class="text-xs text-muted-foreground"> |
311 | | - {{ $t('settings.sources.enabledCount', { |
312 | | - enabled: enabledCountrySourceCount, |
313 | | - total: countrySources.length, |
314 | | - }) }} |
315 | | - </p> |
316 | | - |
317 | | - <Button type="submit" :disabled="countrySourcesPending || !countrySourcesLoaded"> |
318 | | - {{ countrySourcesPending ? $t('settings.sources.saving') : $t('settings.sources.save') }} |
319 | | - </Button> |
320 | | - </form> |
321 | | - </CardContent> |
322 | | - </Card> |
| 97 | + <p class="text-xs text-muted-foreground">{{ $t('settings.sources.enabledCount', { enabled: enabledCountrySourceCount, total: countrySources.length }) }}</p> |
| 98 | + <Button type="submit" :disabled="countrySourcesPending || !countrySourcesLoaded"> |
| 99 | + {{ countrySourcesPending ? $t('settings.sources.saving') : $t('settings.sources.save') }} |
| 100 | + </Button> |
| 101 | + </form> |
| 102 | + </section> |
323 | 103 | </template> |
0 commit comments