Skip to content
This repository was archived by the owner on May 29, 2026. It is now read-only.

Commit a7bf5a7

Browse files
committed
feat(admin): sync master changes — thumbhash migration and translation review
- migrate blurhash → thumbhash across models, api, files feature, and rich-editor types; replace blurhash decoder in FileThumbnail with thumbHashToDataURL - add translation review section to AI settings (enableTranslationReview, translationReviewModel, translationReviewScoreThreshold) with en/zh strings Ports master commits 980fe52, 24aadd8, and c746c33 to the React branch.
1 parent d071e9e commit a7bf5a7

17 files changed

Lines changed: 121 additions & 70 deletions

File tree

apps/admin/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@
6868
"@xterm/addon-fit": "0.11.0",
6969
"@xterm/xterm": "6.0.0",
7070
"better-auth": "1.4.18",
71-
"blurhash": "2.0.5",
7271
"buffer": "6.0.3",
7372
"canvas-confetti": "1.9.4",
7473
"date-fns": "4.1.0",
@@ -101,6 +100,7 @@
101100
"shiki": "3.21.0",
102101
"socket.io-client": "4.8.3",
103102
"sonner": "2.0.7",
103+
"thumbhash": "0.1.1",
104104
"tinykeys": "4.0.0",
105105
"validator": "13.15.26",
106106
"xss": "1.0.15",

apps/admin/src/api/files.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,22 @@ import { translate } from '~/i18n/translate'
44
import { deleteJson, getJson, patchJson, requestJson } from './http'
55

66
export interface FileItem {
7-
blurhash?: null | string
7+
thumbhash?: null | string
88
created?: number
99
name: string
1010
palette?: { dominant?: string; swatches?: string[] } | null
1111
url: string
1212
}
1313

1414
export interface UploadResponse {
15-
blurhash?: null | string
15+
thumbhash?: null | string
1616
name: string
1717
palette?: { dominant?: string; swatches?: string[] } | null
1818
url: string
1919
}
2020

