新增常用提示词功能

This commit is contained in:
yuantao
2025-09-19 17:25:46 +08:00
parent 70684b2ddf
commit eae15ced5a
3 changed files with 148 additions and 6 deletions

View File

@@ -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)}

View 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>
);
};

View File

@@ -60,7 +60,12 @@ function getImageHash(imageData: string): string {
if (imageData.startsWith('blob:')) {
// 对于Blob URL我们使用URL本身作为标识符的一部分
// 这不是完美的解决方案,但对于大多数情况足够了
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 }
}
}