dsh-plugin-image-workbench 0.0.0-stage → 1.1.6

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 (186) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +258 -0
  3. package/README.md +260 -2
  4. package/client.js +241 -0
  5. package/cordis.patch.yml +14 -0
  6. package/host.js +692 -0
  7. package/install.mjs +333 -0
  8. package/package.json +58 -4
  9. package/scripts/fetch-style-images.mjs +104 -0
  10. package/scripts/vendor-snapshot.py +154 -0
  11. package/server/access.py +26 -0
  12. package/server/app.py +1329 -0
  13. package/server/catalog.json +1 -0
  14. package/server/catalog.py +147 -0
  15. package/server/composer.py +391 -0
  16. package/server/drafts.py +158 -0
  17. package/server/history.py +575 -0
  18. package/server/imaging.py +172 -0
  19. package/server/pool.py +1335 -0
  20. package/server/pool_admin.py +921 -0
  21. package/server/pool_service.py +1073 -0
  22. package/server/requirements.txt +6 -0
  23. package/server/smoke_check.py +402 -0
  24. package/server/static/access.html +202 -0
  25. package/server/static/index.html +4725 -0
  26. package/vendor/chatgpt2api/README.md +13 -0
  27. package/vendor/chatgpt2api/SOURCE.json +11 -0
  28. package/vendor/chatgpt2api/runtime/uv.exe +0 -0
  29. package/vendor/chatgpt2api/source/.env.example +82 -0
  30. package/vendor/chatgpt2api/source/.python-version +1 -0
  31. package/vendor/chatgpt2api/source/LICENSE +21 -0
  32. package/vendor/chatgpt2api/source/README.md +391 -0
  33. package/vendor/chatgpt2api/source/VERSION +1 -0
  34. package/vendor/chatgpt2api/source/api/__init__.py +2 -0
  35. package/vendor/chatgpt2api/source/api/accounts.py +537 -0
  36. package/vendor/chatgpt2api/source/api/ai.py +217 -0
  37. package/vendor/chatgpt2api/source/api/app.py +62 -0
  38. package/vendor/chatgpt2api/source/api/errors.py +46 -0
  39. package/vendor/chatgpt2api/source/api/image_inputs.py +309 -0
  40. package/vendor/chatgpt2api/source/api/image_tasks.py +120 -0
  41. package/vendor/chatgpt2api/source/api/support.py +134 -0
  42. package/vendor/chatgpt2api/source/api/system.py +365 -0
  43. package/vendor/chatgpt2api/source/assets/atlascloud.svg +1 -0
  44. package/vendor/chatgpt2api/source/assets/ipwo.png +0 -0
  45. package/vendor/chatgpt2api/source/assets/kunrouai.svg +50 -0
  46. package/vendor/chatgpt2api/source/config.default.json +108 -0
  47. package/vendor/chatgpt2api/source/main.py +9 -0
  48. package/vendor/chatgpt2api/source/pyproject.toml +27 -0
  49. package/vendor/chatgpt2api/source/scripts/init_proxy_config.py +186 -0
  50. package/vendor/chatgpt2api/source/scripts/migrate_storage.py +163 -0
  51. package/vendor/chatgpt2api/source/scripts/privoxy-warp.conf +13 -0
  52. package/vendor/chatgpt2api/source/scripts/test_outlook_token_mailbox.py +385 -0
  53. package/vendor/chatgpt2api/source/scripts/test_storage.py +129 -0
  54. package/vendor/chatgpt2api/source/scripts/verify_oauth_refresh.py +92 -0
  55. package/vendor/chatgpt2api/source/services/__init__.py +0 -0
  56. package/vendor/chatgpt2api/source/services/account_service.py +1757 -0
  57. package/vendor/chatgpt2api/source/services/auth_service.py +253 -0
  58. package/vendor/chatgpt2api/source/services/backup_service.py +671 -0
  59. package/vendor/chatgpt2api/source/services/config.py +667 -0
  60. package/vendor/chatgpt2api/source/services/content_filter.py +242 -0
  61. package/vendor/chatgpt2api/source/services/cpa_service.py +315 -0
  62. package/vendor/chatgpt2api/source/services/editable_file_task_service.py +260 -0
  63. package/vendor/chatgpt2api/source/services/image_service.py +376 -0
  64. package/vendor/chatgpt2api/source/services/image_storage_service.py +405 -0
  65. package/vendor/chatgpt2api/source/services/image_tags_service.py +76 -0
  66. package/vendor/chatgpt2api/source/services/image_task_service.py +550 -0
  67. package/vendor/chatgpt2api/source/services/log_service.py +337 -0
  68. package/vendor/chatgpt2api/source/services/model_service.py +182 -0
  69. package/vendor/chatgpt2api/source/services/oauth_login_service.py +267 -0
  70. package/vendor/chatgpt2api/source/services/openai_backend_api.py +2763 -0
  71. package/vendor/chatgpt2api/source/services/openai_oauth.py +32 -0
  72. package/vendor/chatgpt2api/source/services/protocol/__init__.py +2 -0
  73. package/vendor/chatgpt2api/source/services/protocol/anthropic_v1_messages.py +307 -0
  74. package/vendor/chatgpt2api/source/services/protocol/chat_completion_cache.py +238 -0
  75. package/vendor/chatgpt2api/source/services/protocol/conversation.py +1644 -0
  76. package/vendor/chatgpt2api/source/services/protocol/error_response.py +124 -0
  77. package/vendor/chatgpt2api/source/services/protocol/openai_search.py +19 -0
  78. package/vendor/chatgpt2api/source/services/protocol/openai_v1_chat_complete.py +316 -0
  79. package/vendor/chatgpt2api/source/services/protocol/openai_v1_image_edit.py +86 -0
  80. package/vendor/chatgpt2api/source/services/protocol/openai_v1_image_generations.py +42 -0
  81. package/vendor/chatgpt2api/source/services/protocol/openai_v1_models.py +53 -0
  82. package/vendor/chatgpt2api/source/services/protocol/openai_v1_response.py +453 -0
  83. package/vendor/chatgpt2api/source/services/protocol/web_search_tool.py +160 -0
  84. package/vendor/chatgpt2api/source/services/proxy_service.py +648 -0
  85. package/vendor/chatgpt2api/source/services/storage/__init__.py +5 -0
  86. package/vendor/chatgpt2api/source/services/storage/base.py +38 -0
  87. package/vendor/chatgpt2api/source/services/storage/database_storage.py +194 -0
  88. package/vendor/chatgpt2api/source/services/storage/factory.py +104 -0
  89. package/vendor/chatgpt2api/source/services/storage/git_storage.py +183 -0
  90. package/vendor/chatgpt2api/source/services/storage/json_storage.py +95 -0
  91. package/vendor/chatgpt2api/source/services/sub2api_service.py +531 -0
  92. package/vendor/chatgpt2api/source/utils/__init__.py +0 -0
  93. package/vendor/chatgpt2api/source/utils/helper.py +469 -0
  94. package/vendor/chatgpt2api/source/utils/image_tokens.py +346 -0
  95. package/vendor/chatgpt2api/source/utils/log.py +110 -0
  96. package/vendor/chatgpt2api/source/utils/pkce.py +19 -0
  97. package/vendor/chatgpt2api/source/utils/pow.py +207 -0
  98. package/vendor/chatgpt2api/source/utils/sentinel.py +159 -0
  99. package/vendor/chatgpt2api/source/utils/turnstile.py +184 -0
  100. package/vendor/chatgpt2api/source/uv.lock +904 -0
  101. package/vendor/chatgpt2api/source/web/bun.lock +1501 -0
  102. package/vendor/chatgpt2api/source/web/components.json +24 -0
  103. package/vendor/chatgpt2api/source/web/eslint.config.mjs +17 -0
  104. package/vendor/chatgpt2api/source/web/next.config.ts +40 -0
  105. package/vendor/chatgpt2api/source/web/package.json +58 -0
  106. package/vendor/chatgpt2api/source/web/postcss.config.mjs +5 -0
  107. package/vendor/chatgpt2api/source/web/public/github.svg +1 -0
  108. package/vendor/chatgpt2api/source/web/public/next.svg +1 -0
  109. package/vendor/chatgpt2api/source/web/public/openai.svg +1 -0
  110. package/vendor/chatgpt2api/source/web/public/vercel.svg +1 -0
  111. package/vendor/chatgpt2api/source/web/src/app/accounts/components/account-import-dialog.tsx +925 -0
  112. package/vendor/chatgpt2api/source/web/src/app/accounts/page.tsx +1307 -0
  113. package/vendor/chatgpt2api/source/web/src/app/debug/components/chat-panel.tsx +224 -0
  114. package/vendor/chatgpt2api/source/web/src/app/debug/components/editable-file-panel.tsx +444 -0
  115. package/vendor/chatgpt2api/source/web/src/app/debug/components/ppt-panel.tsx +9 -0
  116. package/vendor/chatgpt2api/source/web/src/app/debug/components/psd-panel.tsx +9 -0
  117. package/vendor/chatgpt2api/source/web/src/app/debug/components/search-panel.tsx +172 -0
  118. package/vendor/chatgpt2api/source/web/src/app/debug/components/skill-panel.tsx +169 -0
  119. package/vendor/chatgpt2api/source/web/src/app/debug/components/types.ts +39 -0
  120. package/vendor/chatgpt2api/source/web/src/app/debug/page.tsx +59 -0
  121. package/vendor/chatgpt2api/source/web/src/app/favicon.ico +0 -0
  122. package/vendor/chatgpt2api/source/web/src/app/globals.css +231 -0
  123. package/vendor/chatgpt2api/source/web/src/app/image/components/image-composer.tsx +542 -0
  124. package/vendor/chatgpt2api/source/web/src/app/image/components/image-results.tsx +554 -0
  125. package/vendor/chatgpt2api/source/web/src/app/image/components/image-sidebar.tsx +176 -0
  126. package/vendor/chatgpt2api/source/web/src/app/image/page.tsx +1799 -0
  127. package/vendor/chatgpt2api/source/web/src/app/image-manager/page.tsx +732 -0
  128. package/vendor/chatgpt2api/source/web/src/app/layout.tsx +50 -0
  129. package/vendor/chatgpt2api/source/web/src/app/login/page.tsx +101 -0
  130. package/vendor/chatgpt2api/source/web/src/app/logs/page.tsx +356 -0
  131. package/vendor/chatgpt2api/source/web/src/app/page.tsx +30 -0
  132. package/vendor/chatgpt2api/source/web/src/app/settings/components/api-docs-card.tsx +341 -0
  133. package/vendor/chatgpt2api/source/web/src/app/settings/components/backup-settings-card.tsx +447 -0
  134. package/vendor/chatgpt2api/source/web/src/app/settings/components/base-url-card.tsx +71 -0
  135. package/vendor/chatgpt2api/source/web/src/app/settings/components/config-card.tsx +491 -0
  136. package/vendor/chatgpt2api/source/web/src/app/settings/components/cpa-pool-dialog.tsx +108 -0
  137. package/vendor/chatgpt2api/source/web/src/app/settings/components/cpa-pools-card.tsx +169 -0
  138. package/vendor/chatgpt2api/source/web/src/app/settings/components/import-browser-dialog.tsx +185 -0
  139. package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-runtime-card.tsx +379 -0
  140. package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-settings-card.tsx +127 -0
  141. package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-settings.tsx +200 -0
  142. package/vendor/chatgpt2api/source/web/src/app/settings/components/settings-header.tsx +12 -0
  143. package/vendor/chatgpt2api/source/web/src/app/settings/components/sub2api-connections.tsx +886 -0
  144. package/vendor/chatgpt2api/source/web/src/app/settings/components/third-party-apps-card.tsx +81 -0
  145. package/vendor/chatgpt2api/source/web/src/app/settings/components/user-keys-card.tsx +422 -0
  146. package/vendor/chatgpt2api/source/web/src/app/settings/page.tsx +135 -0
  147. package/vendor/chatgpt2api/source/web/src/app/settings/store.ts +1069 -0
  148. package/vendor/chatgpt2api/source/web/src/components/date-range-filter.tsx +49 -0
  149. package/vendor/chatgpt2api/source/web/src/components/header-actions.tsx +24 -0
  150. package/vendor/chatgpt2api/source/web/src/components/image-lightbox.tsx +434 -0
  151. package/vendor/chatgpt2api/source/web/src/components/image-thumbnail.tsx +46 -0
  152. package/vendor/chatgpt2api/source/web/src/components/theme-script.tsx +18 -0
  153. package/vendor/chatgpt2api/source/web/src/components/theme-toggle.tsx +14 -0
  154. package/vendor/chatgpt2api/source/web/src/components/top-nav.tsx +263 -0
  155. package/vendor/chatgpt2api/source/web/src/components/ui/animated-theme-toggler.tsx +256 -0
  156. package/vendor/chatgpt2api/source/web/src/components/ui/badge.tsx +41 -0
  157. package/vendor/chatgpt2api/source/web/src/components/ui/button.tsx +59 -0
  158. package/vendor/chatgpt2api/source/web/src/components/ui/calendar.tsx +46 -0
  159. package/vendor/chatgpt2api/source/web/src/components/ui/card.tsx +61 -0
  160. package/vendor/chatgpt2api/source/web/src/components/ui/checkbox.tsx +27 -0
  161. package/vendor/chatgpt2api/source/web/src/components/ui/dia-text-reveal.tsx +267 -0
  162. package/vendor/chatgpt2api/source/web/src/components/ui/dialog.tsx +132 -0
  163. package/vendor/chatgpt2api/source/web/src/components/ui/field.tsx +13 -0
  164. package/vendor/chatgpt2api/source/web/src/components/ui/input.tsx +19 -0
  165. package/vendor/chatgpt2api/source/web/src/components/ui/label.tsx +24 -0
  166. package/vendor/chatgpt2api/source/web/src/components/ui/morphing-text.tsx +149 -0
  167. package/vendor/chatgpt2api/source/web/src/components/ui/popover.tsx +38 -0
  168. package/vendor/chatgpt2api/source/web/src/components/ui/select.tsx +175 -0
  169. package/vendor/chatgpt2api/source/web/src/components/ui/sheet.tsx +143 -0
  170. package/vendor/chatgpt2api/source/web/src/components/ui/table.tsx +29 -0
  171. package/vendor/chatgpt2api/source/web/src/components/ui/tabs.tsx +91 -0
  172. package/vendor/chatgpt2api/source/web/src/components/ui/text-3d-flip.tsx +269 -0
  173. package/vendor/chatgpt2api/source/web/src/components/ui/textarea.tsx +18 -0
  174. package/vendor/chatgpt2api/source/web/src/components/version-release-dialog.tsx +127 -0
  175. package/vendor/chatgpt2api/source/web/src/constants/common-env.ts +6 -0
  176. package/vendor/chatgpt2api/source/web/src/hooks/use-version-check.ts +85 -0
  177. package/vendor/chatgpt2api/source/web/src/lib/api.ts +926 -0
  178. package/vendor/chatgpt2api/source/web/src/lib/auth-session.ts +26 -0
  179. package/vendor/chatgpt2api/source/web/src/lib/release.ts +25 -0
  180. package/vendor/chatgpt2api/source/web/src/lib/request.ts +93 -0
  181. package/vendor/chatgpt2api/source/web/src/lib/use-auth-guard.ts +89 -0
  182. package/vendor/chatgpt2api/source/web/src/lib/utils.ts +6 -0
  183. package/vendor/chatgpt2api/source/web/src/store/auth.ts +112 -0
  184. package/vendor/chatgpt2api/source/web/src/store/editable-file-history.ts +33 -0
  185. package/vendor/chatgpt2api/source/web/src/store/image-conversations.ts +318 -0
  186. package/vendor/chatgpt2api/source/web/tsconfig.json +42 -0
