@aphrody/koharu-ui 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/README.md +37 -2
  2. package/package.json +70 -4
  3. package/src/assets/NotoSansCJK-VF.otf +0 -0
  4. package/src/assets/OFL.txt +92 -0
  5. package/src/components/ClientOnly.tsx +21 -0
  6. package/src/components/ShenronExportPanel.tsx +213 -0
  7. package/src/components/app/AboutDialog.tsx +64 -0
  8. package/src/components/app/KoharuApp.tsx +58 -0
  9. package/src/components/app/StartupView.tsx +67 -0
  10. package/src/components/app/TitleBar.tsx +322 -0
  11. package/src/components/app/Updater.tsx +143 -0
  12. package/src/components/app/WindowChrome.tsx +137 -0
  13. package/src/components/controls/ColorSampling.tsx +70 -0
  14. package/src/components/controls/ColorWell.tsx +139 -0
  15. package/src/components/controls/CommitTextarea.tsx +63 -0
  16. package/src/components/controls/FontPicker.tsx +543 -0
  17. package/src/components/controls/ModelPicker.tsx +178 -0
  18. package/src/components/controls/OutputPicker.tsx +170 -0
  19. package/src/components/editor/ActivityCenter.tsx +234 -0
  20. package/src/components/editor/AgentPanel.tsx +352 -0
  21. package/src/components/editor/CanvasCommandBar.tsx +38 -0
  22. package/src/components/editor/CanvasOverlay.tsx +194 -0
  23. package/src/components/editor/CanvasWorkspace.tsx +947 -0
  24. package/src/components/editor/Editor.tsx +56 -0
  25. package/src/components/editor/InferenceControl.tsx +457 -0
  26. package/src/components/editor/Inspector.tsx +1073 -0
  27. package/src/components/editor/PageRail.tsx +581 -0
  28. package/src/components/editor/ResourceMonitor.tsx +151 -0
  29. package/src/components/editor/RightSidebar.tsx +50 -0
  30. package/src/components/editor/SelectionControls.tsx +184 -0
  31. package/src/components/editor/StatusBar.tsx +41 -0
  32. package/src/components/editor/ToolBar.tsx +182 -0
  33. package/src/components/editor/useCanvas.ts +194 -0
  34. package/src/components/preferences/GenerationPreferences.tsx +118 -0
  35. package/src/components/preferences/KoharuSettings.tsx +229 -0
  36. package/src/components/preferences/PipelinePreferences.tsx +173 -0
  37. package/src/components/preferences/PreferenceFields.tsx +149 -0
  38. package/src/components/preferences/ProviderPreferences.tsx +173 -0
  39. package/src/components/preferences/SettingsPage.tsx +336 -0
  40. package/src/components/preferences/TranslationPreferences.tsx +187 -0
  41. package/src/components/preferences/TypesettingPreferences.tsx +142 -0
  42. package/src/components/preferences/models.ts +103 -0
  43. package/src/components/start/StartView.tsx +277 -0
  44. package/src/index.ts +59 -0
  45. package/src/internal/components/accordion.tsx +72 -0
  46. package/src/internal/components/alert-dialog.tsx +189 -0
  47. package/src/internal/components/alert.tsx +76 -0
  48. package/src/internal/components/aspect-ratio.tsx +22 -0
  49. package/src/internal/components/attachment.tsx +207 -0
  50. package/src/internal/components/avatar.tsx +109 -0
  51. package/src/internal/components/badge.tsx +52 -0
  52. package/src/internal/components/breadcrumb.tsx +125 -0
  53. package/src/internal/components/bubble.tsx +128 -0
  54. package/src/internal/components/button-group.tsx +87 -0
  55. package/src/internal/components/button.tsx +58 -0
  56. package/src/internal/components/card.tsx +103 -0
  57. package/src/internal/components/checkbox.tsx +29 -0
  58. package/src/internal/components/collapsible.tsx +21 -0
  59. package/src/internal/components/combobox.tsx +298 -0
  60. package/src/internal/components/context-menu.tsx +272 -0
  61. package/src/internal/components/dialog.tsx +162 -0
  62. package/src/internal/components/direction.tsx +6 -0
  63. package/src/internal/components/drawer.tsx +230 -0
  64. package/src/internal/components/dropdown-menu.tsx +278 -0
  65. package/src/internal/components/empty.tsx +104 -0
  66. package/src/internal/components/field.tsx +238 -0
  67. package/src/internal/components/hover-card.tsx +52 -0
  68. package/src/internal/components/input-group.tsx +158 -0
  69. package/src/internal/components/input.tsx +20 -0
  70. package/src/internal/components/item.tsx +201 -0
  71. package/src/internal/components/kbd.tsx +26 -0
  72. package/src/internal/components/label.tsx +20 -0
  73. package/src/internal/components/marker.tsx +71 -0
  74. package/src/internal/components/menubar.tsx +280 -0
  75. package/src/internal/components/message.tsx +92 -0
  76. package/src/internal/components/native-select.tsx +61 -0
  77. package/src/internal/components/navigation-menu.tsx +168 -0
  78. package/src/internal/components/number-field.tsx +68 -0
  79. package/src/internal/components/pagination.tsx +130 -0
  80. package/src/internal/components/popover.tsx +91 -0
  81. package/src/internal/components/progress.tsx +83 -0
  82. package/src/internal/components/radio-group.tsx +38 -0
  83. package/src/internal/components/resizable.tsx +50 -0
  84. package/src/internal/components/scroll-area.tsx +88 -0
  85. package/src/internal/components/select.tsx +191 -0
  86. package/src/internal/components/separator.tsx +25 -0
  87. package/src/internal/components/sheet.tsx +140 -0
  88. package/src/internal/components/sidebar.tsx +723 -0
  89. package/src/internal/components/skeleton.tsx +13 -0
  90. package/src/internal/components/slider.tsx +56 -0
  91. package/src/internal/components/spinner.tsx +10 -0
  92. package/src/internal/components/switch.tsx +32 -0
  93. package/src/internal/components/table.tsx +116 -0
  94. package/src/internal/components/tabs.tsx +82 -0
  95. package/src/internal/components/textarea.tsx +18 -0
  96. package/src/internal/components/toast.tsx +235 -0
  97. package/src/internal/components/toggle-group.tsx +89 -0
  98. package/src/internal/components/toggle.tsx +45 -0
  99. package/src/internal/components/tooltip.tsx +67 -0
  100. package/src/internal/hooks/use-mobile.ts +19 -0
  101. package/src/internal/lib/utils.ts +6 -0
  102. package/src/internal/styles/globals.css +73 -0
  103. package/src/lib/backend.ts +85 -0
  104. package/src/lib/document.ts +46 -0
  105. package/src/lib/geometry.ts +272 -0
  106. package/src/lib/i18n.ts +54 -0
  107. package/src/lib/queries.ts +110 -0
  108. package/src/lib/store.ts +191 -0
  109. package/src/lib/translation.ts +35 -0
  110. package/src/locales/en-US/translation.json +536 -0
  111. package/src/locales/es-ES/translation.json +536 -0
  112. package/src/locales/ja-JP/translation.json +536 -0
  113. package/src/locales/ko-KR/translation.json +536 -0
  114. package/src/locales/pt-BR/translation.json +536 -0
  115. package/src/locales/ru-RU/translation.json +536 -0
  116. package/src/locales/tr-TR/translation.json +536 -0
  117. package/src/locales/zh-CN/translation.json +536 -0
  118. package/src/locales/zh-TW/translation.json +536 -0
  119. package/src/navigation.ts +28 -0
  120. package/src/providers.tsx +132 -0
  121. package/src/shenron-pipeline-env.d.ts +14 -0
  122. package/src/styles.css +246 -0
  123. package/tsconfig.json +21 -0
