You've already forked Nano-Banana-AI-Image-Editor
新增常用提示词功能
This commit is contained in:
@@ -6,6 +6,7 @@ import { useImageGeneration, useImageEditing } from '../hooks/useImageGeneration
|
|||||||
import { Upload, Wand2, Edit3, MousePointer, HelpCircle, ChevronDown, ChevronRight, RotateCcw } from 'lucide-react';
|
import { Upload, Wand2, Edit3, MousePointer, HelpCircle, ChevronDown, ChevronRight, RotateCcw } from 'lucide-react';
|
||||||
import { blobToBase64, urlToBlob } from '../utils/imageUtils';
|
import { blobToBase64, urlToBlob } from '../utils/imageUtils';
|
||||||
import { PromptHints } from './PromptHints';
|
import { PromptHints } from './PromptHints';
|
||||||
|
import { PromptSuggestions } from './PromptSuggestions';
|
||||||
import { cn } from '../utils/cn';
|
import { cn } from '../utils/cn';
|
||||||
|
|
||||||
export const PromptComposer: React.FC = () => {
|
export const PromptComposer: React.FC = () => {
|
||||||
@@ -341,6 +342,14 @@ export const PromptComposer: React.FC = () => {
|
|||||||
className="min-h-[120px] resize-none text-sm rounded-xl"
|
className="min-h-[120px] resize-none text-sm rounded-xl"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* 常用提示词 */}
|
||||||
|
<PromptSuggestions
|
||||||
|
onWordSelect={(word) => {
|
||||||
|
setCurrentPrompt(currentPrompt ? `${currentPrompt}, ${word}` : word);
|
||||||
|
}}
|
||||||
|
minFrequency={3}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* 提示质量指示器 */}
|
{/* 提示质量指示器 */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowHintsModal(true)}
|
onClick={() => setShowHintsModal(true)}
|
||||||
|
|||||||
120
src/components/PromptSuggestions.tsx
Normal file
120
src/components/PromptSuggestions.tsx
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Button } from './ui/Button';
|
||||||
|
import { useAppStore } from '../store/useAppStore';
|
||||||
|
import { cn } from '../utils/cn';
|
||||||
|
|
||||||
|
interface WordFrequency {
|
||||||
|
word: string;
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PromptSuggestions: React.FC<{
|
||||||
|
onWordSelect?: (word: string) => void;
|
||||||
|
minFrequency?: number;
|
||||||
|
}> = ({ onWordSelect, minFrequency = 3 }) => {
|
||||||
|
const { currentProject } = useAppStore();
|
||||||
|
const [frequentWords, setFrequentWords] = useState<WordFrequency[]>([]);
|
||||||
|
const [showAll, setShowAll] = useState(false);
|
||||||
|
|
||||||
|
// 从提示词中提取词语并统计频次
|
||||||
|
const extractWords = (text: string): string[] => {
|
||||||
|
// 移除标点符号并分割词语
|
||||||
|
return text
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^\w\s\u4e00-\u9fff]/g, ' ') // 保留中文字符
|
||||||
|
.split(/\s+/)
|
||||||
|
.filter(word => word.length > 1); // 过滤掉单字符
|
||||||
|
};
|
||||||
|
|
||||||
|
// 统计词语频次
|
||||||
|
const calculateWordFrequency = (): WordFrequency[] => {
|
||||||
|
const wordCount: Record<string, number> = {};
|
||||||
|
|
||||||
|
// 收集所有提示词
|
||||||
|
const allPrompts: string[] = [];
|
||||||
|
|
||||||
|
// 添加生成记录的提示词
|
||||||
|
if (currentProject?.generations) {
|
||||||
|
currentProject.generations.forEach(gen => {
|
||||||
|
if (gen.prompt) {
|
||||||
|
allPrompts.push(gen.prompt);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加编辑记录的指令
|
||||||
|
if (currentProject?.edits) {
|
||||||
|
currentProject.edits.forEach(edit => {
|
||||||
|
if (edit.instruction) {
|
||||||
|
allPrompts.push(edit.instruction);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取词语并统计频次
|
||||||
|
allPrompts.forEach(prompt => {
|
||||||
|
const words = extractWords(prompt);
|
||||||
|
words.forEach(word => {
|
||||||
|
wordCount[word] = (wordCount[word] || 0) + 1;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 转换为数组并过滤
|
||||||
|
return Object.entries(wordCount)
|
||||||
|
.map(([word, count]) => ({ word, count }))
|
||||||
|
.filter(({ count }) => count >= minFrequency)
|
||||||
|
.sort((a, b) => b.count - a.count);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFrequentWords(calculateWordFrequency());
|
||||||
|
}, [currentProject, minFrequency]);
|
||||||
|
|
||||||
|
// 显示的词语数量
|
||||||
|
const displayWords = showAll ? frequentWords : frequentWords.slice(0, 20);
|
||||||
|
|
||||||
|
if (frequentWords.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4 bg-gray-50 rounded-lg">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<h3 className="text-sm font-medium text-gray-700">常用提示词</h3>
|
||||||
|
{frequentWords.length > 20 && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="text-xs text-gray-500 hover:text-gray-700"
|
||||||
|
onClick={() => setShowAll(!showAll)}
|
||||||
|
>
|
||||||
|
{showAll ? '收起' : '展开'}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{displayWords.map(({ word, count }) => (
|
||||||
|
<button
|
||||||
|
key={word}
|
||||||
|
onClick={() => onWordSelect?.(word)}
|
||||||
|
className={cn(
|
||||||
|
"px-2 py-1 text-xs rounded-full border transition-all",
|
||||||
|
"bg-white hover:bg-yellow-50 border-gray-200 hover:border-yellow-300",
|
||||||
|
"text-gray-700 hover:text-gray-900"
|
||||||
|
)}
|
||||||
|
title={`出现频次: ${count}`}
|
||||||
|
>
|
||||||
|
{word}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{frequentWords.length > 20 && (
|
||||||
|
<div className="mt-2 text-xs text-gray-500 text-center">
|
||||||
|
共 {frequentWords.length} 个常用提示词
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -60,7 +60,12 @@ function getImageHash(imageData: string): string {
|
|||||||
if (imageData.startsWith('blob:')) {
|
if (imageData.startsWith('blob:')) {
|
||||||
// 对于Blob URL,我们使用URL本身作为标识符的一部分
|
// 对于Blob URL,我们使用URL本身作为标识符的一部分
|
||||||
// 这不是完美的解决方案,但对于大多数情况足够了
|
// 这不是完美的解决方案,但对于大多数情况足够了
|
||||||
return btoa(imageData).slice(0, 32)
|
try {
|
||||||
|
return btoa(imageData).slice(0, 32)
|
||||||
|
} catch (e) {
|
||||||
|
// 如果btoa失败(例如包含非Latin1字符),使用encodeURIComponent
|
||||||
|
return btoa(encodeURIComponent(imageData)).slice(0, 32)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 对于base64数据,使用简单的哈希函数生成图像标识符
|
// 对于base64数据,使用简单的哈希函数生成图像标识符
|
||||||
@@ -132,7 +137,12 @@ export const uploadImage = async (imageData: string | Blob, accessToken: string,
|
|||||||
// 检查缓存是否过期
|
// 检查缓存是否过期
|
||||||
if (Date.now() - cachedResult.timestamp < CACHE_EXPIRY_TIME) {
|
if (Date.now() - cachedResult.timestamp < CACHE_EXPIRY_TIME) {
|
||||||
console.log('从缓存中获取上传结果')
|
console.log('从缓存中获取上传结果')
|
||||||
return cachedResult
|
// 确保返回的数据结构与新上传的结果一致
|
||||||
|
return {
|
||||||
|
success: cachedResult.success,
|
||||||
|
url: cachedResult.url,
|
||||||
|
error: cachedResult.error
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// 缓存过期,删除它
|
// 缓存过期,删除它
|
||||||
uploadCache.delete(imageHash)
|
uploadCache.delete(imageHash)
|
||||||
@@ -150,7 +160,9 @@ export const uploadImage = async (imageData: string | Blob, accessToken: string,
|
|||||||
// 从base64数据创建Blob
|
// 从base64数据创建Blob
|
||||||
const base64Data = imageData.split('base64,')[1]
|
const base64Data = imageData.split('base64,')[1]
|
||||||
const byteString = atob(base64Data)
|
const byteString = atob(base64Data)
|
||||||
const mimeString = 'image/png' // 默认MIME类型
|
// 从base64数据中提取MIME类型
|
||||||
|
const mimeMatch = imageData.match(/^data:([a-zA-Z0-9]+\/[a-zA-Z0-9-.+]+).*,.*/)
|
||||||
|
const mimeString = mimeMatch ? mimeMatch[1] : 'image/png' // 默认MIME类型
|
||||||
const ab = new ArrayBuffer(byteString.length)
|
const ab = new ArrayBuffer(byteString.length)
|
||||||
const ia = new Uint8Array(ab)
|
const ia = new Uint8Array(ab)
|
||||||
for (let i = 0; i < byteString.length; i++) {
|
for (let i = 0; i < byteString.length; i++) {
|
||||||
@@ -214,13 +226,14 @@ export const uploadImage = async (imageData: string | Blob, accessToken: string,
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return uploadResult
|
return { success: true, url: fullUrl, error: undefined }
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`上传失败: ${result.msg}`)
|
throw new Error(`上传失败: ${result.msg}`)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('上传图像时出错:', error)
|
console.error('上传图像时出错:', error)
|
||||||
const errorResult = { success: false, error: error instanceof Error ? error.message : String(error) }
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||||
|
const errorResult = { success: false, error: errorMessage }
|
||||||
|
|
||||||
// 清理过期缓存
|
// 清理过期缓存
|
||||||
cleanupExpiredCache()
|
cleanupExpiredCache()
|
||||||
@@ -236,7 +249,7 @@ export const uploadImage = async (imageData: string | Blob, accessToken: string,
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return errorResult
|
return { success: false, error: errorMessage }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user