data-collection-step.tsx 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571
  1. "use client"
  2. import type React from "react"
  3. import { useState, useRef, useEffect, useCallback } from "react"
  4. import { Button } from "@/components/ui/button"
  5. import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
  6. import { Badge } from "@/components/ui/badge"
  7. import { Progress } from "@/components/ui/progress"
  8. import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
  9. import { Upload, ImageIcon, Trash2, Eye, Info, CheckCircle } from "lucide-react"
  10. interface ImageData {
  11. id: string
  12. name: string
  13. url: string
  14. size: number
  15. type: string
  16. }
  17. interface DataCollectionStepProps {
  18. onNext?: () => void
  19. selectedImages: string[]
  20. onImagesChange: (images: string[]) => void
  21. }
  22. const sampleImageCategories = {
  23. landscape: {
  24. name: "风景画",
  25. images: [
  26. { id: "landscape-1", name: "山水湖泊.jpg", url: "/mountain-lake-painting.png", size: 245760, type: "image/jpeg" },
  27. { id: "landscape-2", name: "日出山峰.jpg", url: "/sunrise-mountain-peaks.png", size: 234560, type: "image/jpeg" },
  28. { id: "landscape-3", name: "森林小径.jpg", url: "/forest-path-painting.png", size: 267890, type: "image/jpeg" },
  29. { id: "landscape-4", name: "海边日落.jpg", url: "/sunset-beach-painting.png", size: 298765, type: "image/jpeg" },
  30. {
  31. id: "landscape-5",
  32. name: "雪山风光.jpg",
  33. url: "/snowy-mountain-landscape.png",
  34. size: 312450,
  35. type: "image/jpeg",
  36. },
  37. { id: "landscape-6", name: "田园风光.jpg", url: "/pastoral-landscape.png", size: 287650, type: "image/jpeg" },
  38. { id: "landscape-7", name: "瀑布奇观.jpg", url: "/waterfall-landscape.png", size: 345670, type: "image/jpeg" },
  39. { id: "landscape-8", name: "沙漠绿洲.jpg", url: "/desert-oasis-painting.png", size: 276540, type: "image/jpeg" },
  40. {
  41. id: "landscape-9",
  42. name: "湖光山色.jpg",
  43. url: "/lake-mountain-reflection-painting.png",
  44. size: 298760,
  45. type: "image/jpeg",
  46. },
  47. {
  48. id: "landscape-10",
  49. name: "秋叶满山.jpg",
  50. url: "/autumn-mountain-foliage-painting.png",
  51. size: 321450,
  52. type: "image/jpeg",
  53. },
  54. ],
  55. },
  56. portrait: {
  57. name: "人物肖像",
  58. images: [
  59. { id: "portrait-1", name: "经典肖像.jpg", url: "/artistic-portrait.png", size: 312320, type: "image/jpeg" },
  60. { id: "portrait-2", name: "女性肖像.jpg", url: "/elegant-woman-portrait.png", size: 298450, type: "image/jpeg" },
  61. { id: "portrait-3", name: "老人肖像.jpg", url: "/elderly-man-portrait.png", size: 287650, type: "image/jpeg" },
  62. { id: "portrait-4", name: "儿童肖像.jpg", url: "/child-portrait-painting.png", size: 276540, type: "image/jpeg" },
  63. {
  64. id: "portrait-5",
  65. name: "艺术家自画像.jpg",
  66. url: "/artist-self-portrait.png",
  67. size: 345670,
  68. type: "image/jpeg",
  69. },
  70. {
  71. id: "portrait-6",
  72. name: "古典美人.jpg",
  73. url: "/classical-beauty-portrait.png",
  74. size: 321450,
  75. type: "image/jpeg",
  76. },
  77. {
  78. id: "portrait-7",
  79. name: "现代肖像.jpg",
  80. url: "/modern-portrait-painting.png",
  81. size: 298760,
  82. type: "image/jpeg",
  83. },
  84. {
  85. id: "portrait-8",
  86. name: "侧面肖像.jpg",
  87. url: "/profile-portrait-painting.png",
  88. size: 287650,
  89. type: "image/jpeg",
  90. },
  91. {
  92. id: "portrait-9",
  93. name: "双人肖像.jpg",
  94. url: "/couple-portrait-painting.png",
  95. size: 356780,
  96. type: "image/jpeg",
  97. },
  98. {
  99. id: "portrait-10",
  100. name: "表情肖像.jpg",
  101. url: "/expressive-portrait-painting.png",
  102. size: 312450,
  103. type: "image/jpeg",
  104. },
  105. ],
  106. },
  107. abstract: {
  108. name: "抽象艺术",
  109. images: [
  110. {
  111. id: "abstract-1",
  112. name: "几何抽象.jpg",
  113. url: "/colorful-geometric-abstract.png",
  114. size: 189440,
  115. type: "image/jpeg",
  116. },
  117. {
  118. id: "abstract-2",
  119. name: "色彩流动.jpg",
  120. url: "/flowing-colors-abstract.png",
  121. size: 234560,
  122. type: "image/jpeg",
  123. },
  124. {
  125. id: "abstract-3",
  126. name: "线条构成.jpg",
  127. url: "/linear-composition-abstract.png",
  128. size: 267890,
  129. type: "image/jpeg",
  130. },
  131. {
  132. id: "abstract-4",
  133. name: "色块拼接.jpg",
  134. url: "/color-blocks-abstract.png",
  135. size: 298765,
  136. type: "image/jpeg",
  137. },
  138. {
  139. id: "abstract-5",
  140. name: "动感抽象.jpg",
  141. url: "/dynamic-abstract-painting.png",
  142. size: 312450,
  143. type: "image/jpeg",
  144. },
  145. {
  146. id: "abstract-6",
  147. name: "纹理抽象.jpg",
  148. url: "/textured-abstract-art.png",
  149. size: 287650,
  150. type: "image/jpeg",
  151. },
  152. {
  153. id: "abstract-7",
  154. name: "光影抽象.jpg",
  155. url: "/light-shadow-abstract.png",
  156. size: 345670,
  157. type: "image/jpeg",
  158. },
  159. {
  160. id: "abstract-8",
  161. name: "螺旋构图.jpg",
  162. url: "/spiral-composition-abstract.png",
  163. size: 276540,
  164. type: "image/jpeg",
  165. },
  166. {
  167. id: "abstract-9",
  168. name: "对比抽象.jpg",
  169. url: "/contrast-abstract-painting.png",
  170. size: 298760,
  171. type: "image/jpeg",
  172. },
  173. {
  174. id: "abstract-10",
  175. name: "渐变抽象.jpg",
  176. url: "/gradient-abstract-art.png",
  177. size: 321450,
  178. type: "image/jpeg",
  179. },
  180. ],
  181. },
  182. stilllife: {
  183. name: "静物画",
  184. images: [
  185. { id: "stilllife-1", name: "花卉静物.jpg", url: "/floral-still-life.png", size: 278528, type: "image/jpeg" },
  186. {
  187. id: "stilllife-2",
  188. name: "水果静物.jpg",
  189. url: "/fruit-still-life.png",
  190. size: 234560,
  191. type: "image/jpeg",
  192. },
  193. {
  194. id: "stilllife-3",
  195. name: "花瓶静物.jpg",
  196. url: "/vase-still-life.png",
  197. size: 267890,
  198. type: "image/jpeg",
  199. },
  200. {
  201. id: "stilllife-4",
  202. name: "书籍静物.jpg",
  203. url: "/books-still-life.png",
  204. size: 298765,
  205. type: "image/jpeg",
  206. },
  207. {
  208. id: "stilllife-5",
  209. name: "茶具静物.jpg",
  210. url: "/tea-set-still-life.png",
  211. size: 312450,
  212. type: "image/jpeg",
  213. },
  214. {
  215. id: "stilllife-6",
  216. name: "蜡烛静物.jpg",
  217. url: "/candle-still-life.png",
  218. size: 287650,
  219. type: "image/jpeg",
  220. },
  221. {
  222. id: "stilllife-7",
  223. name: "乐器静物.jpg",
  224. url: "/musical-instrument-still-life.png",
  225. size: 345670,
  226. type: "image/jpeg",
  227. },
  228. {
  229. id: "stilllife-8",
  230. name: "古董静物.jpg",
  231. url: "/antique-still-life.png",
  232. size: 276540,
  233. type: "image/jpeg",
  234. },
  235. {
  236. id: "stilllife-9",
  237. name: "厨具静物.jpg",
  238. url: "/kitchen-utensils-still-life.png",
  239. size: 298760,
  240. type: "image/jpeg",
  241. },
  242. {
  243. id: "stilllife-10",
  244. name: "珠宝静物.jpg",
  245. url: "/jewelry-still-life.png",
  246. size: 321450,
  247. type: "image/jpeg",
  248. },
  249. ],
  250. },
  251. }
  252. export default function DataCollectionStep({
  253. onNext,
  254. selectedImages: selectedImageUrls,
  255. onImagesChange,
  256. }: DataCollectionStepProps) {
  257. const [selectedImages, setSelectedImages] = useState<ImageData[]>([])
  258. const [uploadedImages, setUploadedImages] = useState<ImageData[]>([])
  259. const [selectedCategory, setSelectedCategory] = useState<string>("landscape")
  260. const fileInputRef = useRef<HTMLInputElement>(null)
  261. useEffect(() => {
  262. if (selectedImageUrls.length > 0 && selectedImages.length === 0) {
  263. const matchingImages: ImageData[] = []
  264. Object.values(sampleImageCategories).forEach((category) => {
  265. category.images.forEach((image) => {
  266. if (selectedImageUrls.includes(image.url)) {
  267. matchingImages.push(image)
  268. }
  269. })
  270. })
  271. setSelectedImages(matchingImages)
  272. }
  273. }, []) // Empty dependency array to run only on mount
  274. const handleImagesChange = useCallback(() => {
  275. const allImageUrls = [...selectedImages, ...uploadedImages].map((img) => img.url)
  276. onImagesChange(allImageUrls)
  277. }, [selectedImages, uploadedImages, onImagesChange])
  278. useEffect(() => {
  279. handleImagesChange()
  280. }, [handleImagesChange])
  281. const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
  282. const files = event.target.files
  283. if (!files) return
  284. Array.from(files).forEach((file) => {
  285. if (file.type.startsWith("image/")) {
  286. const reader = new FileReader()
  287. reader.onload = (e) => {
  288. const newImage: ImageData = {
  289. id: `upload-${Date.now()}-${Math.random()}`,
  290. name: file.name,
  291. url: e.target?.result as string,
  292. size: file.size,
  293. type: file.type,
  294. }
  295. setUploadedImages((prev) => [...prev, newImage])
  296. }
  297. reader.readAsDataURL(file)
  298. }
  299. })
  300. }
  301. const handleSampleSelect = (image: ImageData) => {
  302. if (!selectedImages.find((img) => img.id === image.id)) {
  303. setSelectedImages((prev) => [...prev, image])
  304. }
  305. }
  306. const handleSelectCategory = (categoryKey: string) => {
  307. const category = sampleImageCategories[categoryKey as keyof typeof sampleImageCategories]
  308. const newImages = category.images.filter((img) => !selectedImages.find((selected) => selected.id === img.id))
  309. setSelectedImages((prev) => [...prev, ...newImages])
  310. }
  311. const handleRemoveImage = (imageId: string) => {
  312. setSelectedImages((prev) => prev.filter((img) => img.id !== imageId))
  313. setUploadedImages((prev) => prev.filter((img) => img.id !== imageId))
  314. }
  315. const formatFileSize = (bytes: number) => {
  316. if (bytes === 0) return "0 Bytes"
  317. const k = 1024
  318. const sizes = ["Bytes", "KB", "MB", "GB"]
  319. const i = Math.floor(Math.log(bytes) / Math.log(k))
  320. return Number.parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]
  321. }
  322. const allImages = [...selectedImages, ...uploadedImages]
  323. const totalSize = allImages.reduce((sum, img) => sum + img.size, 0)
  324. const handleNext = () => {
  325. if (onNext && allImages.length > 0) {
  326. onNext()
  327. }
  328. }
  329. return (
  330. <div className="space-y-6">
  331. {/* Introduction */}
  332. <Card className="bg-primary/5 border-primary/20">
  333. <CardHeader>
  334. <div className="flex items-center gap-2">
  335. <Info className="w-5 h-5 text-primary" />
  336. <CardTitle className="text-lg font-serif">什么是数据采集?</CardTitle>
  337. </div>
  338. </CardHeader>
  339. <CardContent>
  340. <p className="text-muted-foreground leading-relaxed">
  341. 数据采集是AI绘画的第一步,也是最关键的步骤。就像人类艺术家需要观察大量的艺术作品来学习绘画技巧一样,
  342. AI模型也需要"看到"大量的图像数据来学习如何创作。数据的质量和多样性直接影响AI生成图像的效果。
  343. </p>
  344. </CardContent>
  345. </Card>
  346. {/* Data Collection Interface */}
  347. <Tabs defaultValue="samples" className="w-full">
  348. <TabsList className="grid w-full grid-cols-2">
  349. <TabsTrigger value="samples">示例数据集</TabsTrigger>
  350. <TabsTrigger value="upload">上传图像</TabsTrigger>
  351. </TabsList>
  352. <TabsContent value="samples" className="space-y-4">
  353. <Card>
  354. <CardHeader>
  355. <CardTitle className="font-serif">选择示例图像</CardTitle>
  356. <CardDescription>
  357. 从我们准备的示例数据集中选择图像作为AI的学习材料,每个类别包含10张精选图像
  358. </CardDescription>
  359. </CardHeader>
  360. <CardContent>
  361. <Tabs value={selectedCategory} onValueChange={setSelectedCategory} className="w-full">
  362. <TabsList className="grid w-full grid-cols-4">
  363. <TabsTrigger value="landscape">风景画</TabsTrigger>
  364. <TabsTrigger value="portrait">人物肖像</TabsTrigger>
  365. <TabsTrigger value="abstract">抽象艺术</TabsTrigger>
  366. <TabsTrigger value="stilllife">静物画</TabsTrigger>
  367. </TabsList>
  368. {Object.entries(sampleImageCategories).map(([categoryKey, category]) => (
  369. <TabsContent key={categoryKey} value={categoryKey} className="space-y-4">
  370. <div className="flex justify-between items-center">
  371. <h3 className="text-lg font-semibold">{category.name} (10张图像)</h3>
  372. <Button
  373. variant="outline"
  374. onClick={() => handleSelectCategory(categoryKey)}
  375. disabled={category.images.every((img) =>
  376. selectedImages.find((selected) => selected.id === img.id),
  377. )}
  378. >
  379. 选择全部
  380. </Button>
  381. </div>
  382. <div className="grid grid-cols-2 md:grid-cols-5 gap-4">
  383. {category.images.map((image) => {
  384. const isSelected = selectedImages.find((img) => img.id === image.id)
  385. return (
  386. <div key={image.id} className="relative group">
  387. <div
  388. className={`border-2 rounded-lg overflow-hidden transition-all ${
  389. isSelected ? "border-primary shadow-lg" : "border-border hover:border-primary/50"
  390. }`}
  391. >
  392. <img
  393. src={image.url || "/placeholder.svg"}
  394. alt={image.name}
  395. className="w-full h-24 object-cover"
  396. />
  397. <div className="p-2">
  398. <p className="text-xs font-medium truncate">{image.name}</p>
  399. <p className="text-xs text-muted-foreground">{formatFileSize(image.size)}</p>
  400. </div>
  401. </div>
  402. {isSelected ? (
  403. <div className="absolute top-1 right-1">
  404. <CheckCircle className="w-4 h-4 text-primary bg-white rounded-full" />
  405. </div>
  406. ) : (
  407. <Button
  408. size="sm"
  409. onClick={() => handleSampleSelect(image)}
  410. className="absolute top-1 right-1 opacity-0 group-hover:opacity-100 transition-opacity text-xs px-2 py-1 h-auto"
  411. >
  412. 选择
  413. </Button>
  414. )}
  415. </div>
  416. )
  417. })}
  418. </div>
  419. </TabsContent>
  420. ))}
  421. </Tabs>
  422. </CardContent>
  423. </Card>
  424. </TabsContent>
  425. <TabsContent value="upload" className="space-y-4">
  426. <Card>
  427. <CardHeader>
  428. <CardTitle className="font-serif">上传自定义图像</CardTitle>
  429. <CardDescription>上传你自己的图像来训练AI模型(支持 JPG, PNG, GIF 格式)</CardDescription>
  430. </CardHeader>
  431. <CardContent>
  432. <div
  433. className="border-2 border-dashed border-border rounded-lg p-8 text-center hover:border-primary/50 transition-colors cursor-pointer"
  434. onClick={() => fileInputRef.current?.click()}
  435. >
  436. <Upload className="w-12 h-12 text-muted-foreground mx-auto mb-4" />
  437. <h3 className="text-lg font-semibold mb-2">点击上传图像</h3>
  438. <p className="text-muted-foreground mb-4">或将图像文件拖拽到此区域</p>
  439. <Button variant="outline">
  440. <ImageIcon className="w-4 h-4 mr-2" />
  441. 选择文件
  442. </Button>
  443. <input
  444. ref={fileInputRef}
  445. type="file"
  446. multiple
  447. accept="image/*"
  448. onChange={handleFileUpload}
  449. className="hidden"
  450. />
  451. </div>
  452. {uploadedImages.length > 0 && (
  453. <div className="mt-6">
  454. <h4 className="font-semibold mb-3">已上传的图像</h4>
  455. <div className="grid grid-cols-2 md:grid-cols-3 gap-4">
  456. {uploadedImages.map((image) => (
  457. <div key={image.id} className="relative group border rounded-lg overflow-hidden">
  458. <img
  459. src={image.url || "/placeholder.svg"}
  460. alt={image.name}
  461. className="w-full h-32 object-cover"
  462. />
  463. <div className="p-3">
  464. <p className="text-sm font-medium truncate">{image.name}</p>
  465. <p className="text-xs text-muted-foreground">{formatFileSize(image.size)}</p>
  466. </div>
  467. <Button
  468. size="sm"
  469. variant="destructive"
  470. onClick={() => handleRemoveImage(image.id)}
  471. className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity"
  472. >
  473. <Trash2 className="w-4 h-4" />
  474. </Button>
  475. </div>
  476. ))}
  477. </div>
  478. </div>
  479. )}
  480. </CardContent>
  481. </Card>
  482. </TabsContent>
  483. </Tabs>
  484. {/* Dataset Statistics */}
  485. <Card>
  486. <CardHeader>
  487. <CardTitle className="font-serif">数据集统计</CardTitle>
  488. <CardDescription>当前选择的训练数据概览</CardDescription>
  489. </CardHeader>
  490. <CardContent>
  491. <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
  492. <div className="text-center p-4 bg-muted rounded-lg">
  493. <div className="text-2xl font-bold text-primary">{allImages.length}</div>
  494. <div className="text-sm text-muted-foreground">图像总数</div>
  495. </div>
  496. <div className="text-center p-4 bg-muted rounded-lg">
  497. <div className="text-2xl font-bold text-primary">{formatFileSize(totalSize)}</div>
  498. <div className="text-sm text-muted-foreground">总大小</div>
  499. </div>
  500. <div className="text-center p-4 bg-muted rounded-lg">
  501. <div className="text-2xl font-bold text-primary">{selectedImages.length}</div>
  502. <div className="text-sm text-muted-foreground">示例图像</div>
  503. </div>
  504. <div className="text-center p-4 bg-muted rounded-lg">
  505. <div className="text-2xl font-bold text-primary">{uploadedImages.length}</div>
  506. <div className="text-sm text-muted-foreground">上传图像</div>
  507. </div>
  508. </div>
  509. <div className="space-y-3">
  510. <div className="flex justify-between text-sm">
  511. <span>数据集完整度</span>
  512. <span>{Math.min(100, (allImages.length / 10) * 100).toFixed(0)}%</span>
  513. </div>
  514. <Progress value={Math.min(100, (allImages.length / 10) * 100)} className="h-2" />
  515. <p className="text-xs text-muted-foreground">建议至少选择 10 张图像以获得更好的训练效果</p>
  516. </div>
  517. {allImages.length > 0 && (
  518. <div className="mt-6">
  519. <h4 className="font-semibold mb-3">已选择的图像</h4>
  520. <div className="flex flex-wrap gap-2">
  521. {allImages.map((image) => (
  522. <Badge key={image.id} variant="secondary" className="flex items-center gap-1">
  523. {image.name}
  524. <button onClick={() => handleRemoveImage(image.id)} className="ml-1 hover:text-destructive">
  525. <Trash2 className="w-3 h-3" />
  526. </button>
  527. </Badge>
  528. ))}
  529. </div>
  530. </div>
  531. )}
  532. </CardContent>
  533. </Card>
  534. {/* Next Step Button */}
  535. <div className="flex justify-end">
  536. <Button size="lg" disabled={allImages.length === 0} className="font-semibold" onClick={handleNext}>
  537. 继续到数据预处理
  538. <Eye className="w-4 h-4 ml-2" />
  539. </Button>
  540. </div>
  541. </div>
  542. )
  543. }