ImagePromptManager.jsx 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. import React, { useState, useEffect } from 'react';
  2. import { GenerationTemplate } from '@/api/entities';
  3. import { Button } from '@/components/ui/button';
  4. import { Input } from '@/components/ui/input';
  5. import { Textarea } from '@/components/ui/textarea';
  6. import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
  7. import { Label } from '@/components/ui/label';
  8. import { Alert, AlertDescription } from '@/components/ui/alert';
  9. import {
  10. HelpCircle,
  11. Save,
  12. PlusCircle,
  13. Loader2,
  14. Image
  15. } from 'lucide-react';
  16. import {
  17. Tooltip,
  18. TooltipContent,
  19. TooltipProvider,
  20. TooltipTrigger,
  21. } from "@/components/ui/tooltip";
  22. export default function ImagePromptManager() {
  23. const [templates, setTemplates] = useState([]);
  24. const [currentTemplate, setCurrentTemplate] = useState({
  25. id: null,
  26. name: '',
  27. image_prompt: '',
  28. style: 'fashion'
  29. });
  30. const [isLoading, setIsLoading] = useState(false);
  31. const [message, setMessage] = useState(null);
  32. useEffect(() => {
  33. loadTemplates();
  34. }, []);
  35. const loadTemplates = async () => {
  36. const data = await GenerationTemplate.list('-created_date');
  37. setTemplates(data);
  38. };
  39. const handleSelectTemplate = (templateId) => {
  40. const selected = templates.find(t => t.id === templateId);
  41. if (selected) {
  42. setCurrentTemplate(selected);
  43. }
  44. };
  45. const handleInputChange = (field, value) => {
  46. setCurrentTemplate(prev => ({ ...prev, [field]: value }));
  47. };
  48. const handleSaveTemplate = async () => {
  49. if (!currentTemplate.name || !currentTemplate.image_prompt) {
  50. setMessage({ type: 'error', text: '模板名称和生图提示词不能为空' });
  51. return;
  52. }
  53. setIsLoading(true);
  54. setMessage(null);
  55. try {
  56. if (currentTemplate.id) {
  57. await GenerationTemplate.update(currentTemplate.id, {
  58. name: currentTemplate.name,
  59. image_prompt: currentTemplate.image_prompt,
  60. style: currentTemplate.style
  61. });
  62. } else {
  63. await GenerationTemplate.create({
  64. name: currentTemplate.name,
  65. image_prompt: currentTemplate.image_prompt,
  66. style: currentTemplate.style,
  67. text_template: "", // 保持兼容性
  68. target_platform: "xiaohongshu"
  69. });
  70. }
  71. await loadTemplates();
  72. setMessage({ type: 'success', text: '生图模板保存成功!' });
  73. handleNewTemplate();
  74. } catch (error) {
  75. setMessage({ type: 'error', text: '保存失败,请重试' });
  76. }
  77. setIsLoading(false);
  78. };
  79. const handleNewTemplate = () => {
  80. setCurrentTemplate({ id: null, name: '', image_prompt: '', style: 'fashion' });
  81. };
  82. const styleOptions = [
  83. { value: 'fashion', label: '时尚穿搭' },
  84. { value: 'lifestyle', label: '生活方式' },
  85. { value: 'product', label: '产品展示' },
  86. { value: 'story', label: '故事场景' }
  87. ];
  88. return (
  89. <div className="space-y-6">
  90. {message && (
  91. <Alert className={message.type === 'error' ? 'border-red-200 bg-red-50 text-red-700' : 'border-green-200 bg-green-50 text-green-700'}>
  92. <AlertDescription>{message.text}</AlertDescription>
  93. </Alert>
  94. )}
  95. <div className="flex items-center justify-between">
  96. <h3 className="text-lg font-semibold text-slate-900 flex items-center gap-2">
  97. <Image className="w-5 h-5" />
  98. 生图提示词模板
  99. </h3>
  100. <div className="flex gap-2">
  101. <Button variant="outline" size="sm" onClick={handleNewTemplate} className="gap-1">
  102. <PlusCircle className="w-4 h-4" />
  103. 新建模板
  104. </Button>
  105. <Button
  106. size="sm"
  107. onClick={handleSaveTemplate}
  108. disabled={isLoading}
  109. className="gap-1 bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700"
  110. >
  111. {isLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />}
  112. 保存模板
  113. </Button>
  114. </div>
  115. </div>
  116. <div className="grid md:grid-cols-2 gap-6">
  117. <div>
  118. <Label className="flex items-center mb-2 font-medium text-slate-700">
  119. 选择模板
  120. <TooltipProvider>
  121. <Tooltip>
  122. <TooltipTrigger asChild>
  123. <HelpCircle className="w-3.5 h-3.5 ml-1.5 text-slate-400" />
  124. </TooltipTrigger>
  125. <TooltipContent><p>选择一个现有模板进行编辑,或新建一个模板</p></TooltipContent>
  126. </Tooltip>
  127. </TooltipProvider>
  128. </Label>
  129. <Select onValueChange={handleSelectTemplate} value={currentTemplate.id || ''}>
  130. <SelectTrigger>
  131. <SelectValue placeholder="选择或新建一个模板..." />
  132. </SelectTrigger>
  133. <SelectContent>
  134. {templates.map(t => (
  135. <SelectItem key={t.id} value={t.id}>{t.name}</SelectItem>
  136. ))}
  137. </SelectContent>
  138. </Select>
  139. </div>
  140. <div>
  141. <Label htmlFor="template-name" className="font-medium text-slate-700">模板名称</Label>
  142. <Input
  143. id="template-name"
  144. value={currentTemplate.name}
  145. onChange={e => handleInputChange('name', e.target.value)}
  146. placeholder="为你的模板起个名字"
  147. className="mt-2"
  148. />
  149. </div>
  150. </div>
  151. <div>
  152. <Label htmlFor="style" className="font-medium text-slate-700">风格类型</Label>
  153. <Select value={currentTemplate.style} onValueChange={value => handleInputChange('style', value)}>
  154. <SelectTrigger className="mt-2">
  155. <SelectValue placeholder="选择风格类型" />
  156. </SelectTrigger>
  157. <SelectContent>
  158. {styleOptions.map(option => (
  159. <SelectItem key={option.value} value={option.value}>{option.label}</SelectItem>
  160. ))}
  161. </SelectContent>
  162. </Select>
  163. </div>
  164. <div>
  165. <Label htmlFor="image-prompt" className="flex items-center mb-2 font-medium text-slate-700">
  166. 生图提示词
  167. <TooltipProvider>
  168. <Tooltip>
  169. <TooltipTrigger asChild>
  170. <HelpCircle className="w-3.5 h-3.5 ml-1.5 text-slate-400" />
  171. </TooltipTrigger>
  172. <TooltipContent><p>用于生成图片的详细描述,包含场景、人物、服装、风格等要素</p></TooltipContent>
  173. </Tooltip>
  174. </TooltipProvider>
  175. </Label>
  176. <Textarea
  177. id="image-prompt"
  178. value={currentTemplate.image_prompt}
  179. onChange={e => handleInputChange('image_prompt', e.target.value)}
  180. placeholder="例如:一个亚洲女孩,身穿飘逸的白色连衣裙,赤脚走在黄昏时分的沙滩上,海浪轻轻拍打着她的脚踝。背景是橘色和紫色的晚霞,光线柔和,氛围浪漫..."
  181. className="h-32"
  182. />
  183. </div>
  184. </div>
  185. );
  186. }