@@ -0,0 +1,269 @@
1
+ "use client"
2
+
3
+ import React, {
4
+ memo,
5
+ useCallback,
6
+ useEffect,
7
+ useMemo,
8
+ useRef,
9
+ type ElementType,
10
+ } from "react"
11
+ import {
12
+ useAnimate,
13
+ type AnimationOptions,
14
+ type ValueAnimationTransition,
15
+ } from "motion/react"
16
+
17
+ import { cn } from "@/lib/utils"
18
+
19
+ const HAS_SEGMENTER = typeof Intl !== "undefined" && "Segmenter" in Intl
20
+
21
+ const splitIntoCharacters = (text: string): string[] => {
22
+ if (HAS_SEGMENTER) {
23
+ const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" })
24
+ return Array.from(segmenter.segment(text), ({ segment }) => segment)
25
+ }
26
+ return Array.from(text)
27
+ }
28
+
29
+ const extractTextFromChildren = (children: React.ReactNode): string => {
30
+ if (children == null) return ""
31
+ if (typeof children === "string") return children
32
+ if (typeof children === "number") return String(children)
33
+
34
+ if (Array.isArray(children)) {
35
+ return children.map(extractTextFromChildren).join("")
36
+ }
37
+
38
+ if (React.isValidElement(children)) {
39
+ const props = children.props as Record<string, unknown>
40
+ const childText = props.children as React.ReactNode
41
+ if (childText != null) {
42
+ return extractTextFromChildren(childText)
43
+ }
44
+ }
45
+
46
+ return ""
47
+ }
48
+
49
+ const ROTATION_MAP = {
50
+ top: "rotateX(90deg)",
51
+ right: "rotateY(90deg)",
52
+ bottom: "rotateX(-90deg)",
53
+ left: "rotateY(-90deg)",
54
+ } as const
55
+
56
+ const DEFAULT_TRANSITION: ValueAnimationTransition = {
57
+ type: "spring",
58
+ damping: 30,
59
+ stiffness: 300,
60
+ }
61
+
62
+ interface Text3DFlipProps {
63
+ children: React.ReactNode
64
+ as?: ElementType
65
+ className?: string
66
+ textClassName?: string
67
+ flipTextClassName?: string
68
+ staggerDuration?: number
69
+ staggerFrom?: "first" | "last" | "center" | number | "random"
70
+ transition?: ValueAnimationTransition | AnimationOptions
71
+ rotateDirection?: "top" | "right" | "bottom" | "left"
72
+ }
73
+
74
+ const Text3DFlip = ({
75
+ children,
76
+ as: ElementTag = "p",
77
+ className,
78
+ textClassName,
79
+ flipTextClassName,
80
+ staggerDuration = 0.05,
81
+ staggerFrom = "first",
82
+ transition = DEFAULT_TRANSITION,
83
+ rotateDirection = "right",
84
+ ...props
85
+ }: Text3DFlipProps) => {
86
+ const isAnimatingRef = useRef(false)
87
+ const isMountedRef = useRef(false)
88
+ const [scope, animate] = useAnimate()
89
+
90
+ const rotationTransform = ROTATION_MAP[rotateDirection]
91
+
92
+ useEffect(() => {
93
+ isMountedRef.current = true
94
+
95
+ return () => {
96
+ isMountedRef.current = false
97
+ isAnimatingRef.current = false
98
+ }
99
+ }, [])
100
+
101
+ const text = useMemo(() => {
102
+ try {
103
+ return extractTextFromChildren(children)
104
+ } catch {
105
+ return ""
106
+ }
107
+ }, [children])
108
+
109
+ const characters = useMemo(() => {
110
+ const words = text.split(" ")
111
+ return words.map((word, i) => ({
112
+ characters: splitIntoCharacters(word),
113
+ needsSpace: i !== words.length - 1,
114
+ }))
115
+ }, [text])
116
+
117
+ const charOffsets = useMemo(() => {
118
+ const offsets = [0]
119
+ for (const word of characters) {
120
+ offsets.push(offsets.at(-1)! + word.characters.length)
121
+ }
122
+ return offsets
123
+ }, [characters])
124
+
125
+ const getStaggerDelay = useCallback(
126
+ (index: number, totalChars: number) => {
127
+ if (staggerFrom === "first") return index * staggerDuration
128
+ if (staggerFrom === "last")
129
+ return (totalChars - 1 - index) * staggerDuration
130
+ if (staggerFrom === "center") {
131
+ const center = Math.floor(totalChars / 2)
132
+ return Math.abs(center - index) * staggerDuration
133
+ }
134
+ if (staggerFrom === "random") {
135
+ const randomIndex = Math.floor(Math.random() * totalChars)
136
+ return Math.abs(randomIndex - index) * staggerDuration
137
+ }
138
+ return Math.abs(staggerFrom - index) * staggerDuration
139
+ },
140
+ [staggerFrom, staggerDuration]
141
+ )
142
+
143
+ const handleHoverStart = useCallback(async () => {
144
+ if (isAnimatingRef.current) return
145
+ isAnimatingRef.current = true
146
+
147
+ try {
148
+ const totalChars = characters.reduce(
149
+ (sum, word) => sum + word.characters.length,
150
+ 0
151
+ )
152
+
153
+ const delays = Array.from({ length: totalChars }, (_, i) =>
154
+ getStaggerDelay(i, totalChars)
155
+ )
156
+
157
+ await animate(
158
+ ".text-3d-flip-char",
159
+ { transform: rotationTransform },
160
+ {
161
+ ...transition,
162
+ delay: (i: number) => delays[i],
163
+ }
164
+ )
165
+
166
+ if (!isMountedRef.current) return
167
+
168
+ await animate(
169
+ ".text-3d-flip-char",
170
+ { transform: "rotateX(0deg) rotateY(0deg)" },
171
+ { duration: 0 }
172
+ )
173
+ } finally {
174
+ if (isMountedRef.current) {
175
+ isAnimatingRef.current = false
176
+ }
177
+ }
178
+ }, [characters, transition, getStaggerDelay, rotationTransform, animate])
179
+
180
+ return (
181
+ <ElementTag
182
+ className={cn("relative flex flex-wrap", className)}
183
+ onMouseEnter={handleHoverStart}
184
+ ref={scope}
185
+ {...props}
186
+ >
187
+ <span className="sr-only">{text}</span>
188
+
189
+ {characters.map((wordObj, wordIndex) => (
190
+ <span key={wordIndex} className="inline-flex">
191
+ {wordObj.characters.map((char, charIndex) => (
192
+ <CharBox
193
+ key={charOffsets[wordIndex] + charIndex}
194
+ char={char}
195
+ textClassName={textClassName}
196
+ flipTextClassName={flipTextClassName}
197
+ rotateDirection={rotateDirection}
198
+ />
199
+ ))}
200
+ {wordObj.needsSpace && <span className="whitespace-pre"> </span>}
201
+ </span>
202
+ ))}
203
+ </ElementTag>
204
+ )
205
+ }
206
+
207
+ interface CharBoxProps {
208
+ char: string
209
+ textClassName?: string
210
+ flipTextClassName?: string
211
+ rotateDirection: "top" | "right" | "bottom" | "left"
212
+ }
213
+
214
+ const SECOND_FACE_TRANSFORMS = {
215
+ top: "rotateX(-90deg) translateZ(0.5lh)",
216
+ right:
217
+ "rotateY(90deg) translateX(50%) rotateY(-90deg) translateX(-50%) rotateY(-90deg) translateX(50%)",
218
+ bottom: "rotateX(90deg) translateZ(0.5lh)",
219
+ left: "rotateY(90deg) translateX(50%) rotateY(-90deg) translateX(50%) rotateY(-90deg) translateX(50%)",
220
+ } as const
221
+
222
+ const FRONT_FACE_TRANSFORMS = {
223
+ top: "translateZ(0.5lh)",
224
+ bottom: "translateZ(0.5lh)",
225
+ left: "rotateY(90deg) translateX(50%) rotateY(-90deg)",
226
+ right: "rotateY(-90deg) translateX(50%) rotateY(90deg)",
227
+ } as const
228
+
229
+ const CONTAINER_TRANSFORMS = {
230
+ top: "translateZ(-0.5lh)",
231
+ bottom: "translateZ(-0.5lh)",
232
+ left: "rotateY(90deg) translateX(50%) rotateY(-90deg)",
233
+ right: "rotateY(90deg) translateX(50%) rotateY(-90deg)",
234
+ } as const
235
+
236
+ const CharBox = memo(
237
+ ({
238
+ char,
239
+ textClassName,
240
+ flipTextClassName,
241
+ rotateDirection,
242
+ }: CharBoxProps) => (
243
+ <span
244
+ className="text-3d-flip-char inline transform-3d"
245
+ style={{ transform: CONTAINER_TRANSFORMS[rotateDirection] }}
246
+ >
247
+ <span
248
+ className={cn("relative h-[1lh] backface-hidden", textClassName)}
249
+ style={{ transform: FRONT_FACE_TRANSFORMS[rotateDirection] }}
250
+ >
251
+ {char}
252
+ </span>
253
+ <span
254
+ className={cn(
255
+ "absolute top-0 left-0 h-[1lh] backface-hidden",
256
+ flipTextClassName
257
+ )}
258
+ style={{ transform: SECOND_FACE_TRANSFORMS[rotateDirection] }}
259
+ >
260
+ {char}
261
+ </span>
262
+ </span>
263
+ )
264
+ )
265
+
266
+ CharBox.displayName = "CharBox"
267
+ Text3DFlip.displayName = "Text3DFlip"
268
+
269
+ export default Text3DFlip
@@ -0,0 +1,18 @@
1
+ import * as React from "react";
2
+
3
+ import { cn } from "@/lib/utils";
4
+
5
+ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
6
+ return (
7
+ <textarea
8
+ data-slot="textarea"
9
+ className={cn(
10
+ "border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 block min-h-32 w-full rounded-[24px] border bg-white/90 px-4 py-3 text-sm shadow-sm outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
11
+ className,
12
+ )}
13
+ {...props}
14
+ />
15
+ );
16
+ }
17
+
18
+ export { Textarea };
@@ -0,0 +1,127 @@
1
+ "use client";
2
+
3
+ import type { ReactNode } from "react";
4
+
5
+ import { Badge } from "@/components/ui/badge";
6
+ import { Button } from "@/components/ui/button";
7
+ import {
8
+ Dialog,
9
+ DialogContent,
10
+ DialogHeader,
11
+ DialogTitle,
12
+ } from "@/components/ui/dialog";
13
+ import webConfig from "@/constants/common-env";
14
+ import { useVersionCheck } from "@/hooks/use-version-check";
15
+ import { cn } from "@/lib/utils";
16
+
17
+ function typeVariant(type: string): "success" | "danger" | "info" | "violet" | "outline" {
18
+ if (type === "新增") return "success";
19
+ if (type === "修复") return "danger";
20
+ if (type === "调整") return "info";
21
+ if (type === "文档") return "violet";
22
+ return "outline";
23
+ }
24
+
25
+ export function VersionReleaseDialog({ className }: { className?: string }) {
26
+ const {
27
+ open,
28
+ setOpen,
29
+ openReleaseModal,
30
+ latestVersion,
31
+ releases,
32
+ checking,
33
+ hasNewVersion,
34
+ checkLatestRelease,
35
+ } = useVersionCheck();
36
+
37
+ return (
38
+ <>
39
+ <button
40
+ type="button"
41
+ className={cn(
42
+ "relative px-1 py-1 text-[11px] font-medium text-stone-500 transition hover:text-stone-900 dark:text-stone-300 dark:hover:text-white",
43
+ className,
44
+ )}
45
+ onClick={openReleaseModal}
46
+ title="查看版本更新"
47
+ >
48
+ v{webConfig.appVersion}
49
+ {hasNewVersion ? (
50
+ <span className="absolute -top-1 -right-1 size-2 rounded-full bg-emerald-500" />
51
+ ) : null}
52
+ </button>
53
+ <Dialog open={open} onOpenChange={setOpen}>
54
+ <DialogContent className="w-[min(94vw,680px)] rounded-2xl">
55
+ <DialogHeader>
56
+ <DialogTitle>版本更新</DialogTitle>
57
+ </DialogHeader>
58
+ <div className="grid grid-cols-2 gap-3">
59
+ <VersionCard label="当前版本" value={webConfig.appVersion} />
60
+ <VersionCard
61
+ label="最新版本"
62
+ value={latestVersion}
63
+ action={
64
+ <button
65
+ type="button"
66
+ className="text-[11px] text-stone-400 underline-offset-2 hover:text-stone-700 hover:underline dark:hover:text-stone-200"
67
+ onClick={() => void checkLatestRelease(true)}
68
+ >
69
+ {checking ? "检查中..." : "检查更新"}
70
+ </button>
71
+ }
72
+ />
73
+ </div>
74
+ <div className="max-h-[56vh] space-y-5 overflow-y-auto pr-1">
75
+ {releases.map((release) => (
76
+ <div key={release.version} className="border-l border-stone-200 pl-4 dark:border-white/10">
77
+ <div className="flex flex-wrap items-center gap-2">
78
+ <span className="text-sm font-semibold text-stone-950 dark:text-stone-100">
79
+ {release.version === "Unreleased" ? "未发布" : release.version}
80
+ </span>
81
+ <span className="text-xs text-stone-500 dark:text-stone-400">{release.date}</span>
82
+ {release.version === latestVersion ? <Badge variant="success">最新</Badge> : null}
83
+ {release.version === webConfig.appVersion ? <Badge variant="outline">当前</Badge> : null}
84
+ </div>
85
+ <div className="mt-2 space-y-1.5">
86
+ {release.items.map((item, index) => (
87
+ <div key={index} className="flex items-start gap-2 text-sm leading-6 text-stone-700 dark:text-stone-300">
88
+ <Badge variant={typeVariant(item.type)} className="mt-0.5 shrink-0">
89
+ {item.type}
90
+ </Badge>
91
+ <span className="min-w-0 flex-1">{item.content}</span>
92
+ </div>
93
+ ))}
94
+ </div>
95
+ </div>
96
+ ))}
97
+ </div>
98
+ <Button variant="outline" size="sm" asChild>
99
+ <a href="https://github.com/basketikun/chatgpt2api" target="_blank" rel="noreferrer">
100
+ 前往 GitHub 更新
101
+ </a>
102
+ </Button>
103
+ </DialogContent>
104
+ </Dialog>
105
+ </>
106
+ );
107
+ }
108
+
109
+ function VersionCard({
110
+ label,
111
+ value,
112
+ action,
113
+ }: {
114
+ label: string;
115
+ value: string;
116
+ action?: ReactNode;
117
+ }) {
118
+ return (
119
+ <div className="rounded-xl border border-stone-200 bg-white/55 p-3 dark:border-white/10 dark:bg-white/5">
120
+ <div className="flex items-center justify-between gap-2">
121
+ <div className="text-xs text-stone-500 dark:text-stone-400">{label}</div>
122
+ {action}
123
+ </div>
124
+ <div className="mt-1 text-base font-semibold text-stone-950 dark:text-stone-100">{value}</div>
125
+ </div>
126
+ );
127
+ }
@@ -0,0 +1,6 @@
1
+ const webConfig = {
2
+ apiUrl: process.env.NODE_ENV === 'development' ? 'http://127.0.0.1:8000' : '',
3
+ appVersion: process.env.NEXT_PUBLIC_APP_VERSION || '0.0.0',
4
+ }
5
+
6
+ export default webConfig
@@ -0,0 +1,85 @@
1
+ "use client";
2
+
3
+ import { useCallback, useMemo, useState } from "react";
4
+ import { toast } from "sonner";
5
+
6
+ import webConfig from "@/constants/common-env";
7
+ import { parseChangelog, type ReleaseInfo } from "@/lib/release";
8
+
9
+ const latestVersionUrl =
10
+ "https://raw.githubusercontent.com/basketikun/chatgpt2api/main/VERSION";
11
+ const latestChangelogUrl =
12
+ "https://raw.githubusercontent.com/basketikun/chatgpt2api/main/CHANGELOG.md";
13
+
14
+ function readLocalReleases(): ReleaseInfo[] {
15
+ return JSON.parse(process.env.NEXT_PUBLIC_APP_RELEASES || "[]");
16
+ }
17
+
18
+ function toVersionParts(version: string) {
19
+ const match = version.trim().match(/^v?(\d+)\.(\d+)\.(\d+)/);
20
+ return match ? match.slice(1).map(Number) : null;
21
+ }
22
+
23
+ function isNewerVersion(latestVersion: string, currentVersion: string) {
24
+ const latest = toVersionParts(latestVersion);
25
+ const current = toVersionParts(currentVersion);
26
+ if (!latest || !current) return false;
27
+ return latest.some(
28
+ (value, index) =>
29
+ value > current[index] &&
30
+ latest.slice(0, index).every((part, prevIndex) => part === current[prevIndex]),
31
+ );
32
+ }
33
+
34
+ export function useVersionCheck() {
35
+ const currentVersion = webConfig.appVersion;
36
+ const localReleases = useMemo(readLocalReleases, []);
37
+ const [latestVersion, setLatestVersion] = useState(currentVersion);
38
+ const [releases, setReleases] = useState<ReleaseInfo[]>(localReleases);
39
+ const [checking, setChecking] = useState(false);
40
+ const [open, setOpen] = useState(false);
41
+ const hasNewVersion = isNewerVersion(latestVersion, currentVersion);
42
+
43
+ const checkLatestRelease = useCallback(
44
+ async (showMessage = false) => {
45
+ setChecking(true);
46
+ try {
47
+ const [versionResponse, changelogResponse] = await Promise.all([
48
+ fetch(latestVersionUrl),
49
+ fetch(latestChangelogUrl),
50
+ ]);
51
+ if (!versionResponse.ok || !changelogResponse.ok) throw new Error();
52
+ const [version, changelog] = await Promise.all([
53
+ versionResponse.text(),
54
+ changelogResponse.text(),
55
+ ]);
56
+ setLatestVersion(version.trim() || currentVersion);
57
+ if (changelog.trim()) setReleases(parseChangelog(changelog));
58
+ if (showMessage) toast.success("已获取最新版本信息");
59
+ } catch {
60
+ setLatestVersion(currentVersion);
61
+ setReleases(localReleases);
62
+ if (showMessage) toast.error("获取最新版本信息失败");
63
+ } finally {
64
+ setChecking(false);
65
+ }
66
+ },
67
+ [currentVersion, localReleases],
68
+ );
69
+
70
+ const openReleaseModal = () => {
71
+ setOpen(true);
72
+ void checkLatestRelease();
73
+ };
74
+
75
+ return {
76
+ open,
77
+ setOpen,
78
+ openReleaseModal,
79
+ latestVersion,
80
+ releases,
81
+ checking,
82
+ hasNewVersion,
83
+ checkLatestRelease,
84
+ };
85
+ }