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 { blobToBase64, urlToBlob } from '../utils/imageUtils';
|
||||
import { PromptHints } from './PromptHints';
|
||||
import { PromptSuggestions } from './PromptSuggestions';
|
||||
import { cn } from '../utils/cn';
|
||||
|
||||
export const PromptComposer: React.FC = () => {
|
||||
@@ -341,6 +342,14 @@ export const PromptComposer: React.FC = () => {
|
||||
className="min-h-[120px] resize-none text-sm rounded-xl"
|
||||
/>
|
||||
|
||||
{/* 常用提示词 */}
|
||||
<PromptSuggestions
|
||||
onWordSelect={(word) => {
|
||||
setCurrentPrompt(currentPrompt ? `${currentPrompt}, ${word}` : word);
|
||||
}}
|
||||
minFrequency={3}
|
||||
/>
|
||||
|
||||
{/* 提示质量指示器 */}
|
||||
<button
|
||||
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:')) {
|
||||
// 对于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数据,使用简单的哈希函数生成图像标识符
|
||||
@@ -132,7 +137,12 @@ export const uploadImage = async (imageData: string | Blob, accessToken: string,
|
||||
// 检查缓存是否过期
|
||||
if (Date.now() - cachedResult.timestamp < CACHE_EXPIRY_TIME) {
|
||||
console.log('从缓存中获取上传结果')
|
||||
return cachedResult
|
||||
// 确保返回的数据结构与新上传的结果一致
|
||||
return {
|
||||
success: cachedResult.success,
|
||||
url: cachedResult.url,
|
||||
error: cachedResult.error
|
||||
}
|
||||
} else {
|
||||
// 缓存过期,删除它
|
||||
uploadCache.delete(imageHash)
|
||||
@@ -150,7 +160,9 @@ export const uploadImage = async (imageData: string | Blob, accessToken: string,
|
||||
// 从base64数据创建Blob
|
||||
const base64Data = imageData.split('base64,')[1]
|
||||
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 ia = new Uint8Array(ab)
|
||||
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 {
|
||||
throw new Error(`上传失败: ${result.msg}`)
|
||||
}
|
||||
} catch (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()
|
||||
@@ -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