2121
export interface OrphanFile {
22-
blurhash?: null | string
22+
thumbhash?: null | string
2323
byteSize?: null | number
2424
createdAt: string
2525
detachedAt?: null | string
@@ -55,7 +55,7 @@ export interface CleanupResult {
5555
}
5656

5757
export interface CommentUploadFile {
58-
blurhash?: null | string
58+
thumbhash?: null | string
5959
byteSize?: number
6060
createdAt: string
6161
detachedAt?: string

apps/admin/src/features/files/components/CommentImagesPage.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ export function CommentImagesPage() {
330330
<section className="h-full min-h-0">
331331
{selectedItem ? (
332332
<FileDetailPane
333-
blurhash={selectedItem.blurhash}
333+
thumbhash={selectedItem.thumbhash}
334334
deleteDisabled={deleteMutation.isPending}
335335
dominantColor={selectedItem.palette?.dominant}
336336
isMobile={!isDesktop}
@@ -460,10 +460,10 @@ function buildSections(args: {
460460
value: <PaletteSwatches palette={raw.palette} />,
461461
},
462462
{
463-
key: 'blurhash',
464-
label: t('files.detail.field.blurhash'),
463+
key: 'thumbhash',
464+
label: t('files.detail.field.thumbhash'),
465465
mono: true,
466-
value: raw.blurhash ?? unknown,
466+
value: raw.thumbhash ?? unknown,
467467
},
468468
]}
469469
/>

apps/admin/src/features/files/components/FileDetailPane.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export interface DetailSection {
2121
interface FileDetailPaneProps {
2222
name: string
2323
url: string
24-
blurhash?: null | string
24+
thumbhash?: null | string
2525
dominantColor?: string
2626
sections: DetailSection[]
2727
isMobile?: boolean
@@ -98,7 +98,7 @@ export function FileDetailPane(props: FileDetailPaneProps) {
9898
{showImage ? (
9999
<DetailHeroImage
100100
alt={props.name}
101-
blurhash={props.blurhash}
101+
thumbhash={props.thumbhash}
102102
dominantColor={props.dominantColor}
103103
onClick={props.onOpenPreview}
104104
onDimensions={props.onDimensions}
@@ -120,7 +120,7 @@ export function FileDetailPane(props: FileDetailPaneProps) {
120120

121121
function DetailHeroImage(props: {
122122
alt: string
123-
blurhash?: null | string
123+
thumbhash?: null | string
124124
dominantColor?: string
125125
onClick?: () => void
126126
onDimensions?: (dim: { width: number; height: number }) => void
@@ -151,7 +151,7 @@ function DetailHeroImage(props: {
151151
>
152152
<FileThumbnail
153153
alt={props.alt}
154-
blurhash={props.blurhash}
154+
thumbhash={props.thumbhash}
155155
className="max-h-[50vh] w-full object-contain"
156156
dominantColor={props.dominantColor}
157157
src={props.src}

apps/admin/src/features/files/components/FileListRow.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export function FileListRow<TRaw>(props: FileListRowProps<TRaw>) {
6969
{showImage ? (
7070
<FileThumbnail
7171
alt={props.item.name}
72-
blurhash={props.item.blurhash}
72+
thumbhash={props.item.thumbhash}
7373
className="h-full w-full object-cover"
7474
dominantColor={props.item.palette?.dominant}
7575
src={props.item.url}

apps/admin/src/features/files/components/FileThumbnail.tsx

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,23 @@
11
import { useEffect, useMemo, useState } from 'react'
2-
import { decode } from 'blurhash'
2+
import { thumbHashToDataURL } from 'thumbhash'
33

44
import { cn } from '~/utils/cn'
55

66
import { isPreviewColor } from '../utils/format'
77

8-
const PREVIEW_SIZE = 32
9-
108
interface FileThumbnailProps {
119
src: string
1210
alt: string
13-
blurhash?: null | string
11+
thumbhash?: null | string
1412
dominantColor?: string
1513
className?: string
1614
}
1715

1816
export function FileThumbnail(props: FileThumbnailProps) {
1917
const [loaded, setLoaded] = useState(false)
2018
const placeholder = useMemo(
21-
() =>
22-
props.blurhash
23-
? decodeBlurhashToDataUrl(props.blurhash, PREVIEW_SIZE)
24-
: null,
25-
[props.blurhash],
19+
() => (props.thumbhash ? decodeThumbhashToDataUrl(props.thumbhash) : null),
20+
[props.thumbhash],
2621
)
2722
const backgroundColor = isPreviewColor(props.dominantColor)
2823
? props.dominantColor
@@ -74,19 +69,13 @@ export function FileThumbnail(props: FileThumbnailProps) {
7469
)
7570
}
7671

77-
function decodeBlurhashToDataUrl(hash: string, size: number): null | string {
72+
function decodeThumbhashToDataUrl(hash: string): null | string {
7873
try {
79-
if (typeof document === 'undefined') return null
80-
const pixels = decode(hash, size, size)
81-
const canvas = document.createElement('canvas')
82-
canvas.width = size
83-
canvas.height = size
84-
const context = canvas.getContext('2d')
85-
if (!context) return null
86-
const imageData = context.createImageData(size, size)
87-
imageData.data.set(pixels)
88-
context.putImageData(imageData, 0, 0)
89-
return canvas.toDataURL()
74+
if (typeof window === 'undefined') return null
75+
const bin = atob(hash)
76+
const bytes = new Uint8Array(bin.length)
77+
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i)
78+
return thumbHashToDataURL(bytes)
9079
} catch {
9180
return null
9281
}

apps/admin/src/features/files/components/FilesByTypePage.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@ export function FilesByTypePage() {
352352
<section className="h-full min-h-0">
353353
{selectedItem ? (
354354
<FileDetailPane
355-
blurhash={selectedItem.blurhash}
355+
thumbhash={selectedItem.thumbhash}
356356
deleteDisabled={deleteMutation.isPending}
357357
dominantColor={selectedItem.palette?.dominant}
358358
isMobile={!isDesktop}
@@ -463,10 +463,10 @@ function buildSections(args: {
463463
value: <PaletteSwatches palette={item.palette} />,
464464
},
465465
{
466-
key: 'blurhash',
467-
label: t('files.detail.field.blurhash'),
466+
key: 'thumbhash',
467+
label: t('files.detail.field.thumbhash'),
468468
mono: true,
469-
value: item.blurhash ?? unknown,
469+
value: item.thumbhash ?? unknown,
470470
},
471471
]}
472472
/>

apps/admin/src/features/files/components/OrphanFilesPage.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,7 @@ export function OrphanFilesPage() {
423423
<section className="h-full min-h-0">
424424
{selectedItem ? (
425425
<FileDetailPane
426-
blurhash={selectedItem.blurhash}
426+
thumbhash={selectedItem.thumbhash}
427427
deleteDisabled={deleteMutation.isPending}
428428
dominantColor={selectedItem.palette?.dominant}
429429
isMobile={!isDesktop}
@@ -550,10 +550,10 @@ function buildSections(args: { item: FileRowItem<OrphanFile>; t: Translator }) {
550550
value: <PaletteSwatches palette={raw.palette} />,
551551
},
552552
{
553-
key: 'blurhash',
554-
label: t('files.detail.field.blurhash'),
553+
key: 'thumbhash',
554+
label: t('files.detail.field.thumbhash'),
555555
mono: true,
556-
value: raw.blurhash ?? unknown,
556+
value: raw.thumbhash ?? unknown,
557557
},
558558
]}
559559
/>

apps/admin/src/features/files/utils/adapters.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export interface FileRowItem<TRaw = unknown> {
2121
id: string
2222
name: string
2323
url: string
24-
blurhash?: null | string
24+
thumbhash?: null | string
2525
palette?: { dominant?: string; swatches?: string[] } | null
2626
primary: string
2727
secondary?: string
@@ -41,7 +41,7 @@ export function adaptFileItem(item: FileItem): FileRowItem<FileItem> {
4141
id: item.name,
4242
name: item.name,
4343
url: item.url,
44-
blurhash: item.blurhash,
44+
thumbhash: item.thumbhash,
4545
palette: item.palette,
4646
primary: item.name,
4747
secondary: created,
@@ -66,7 +66,7 @@ export function adaptOrphanFile(
6666
id: item.id,
6767
name: item.fileName,
6868
url: item.fileUrl,
69-
blurhash: item.blurhash,
69+
thumbhash: item.thumbhash,
7070
palette: item.palette,
7171
primary: item.fileName,
7272
secondary: meta,
@@ -109,7 +109,7 @@ export function adaptCommentUpload(
109109
id: item.id,
110110
name: item.fileName,
111111
url: item.fileUrl,
112-
blurhash: item.blurhash,
112+
thumbhash: item.thumbhash,
113113
palette: item.palette,
114114
primary: item.fileName,
115115
secondary: meta,

apps/admin/src/features/settings/components/ai/AIConfigEditor.tsx

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -254,15 +254,26 @@ export function AIConfigEditor(props: {
254254

255255
<FeatureSection
256256
assignment={
257-
<AIModelAssignmentField
258-
label={t('settings.ai.assignment.translationLabel')}
259-
models={providerModels}
260-
onChange={(translationModel) =>
261-
updateConfig({ translationModel })
262-
}
263-
providers={providers}
264-
value={props.value.translationModel}
265-
/>
257+
<>
258+
<AIModelAssignmentField
259+
label={t('settings.ai.assignment.translationLabel')}
260+
models={providerModels}
261+
onChange={(translationModel) =>
262+
updateConfig({ translationModel })
263+
}
264+
providers={providers}
265+
value={props.value.translationModel}
266+
/>
267+
<AIModelAssignmentField
268+
label={t('settings.ai.assignment.translationReviewLabel')}
269+
models={providerModels}
270+
onChange={(translationReviewModel) =>
271+
updateConfig({ translationReviewModel })
272+
}
273+
providers={providers}
274+
value={props.value.translationReviewModel}
275+
/>
276+
</>
266277
}
267278
description={t('settings.ai.section.translationDescription')}
268279
enabled={Boolean(props.value.enableTranslation)}
@@ -280,6 +291,40 @@ export function AIConfigEditor(props: {
280291
updateConfig({ enableAutoGenerateTranslation })
281292
}
282293
/>
294+
<Switch
295+
checked={Boolean(props.value.enableTranslationReview)}
296+
disabled={!props.value.enableTranslation}
297+
label={t('settings.ai.switch.enableTranslationReview')}
298+
onCheckedChange={(enableTranslationReview) =>
299+
updateConfig({ enableTranslationReview })
300+
}
301+
/>
302+
<TextInput
303+
disabled={
304+
!props.value.enableTranslation ||
305+
!props.value.enableTranslationReview
306+
}
307+
inputMode="numeric"
308+
label={t('settings.ai.switch.translationReviewScoreThreshold')}
309+
min={0}
310+
onChange={(value) => {
311+
const trimmed = value.trim()
312+
if (!trimmed) {
313+
updateConfig({ translationReviewScoreThreshold: 85 })
314+
return
315+
}
316+
const parsed = Number(trimmed)
317+
if (Number.isNaN(parsed)) return
318+
updateConfig({
319+
translationReviewScoreThreshold: Math.max(
320+
0,
321+
Math.min(100, parsed),
322+
),
323+
})
324+
}}
325+
type="number"
326+
value={String(props.value.translationReviewScoreThreshold ?? 85)}
327+
/>
283328
<AITextListField
284329
disabled={!props.value.enableTranslation}
285330
label={t('settings.ai.switch.translationTargetLanguages')}

0 commit comments

Comments
 (0)