@@ -0,0 +1,173 @@
1
+ 'use client'
2
+
3
+ import { Eraser, FileText, Search } from 'lucide-react'
4
+ import { useTranslation } from 'react-i18next'
5
+
6
+ import {
7
+ defaultModel,
8
+ modelNames,
9
+ modelOptions,
10
+ replaceStage,
11
+ stageModel,
12
+ type ModelName,
13
+ type ModelStage,
14
+ type PipelineModel,
15
+ } from '@aphrody/koharu-ui/components/preferences/models'
16
+ import {
17
+ NumberField,
18
+ PreferencePage,
19
+ PreferenceRow,
20
+ PreferenceSection,
21
+ TextField,
22
+ } from '@aphrody/koharu-ui/components/preferences/PreferenceFields'
23
+ import type { PipelineConfig } from '@aphrody/koharu-bridge/protocol'
24
+ import {
25
+ Select,
26
+ SelectContent,
27
+ SelectItem,
28
+ SelectTrigger,
29
+ SelectValue,
30
+ } from '../../internal/components/select'
31
+
32
+ const stages = [
33
+ ['detection', Search],
34
+ ['ocr', FileText],
35
+ ['inpainting', Eraser],
36
+ ] as const satisfies ReadonlyArray<readonly [ModelStage, typeof Search]>
37
+
38
+ export function PipelinePreferences({
39
+ value,
40
+ onChange,
41
+ }: {
42
+ value: PipelineConfig
43
+ onChange: (value: PipelineConfig) => void
44
+ }) {
45
+ const { t } = useTranslation()
46
+ return (
47
+ <PreferencePage
48
+ title={t('settings.pipeline.title')}
49
+ description={t('settings.pipeline.description')}
50
+ >
51
+ <PreferenceSection title={t('settings.pipeline.processing')}>
52
+ {stages.map(([stage, Icon]) => {
53
+ const model = stageModel(value, stage)
54
+ const title = t(`settings.pipeline.stages.${stage}.title`)
55
+ return (
56
+ <PreferenceRow
57
+ key={stage}
58
+ title={title}
59
+ description={t(`settings.pipeline.stages.${stage}.description`)}
60
+ align='start'
61
+ >
62
+ <div className='grid gap-3'>
63
+ <div className='flex items-center gap-2'>
64
+ <Icon className='size-3.5 shrink-0 text-muted-foreground' />
65
+ <Select
66
+ value={model.model}
67
+ items={Object.fromEntries(
68
+ modelOptions[stage].map((name) => [name, modelNames[name]]),
69
+ )}
70
+ onValueChange={(name) => {
71
+ if (name) {
72
+ onChange(replaceStage(value, stage, defaultModel(name as ModelName)))
73
+ }
74
+ }}
75
+ >
76
+ <SelectTrigger
77
+ aria-label={t('settings.pipeline.modelLabel', { stage: title })}
78
+ className='h-8 min-w-0 flex-1 text-[11px]'
79
+ >
80
+ <SelectValue />
81
+ </SelectTrigger>
82
+ <SelectContent>
83
+ {modelOptions[stage].map((name) => (
84
+ <SelectItem key={name} value={name}>
85
+ {modelNames[name]}
86
+ </SelectItem>
87
+ ))}
88
+ </SelectContent>
89
+ </Select>
90
+ </div>
91
+ <ModelOptions
92
+ model={model}
93
+ onChange={(next) => onChange(replaceStage(value, stage, next))}
94
+ />
95
+ </div>
96
+ </PreferenceRow>
97
+ )
98
+ })}
99
+ </PreferenceSection>
100
+ </PreferencePage>
101
+ )
102
+ }
103
+
104
+ function ModelOptions({
105
+ model,
106
+ onChange,
107
+ }: {
108
+ model: PipelineModel
109
+ onChange: (model: PipelineModel) => void
110
+ }) {
111
+ const { t } = useTranslation()
112
+ switch (model.model) {
113
+ case 'koharu-layout-rfdetr-seg-2xl':
114
+ return (
115
+ <div className='grid grid-cols-3 gap-2'>
116
+ <NumberField
117
+ label={t('settings.pipeline.options.textThreshold')}
118
+ value={model.text_threshold ?? null}
119
+ min={0}
120
+ max={1}
121
+ step={0.05}
122
+ onChange={(text_threshold) => onChange({ ...model, text_threshold })}
123
+ />
124
+ <NumberField
125
+ label={t('settings.pipeline.options.bubbleThreshold')}
126
+ value={model.bubble_threshold ?? null}
127
+ min={0}
128
+ max={1}
129
+ step={0.05}
130
+ onChange={(bubble_threshold) => onChange({ ...model, bubble_threshold })}
131
+ />
132
+ <NumberField
133
+ label={t('settings.pipeline.options.panelThreshold')}
134
+ value={model.panel_threshold ?? null}
135
+ min={0}
136
+ max={1}
137
+ step={0.05}
138
+ onChange={(panel_threshold) => onChange({ ...model, panel_threshold })}
139
+ />
140
+ </div>
141
+ )
142
+ case 'flux2-klein':
143
+ return (
144
+ <TextField
145
+ label={t('settings.pipeline.options.prompt')}
146
+ value={model.prompt ?? 'Remove the text and reconstruct the background.'}
147
+ onChange={(prompt) => onChange({ ...model, prompt })}
148
+ />
149
+ )
150
+ case 'rorem-mixed':
151
+ return (
152
+ <div className='grid grid-cols-2 gap-2'>
153
+ <TextField
154
+ label={t('settings.pipeline.options.prompt')}
155
+ value={model.prompt ?? ''}
156
+ onChange={(prompt) => onChange({ ...model, prompt })}
157
+ />
158
+ <TextField
159
+ label={t('settings.pipeline.options.negativePrompt')}
160
+ value={model.negative_prompt ?? ''}
161
+ onChange={(negative_prompt) => onChange({ ...model, negative_prompt })}
162
+ />
163
+ </div>
164
+ )
165
+ case 'paddleocr-vl-1.6':
166
+ case 'manga-ocr':
167
+ case 'baberu-ocr':
168
+ case 'hayai-ocr':
169
+ case 'lama':
170
+ case 'aot-inpainting':
171
+ return null
172
+ }
173
+ }
@@ -0,0 +1,149 @@
1
+ 'use client'
2
+
3
+ import { useId } from 'react'
4
+ import { useTranslation } from 'react-i18next'
5
+
6
+ import { Input } from '@aphrody/m3-primitives'
7
+
8
+ export function PreferencePage({
9
+ title,
10
+ description,
11
+ children,
12
+ }: {
13
+ title: string
14
+ description: string
15
+ children: React.ReactNode
16
+ }) {
17
+ return (
18
+ <div className='max-w-3xl'>
19
+ <header className='mb-8'>
20
+ <h2 className='text-[20px] font-semibold tracking-[-0.02em]'>{title}</h2>
21
+ <p className='mt-1 max-w-xl text-[12px] leading-5 text-muted-foreground'>{description}</p>
22
+ </header>
23
+ <div className='grid gap-8'>{children}</div>
24
+ </div>
25
+ )
26
+ }
27
+
28
+ export function PreferenceSection({
29
+ title,
30
+ description,
31
+ children,
32
+ }: {
33
+ title: string
34
+ description?: string
35
+ children: React.ReactNode
36
+ }) {
37
+ return (
38
+ <section>
39
+ <div className='mb-3'>
40
+ <h3 className='text-[12px] font-semibold'>{title}</h3>
41
+ {description && (
42
+ <p className='mt-0.5 text-[11px] leading-4 text-muted-foreground'>{description}</p>
43
+ )}
44
+ </div>
45
+ <div className='divide-y divide-border rounded-xl border border-border/80 bg-[var(--surface-panel)] px-5'>
46
+ {children}
47
+ </div>
48
+ </section>
49
+ )
50
+ }
51
+
52
+ export function PreferenceRow({
53
+ title,
54
+ description,
55
+ children,
56
+ align = 'center',
57
+ }: {
58
+ title: string
59
+ description?: string
60
+ children: React.ReactNode
61
+ align?: 'center' | 'start'
62
+ }) {
63
+ return (
64
+ <div
65
+ className={`grid grid-cols-[minmax(150px,0.9fr)_minmax(220px,1.1fr)] gap-6 py-3.5 ${align === 'start' ? 'items-start' : 'items-center'}`}
66
+ >
67
+ <div>
68
+ <p className='text-[12px] font-medium'>{title}</p>
69
+ {description && (
70
+ <p className='mt-0.5 text-[10px] leading-4 text-muted-foreground'>{description}</p>
71
+ )}
72
+ </div>
73
+ <div className='min-w-0'>{children}</div>
74
+ </div>
75
+ )
76
+ }
77
+
78
+ export function TextField({
79
+ label,
80
+ value,
81
+ type = 'text',
82
+ onChange,
83
+ }: {
84
+ label: string
85
+ value: string
86
+ type?: 'text' | 'url'
87
+ onChange: (value: string) => void
88
+ }) {
89
+ return (
90
+ <label className='grid gap-1 text-[10px] text-muted-foreground'>
91
+ {label}
92
+ <Input
93
+ type={type}
94
+ value={value}
95
+ className='h-8 text-[12px] text-foreground'
96
+ onChange={(event) => onChange(event.currentTarget.value)}
97
+ />
98
+ </label>
99
+ )
100
+ }
101
+
102
+ /**
103
+ * `min` and `max` on a number input are validation hints the browser never
104
+ * enforces on typed text, so bounds are applied here before the value reaches
105
+ * the stored configuration.
106
+ */
107
+ function parseBounded(raw: string, min?: number, max?: number) {
108
+ if (raw === '') return null
109
+ const value = Number(raw)
110
+ if (!Number.isFinite(value)) return null
111
+ if (min !== undefined && value < min) return min
112
+ if (max !== undefined && value > max) return max
113
+ return value
114
+ }
115
+
116
+ export function NumberField({
117
+ label,
118
+ value,
119
+ min,
120
+ max,
121
+ step,
122
+ onChange,
123
+ }: {
124
+ label: string
125
+ value: number | null
126
+ min?: number
127
+ max?: number
128
+ step?: number
129
+ onChange: (value: number | null) => void
130
+ }) {
131
+ const { t } = useTranslation()
132
+ const id = useId()
133
+ return (
134
+ <label className='grid gap-1 text-[10px] text-muted-foreground'>
135
+ {label}
136
+ <Input
137
+ id={id}
138
+ type='number'
139
+ value={value ?? ''}
140
+ min={min}
141
+ max={max}
142
+ step={step}
143
+ placeholder={t('model.default')}
144
+ className='h-8 text-[12px] text-foreground'
145
+ onChange={(event) => onChange(parseBounded(event.currentTarget.value, min, max))}
146
+ />
147
+ </label>
148
+ )
149
+ }
@@ -0,0 +1,173 @@
1
+ 'use client'
2
+
3
+ import { Eraser } from 'lucide-react'
4
+ import { useEffect, useId, useState } from 'react'
5
+ import { useTranslation } from 'react-i18next'
6
+
7
+ import {
8
+ PreferencePage,
9
+ PreferenceRow,
10
+ PreferenceSection,
11
+ TextField,
12
+ } from '@aphrody/koharu-ui/components/preferences/PreferenceFields'
13
+ import type {
14
+ CredentialInput,
15
+ ProviderConfig,
16
+ ProviderPreference,
17
+ ProviderPreferences as ProviderSettings,
18
+ } from '@aphrody/koharu-bridge/protocol'
19
+ import { Button } from '../../internal/components/button'
20
+ import { Input } from '../../internal/components/input'
21
+
22
+
23
+ type ConfigWithSetting<T, Key extends PropertyKey> = T extends { settings: infer Settings }
24
+ ? Key extends keyof Settings
25
+ ? [Settings[Key]] extends [never]
26
+ ? never
27
+ : T
28
+ : never
29
+ : never
30
+
31
+ type BaseUrlConfig = ConfigWithSetting<ProviderConfig, 'base_url'>
32
+
33
+ export function ProviderPreferences({
34
+ value,
35
+ onChange,
36
+ }: {
37
+ value: ProviderSettings
38
+ onChange: (value: ProviderSettings) => void
39
+ }) {
40
+ const { t } = useTranslation()
41
+ return (
42
+ <PreferencePage
43
+ title={t('settings.providers.title')}
44
+ description={t('settings.providers.description')}
45
+ >
46
+ <PreferenceSection
47
+ title={t('settings.providers.connections')}
48
+ description={t('settings.providers.connectionsDescription')}
49
+ >
50
+ {value.entries.filter(isConfigurable).map((entry) => (
51
+ <PreferenceRow
52
+ key={entry.config.provider}
53
+ title={entry.name}
54
+ description={t(`providerDescriptions.${entry.config.provider}`)}
55
+ align='start'
56
+ >
57
+ <div className='grid gap-2'>
58
+ {entry.credential && (
59
+ <CredentialField
60
+ label={entry.name}
61
+ value={entry.credential}
62
+ onChange={(credential) => onChange(replaceEntry(value, { ...entry, credential }))}
63
+ />
64
+ )}
65
+ {hasBaseUrl(entry.config) && (
66
+ <TextField
67
+ label={t('model.baseUrl')}
68
+ type='url'
69
+ value={entry.config.settings.base_url ?? ''}
70
+ onChange={(base_url) =>
71
+ onChange(
72
+ replaceEntry(value, {
73
+ ...entry,
74
+ config: withBaseUrl(entry.config, base_url),
75
+ }),
76
+ )
77
+ }
78
+ />
79
+ )}
80
+ </div>
81
+ </PreferenceRow>
82
+ ))}
83
+ </PreferenceSection>
84
+ </PreferencePage>
85
+ )
86
+ }
87
+
88
+ function CredentialField({
89
+ label,
90
+ value,
91
+ onChange,
92
+ }: {
93
+ label: string
94
+ value: CredentialInput
95
+ onChange: (value: CredentialInput) => void
96
+ }) {
97
+ const { t } = useTranslation()
98
+ const inputId = useId()
99
+ const [draftValue, setDraftValue] = useState(value.value ?? '')
100
+ useEffect(() => {
101
+ if (value.value !== null) setDraftValue(value.value)
102
+ else if (!value.configured || value.clear) setDraftValue('')
103
+ }, [value.clear, value.configured, value.value])
104
+ const configured = !value.clear && (value.configured || Boolean(draftValue))
105
+ return (
106
+ <div className='grid gap-1'>
107
+ <label htmlFor={inputId} className='text-[10px] text-muted-foreground'>
108
+ {t('settings.providers.credential')}
109
+ </label>
110
+ <div className='flex gap-2'>
111
+ <Input
112
+ id={inputId}
113
+ aria-label={t('settings.providers.credentialLabel', { provider: label })}
114
+ type='text'
115
+ autoComplete='off'
116
+ autoCapitalize='none'
117
+ spellCheck={false}
118
+ value={draftValue}
119
+ placeholder={
120
+ configured ? t('settings.providers.configured') : t('settings.providers.notConfigured')
121
+ }
122
+ className='h-8 min-w-0 flex-1 text-[12px] [-webkit-text-security:disc] [&::placeholder]:[-webkit-text-security:none]'
123
+ onChange={(event) => {
124
+ const draft = event.currentTarget.value
125
+ setDraftValue(draft)
126
+ onChange({ ...value, value: draft || null, clear: false })
127
+ }}
128
+ />
129
+ {configured && (
130
+ <Button
131
+ type='button'
132
+ variant='outline'
133
+ size='icon'
134
+ aria-label={t('settings.providers.clearCredential', { provider: label })}
135
+ onClick={() => {
136
+ setDraftValue('')
137
+ onChange({ configured: false, value: null, clear: true })
138
+ }}
139
+ >
140
+ <Eraser />
141
+ </Button>
142
+ )}
143
+ </div>
144
+ </div>
145
+ )
146
+ }
147
+
148
+ function isConfigurable(entry: ProviderPreference): boolean {
149
+ return entry.credential !== null || hasBaseUrl(entry.config)
150
+ }
151
+
152
+ function hasBaseUrl(config: ProviderConfig): config is BaseUrlConfig {
153
+ return 'base_url' in config.settings
154
+ }
155
+
156
+ function withBaseUrl(config: ProviderConfig, base_url: string): ProviderConfig {
157
+ if (!hasBaseUrl(config)) return config
158
+ return {
159
+ ...config,
160
+ settings: { ...config.settings, base_url: base_url || null },
161
+ } as ProviderConfig
162
+ }
163
+
164
+ function replaceEntry(
165
+ preferences: ProviderSettings,
166
+ replacement: ProviderPreference,
167
+ ): ProviderSettings {
168
+ return {
169
+ entries: preferences.entries.map((entry) =>
170
+ entry.config.provider === replacement.config.provider ? replacement : entry,
171
+ ),
172
+ }
173
+ }