@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,110 @@
1
+ 'use client'
2
+
3
+ import {
4
+ QueryClient,
5
+ type QueryKey,
6
+ queryOptions,
7
+ useIsMutating,
8
+ useMutation,
9
+ useQuery,
10
+ } from '@tanstack/react-query'
11
+
12
+ import { commands, type FontFamily } from '@aphrody/koharu-bridge/protocol'
13
+
14
+ import { call } from './backend'
15
+
16
+ export const projectKey = ['project'] as const
17
+ export const pagesKey = ['pages'] as const
18
+ export const pageKey = ['page'] as const
19
+ export const preparedPageKey = (page: string) => ['prepared-page', page] as const
20
+ export const fontsKey = ['fonts'] as const
21
+
22
+ const projectQuery = queryOptions({
23
+ queryKey: projectKey,
24
+ queryFn: () => call(commands.getProject),
25
+ })
26
+
27
+ const pagesQuery = queryOptions({
28
+ queryKey: pagesKey,
29
+ queryFn: () => call(commands.getPages),
30
+ })
31
+
32
+ const pageQuery = queryOptions({
33
+ queryKey: pageKey,
34
+ queryFn: () => call(commands.getPage),
35
+ })
36
+
37
+ const fontsQuery = queryOptions({
38
+ queryKey: fontsKey,
39
+ queryFn: () => call(commands.getFonts),
40
+ })
41
+
42
+ export const queryClient = new QueryClient({
43
+ defaultOptions: {
44
+ queries: {
45
+ staleTime: Number.POSITIVE_INFINITY,
46
+ retry: false,
47
+ refetchOnReconnect: false,
48
+ refetchOnWindowFocus: false,
49
+ },
50
+ },
51
+ })
52
+
53
+ export function useProject(enabled = true) {
54
+ return useQuery({ ...projectQuery, enabled })
55
+ }
56
+
57
+ export function usePages(enabled = true) {
58
+ return useQuery({ ...pagesQuery, enabled })
59
+ }
60
+
61
+ export function usePage(enabled = true) {
62
+ return useQuery({ ...pageQuery, enabled })
63
+ }
64
+
65
+ export function useFonts(enabled = true) {
66
+ return useQuery({ ...fontsQuery, enabled })
67
+ }
68
+
69
+ export function useFontPreview(font: FontFamily | undefined, enabled = true) {
70
+ return useQuery({
71
+ queryKey: ['font-preview', font?.name],
72
+ queryFn: async () => {
73
+ if (!font) return null
74
+ try {
75
+ return new Uint8Array(await commands.getFontPreview(font.name))
76
+ } catch {
77
+ return null
78
+ }
79
+ },
80
+ enabled: enabled && font !== undefined,
81
+ gcTime: 5 * 60 * 1000,
82
+ })
83
+ }
84
+
85
+ export function useCommand<Args extends unknown[], Result>(
86
+ key: QueryKey,
87
+ command: (...args: Args) => Promise<Result>,
88
+ label: string,
89
+ onSuccess?: () => Promise<void>,
90
+ ) {
91
+ const busy = useIsMutating({ mutationKey: key }) > 0
92
+ const mutation = useMutation({
93
+ mutationKey: key,
94
+ mutationFn: (args: Args) => call(command, ...args),
95
+ meta: { activity: label },
96
+ onSuccess,
97
+ })
98
+ return { run: (...args: Args) => mutation.mutate(args), busy }
99
+ }
100
+
101
+ export function useImportPages() {
102
+ const { run, busy } = useCommand(['import-pages'], commands.import, 'navigator.importing', () =>
103
+ refresh(projectKey, pagesKey, pageKey),
104
+ )
105
+ return { importPages: run, importing: busy }
106
+ }
107
+
108
+ export async function refresh(...keys: QueryKey[]): Promise<void> {
109
+ await Promise.all(keys.map((queryKey) => queryClient.invalidateQueries({ queryKey })))
110
+ }
@@ -0,0 +1,191 @@
1
+ 'use client'
2
+
3
+ import { create } from 'zustand'
4
+
5
+ import type {
6
+ CanvasState,
7
+ Download,
8
+ EntityId,
9
+ Job,
10
+ Model,
11
+ Frame,
12
+ Preferences,
13
+ Stage,
14
+ StartupState,
15
+ ModelResources,
16
+ } from '@aphrody/koharu-bridge/protocol'
17
+ import { toast } from '../internal/components/toast'
18
+
19
+ export type CanvasTool = 'select' | 'text' | 'draw' | 'eraser' | 'color_picker' | 'remove' | 'pan'
20
+ export const MIN_BRUSH_DIAMETER = 1
21
+ export const MAX_BRUSH_DIAMETER = 128
22
+
23
+ export function isBrushTool(tool: CanvasTool): boolean {
24
+ return tool === 'draw' || tool === 'eraser' || tool === 'remove'
25
+ }
26
+
27
+ export interface CanvasBrush {
28
+ diameter: number
29
+ color: string
30
+ }
31
+ export type InspectorSection = 'copy' | 'type' | 'layers'
32
+ export type ShortcutAction = CanvasTool | 'fit'
33
+ export type Shortcuts = Record<ShortcutAction, string>
34
+ export type PipelineScope = 'page' | 'selected-pages' | 'project'
35
+ export const pipelineStages: readonly Stage[] = ['detection', 'ocr', 'translation', 'inpainting']
36
+
37
+ interface KoharuStore {
38
+ initialized: boolean
39
+ preferences: Preferences | null
40
+ translationModels: Model[]
41
+ resources: ModelResources | null
42
+ jobs: Record<string, Job>
43
+ downloads: Record<string, Download>
44
+ camera: { zoom: number; translation: [number, number]; fitted: boolean }
45
+ canvasPage: EntityId | null
46
+ canvasRevision: number | null
47
+ canvasGeneration: number
48
+ canvasSize: [number, number]
49
+ fitCanvasRequest: number
50
+ layerFrames: Record<EntityId, Frame>
51
+ selectedLayers: EntityId[]
52
+ selectedPages: EntityId[]
53
+ tool: CanvasTool
54
+ brush: CanvasBrush
55
+ inspector: InspectorSection
56
+ processingScope: PipelineScope
57
+ processingStages: Stage[]
58
+ settingsOpen: boolean
59
+ shortcuts: Shortcuts
60
+ selectPages: (pages: EntityId[]) => void
61
+ showInspector: (section: InspectorSection) => void
62
+ setProcessingScope: (scope: PipelineScope) => void
63
+ setProcessingStages: (stages: Stage[]) => void
64
+ setSettingsOpen: (open: boolean) => void
65
+ selectLayers: (layers: EntityId[]) => void
66
+ setTool: (tool: CanvasTool) => void
67
+ setBrush: (brush: CanvasBrush) => void
68
+ setShortcut: (action: ShortcutAction, key: string) => void
69
+ requestCanvasFit: () => void
70
+ dismissJob: (id: string) => void
71
+ dismissDownload: (id: number) => void
72
+ }
73
+
74
+ export const defaultShortcuts: Shortcuts = {
75
+ select: 'v',
76
+ text: 't',
77
+ draw: 'b',
78
+ eraser: 'e',
79
+ color_picker: 'i',
80
+ remove: 'j',
81
+ pan: 'h',
82
+ fit: '0',
83
+ }
84
+
85
+ export const useKoharuStore = create<KoharuStore>()((set) => ({
86
+ initialized: false,
87
+ preferences: null,
88
+ translationModels: [],
89
+ resources: null,
90
+ jobs: {},
91
+ downloads: {},
92
+ camera: { zoom: 1, translation: [0, 0], fitted: true },
93
+ canvasPage: null,
94
+ canvasRevision: null,
95
+ canvasGeneration: 0,
96
+ canvasSize: [0, 0],
97
+ fitCanvasRequest: 0,
98
+ layerFrames: {},
99
+ selectedLayers: [],
100
+ selectedPages: [],
101
+ tool: 'select',
102
+ brush: { diameter: 48, color: '#FFFFFF' },
103
+ inspector: 'copy',
104
+ processingScope: 'selected-pages',
105
+ processingStages: [...pipelineStages],
106
+ settingsOpen: false,
107
+ shortcuts: defaultShortcuts,
108
+ selectPages: (selectedPages) => set({ selectedPages: [...new Set(selectedPages)] }),
109
+ showInspector: (inspector) => set({ inspector }),
110
+ setProcessingScope: (processingScope) => set({ processingScope }),
111
+ setProcessingStages: (processingStages) => set({ processingStages }),
112
+ setSettingsOpen: (settingsOpen) => set({ settingsOpen }),
113
+ selectLayers: (selectedLayers) => set({ selectedLayers: [...new Set(selectedLayers)] }),
114
+ setTool: (tool) => set({ tool }),
115
+ setBrush: (brush) => set({ brush }),
116
+ setShortcut: (action, key) =>
117
+ set((state) => ({
118
+ shortcuts: { ...state.shortcuts, [action]: key.toLowerCase().slice(0, 1) },
119
+ })),
120
+ requestCanvasFit: () => set((state) => ({ fitCanvasRequest: state.fitCanvasRequest + 1 })),
121
+ dismissJob: (id) =>
122
+ set((state) => {
123
+ const jobs = { ...state.jobs }
124
+ delete jobs[id]
125
+ return { jobs }
126
+ }),
127
+ dismissDownload: (id) =>
128
+ set((state) => {
129
+ const downloads = { ...state.downloads }
130
+ delete downloads[String(id)]
131
+ return { downloads }
132
+ }),
133
+ }))
134
+
135
+ export function receiveStartupState(state: StartupState): void {
136
+ useKoharuStore.setState({
137
+ initialized: true,
138
+ preferences: state.preferences,
139
+ jobs: byId(state.jobs),
140
+ ...canvasSnapshot(state.canvas),
141
+ })
142
+ }
143
+
144
+ export function receiveCanvas(canvas: CanvasState): void {
145
+ useKoharuStore.setState(canvasSnapshot(canvas))
146
+ }
147
+
148
+ export function receiveJob(job: Job): void {
149
+ useKoharuStore.setState((state) => ({ jobs: { ...state.jobs, [job.id]: job } }))
150
+ }
151
+
152
+ export function receiveDownload(download: Download): void {
153
+ useKoharuStore.setState((state) => {
154
+ const downloads = { ...state.downloads }
155
+ if (download.state === 'finished') delete downloads[String(download.id)]
156
+ else downloads[String(download.id)] = download
157
+ return { downloads }
158
+ })
159
+ }
160
+
161
+ export function receivePreferences(preferences: Preferences): void {
162
+ useKoharuStore.setState({ preferences })
163
+ }
164
+
165
+ export function receiveTranslationModels(translationModels: Model[]): void {
166
+ useKoharuStore.setState({ translationModels })
167
+ }
168
+
169
+ export function receiveResources(resources: ModelResources): void {
170
+ useKoharuStore.setState({ resources })
171
+ }
172
+
173
+ export function receiveError(message: string): void {
174
+ toast.add({ type: 'error', title: 'Could not complete that action', description: message })
175
+ }
176
+
177
+ function canvasSnapshot(canvas: CanvasState) {
178
+ return {
179
+ canvasPage: canvas.page,
180
+ canvasRevision: canvas.revision,
181
+ canvasGeneration: canvas.generation,
182
+ canvasSize: canvas.size,
183
+ layerFrames: Object.fromEntries(
184
+ canvas.element_frames.map(({ element, frame }) => [element, frame]),
185
+ ),
186
+ }
187
+ }
188
+
189
+ function byId<T extends { id: string | number }>(items: T[]): Record<string, T> {
190
+ return Object.fromEntries(items.map((item) => [String(item.id), item]))
191
+ }
@@ -0,0 +1,35 @@
1
+ import type {
2
+ LanguageChoice,
3
+ Model,
4
+ ModelSelection,
5
+ Provider,
6
+ ProviderPreference,
7
+ } from '@aphrody/koharu-bridge/protocol'
8
+
9
+ export function providerName(entries: ProviderPreference[], provider: Provider): string {
10
+ return entries.find((entry) => entry.config.provider === provider)?.name ?? provider
11
+ }
12
+
13
+ export function modelKey(model: Model | ModelSelection): string {
14
+ return `${model.provider}:${model.model ?? ''}`
15
+ }
16
+
17
+ export function modelSelection(model: Model, quantization: string | null): ModelSelection {
18
+ return {
19
+ provider: model.provider,
20
+ model: model.model,
21
+ quantization,
22
+ vision: model.vision,
23
+ reasoning: model.reasoning,
24
+ }
25
+ }
26
+
27
+ export function orderedLanguageChoices(
28
+ languages: readonly LanguageChoice[],
29
+ ): Array<{ tag: string; name: string }> {
30
+ return languages
31
+ .map((language) => ({ tag: language.tag, name: language.name }))
32
+ .toSorted((left, right) =>
33
+ left.name.localeCompare(right.name, undefined, { numeric: true, sensitivity: 'base' }),
34
+ )
35
+ }