Compare commits

..

No commits in common. "bd09e67988a7db22a329c875733204e3c3ea4d36" and "3265b6614951d77858574a192c5cedf56e07dc14" have entirely different histories.

10 changed files with 26 additions and 991 deletions

View File

@ -479,22 +479,20 @@ export default function ChatPage({ params }: PageProps) {
)}
</div>
<div className="flex items-center gap-2">
{/* 思考模式开关 - 只在非 Codex 模型时显示 */}
{!selectedModelId.toLowerCase().includes('codex') && (
<button
onClick={handleThinkingToggle}
className={cn(
'flex items-center gap-2 px-3 py-1.5 text-sm rounded-lg transition-colors',
enableThinking
? 'bg-[var(--color-primary-light)] text-[var(--color-primary)]'
: 'text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-hover)]'
)}
title={enableThinking ? '关闭思考模式' : '开启思考模式'}
>
<Clock size={16} />
<span></span>
</button>
)}
{/* 思考模式开关 */}
<button
onClick={handleThinkingToggle}
className={cn(
'flex items-center gap-2 px-3 py-1.5 text-sm rounded-lg transition-colors',
enableThinking
? 'bg-[var(--color-primary-light)] text-[var(--color-primary)]'
: 'text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-hover)]'
)}
title={enableThinking ? '关闭思考模式' : '开启思考模式'}
>
<Clock size={16} />
<span></span>
</button>
<button
className="flex items-center gap-2 px-3 py-1.5 text-sm text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-hover)] rounded-lg transition-colors"

View File

@ -5,14 +5,11 @@ import { Paperclip, ArrowUp, Upload } from 'lucide-react';
import { ModelSelector } from './ModelSelector';
import { ToolsDropdown } from './ToolsDropdown';
import { FilePreviewList } from './FilePreviewList';
import { QuickPhrasesTrigger, QuickPhrasesPopover } from './QuickPhrasesPopover';
import { QuickPhrasesModal } from './QuickPhrasesModal';
import { Tooltip } from '@/components/ui/Tooltip';
import { useFileUpload } from '@/hooks/useFileUpload';
import { usePromptOptimizer } from '@/providers/PromptOptimizerProvider';
import { useQuickPhrases } from '@/hooks/useQuickPhrases';
import { cn } from '@/lib/utils';
import type { Model, Tool, QuickPhrase } from '@/types';
import type { Model, Tool } from '@/types';
import type { UploadFile } from '@/types/file-upload';
interface ChatInputProps {
@ -39,22 +36,11 @@ export function ChatInput({
className,
}: ChatInputProps) {
const [message, setMessage] = useState('');
const [isManageModalOpen, setIsManageModalOpen] = useState(false);
const [isPhrasesOpen, setIsPhrasesOpen] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const phrasesContainerRef = useRef<HTMLDivElement>(null);
// 使用提示词优化 Hook
const { consumeOptimizedPrompt } = usePromptOptimizer();
// 使用快捷短语 Hook
const {
phrases,
addPhrase,
updatePhrase,
deletePhrase,
} = useQuickPhrases();
// 监听优化后的提示词并填入输入框
useEffect(() => {
const prompt = consumeOptimizedPrompt();
@ -63,39 +49,6 @@ export function ChatInput({
}
}, [consumeOptimizedPrompt]);
// 点击外部关闭快捷短语弹出层
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (
phrasesContainerRef.current &&
!phrasesContainerRef.current.contains(event.target as Node)
) {
setIsPhrasesOpen(false);
}
}
if (isPhrasesOpen) {
document.addEventListener('mousedown', handleClickOutside);
}
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [isPhrasesOpen]);
// ESC键关闭快捷短语弹出层
useEffect(() => {
function handleEscKey(event: KeyboardEvent) {
if (event.key === 'Escape' && isPhrasesOpen) {
setIsPhrasesOpen(false);
}
}
if (isPhrasesOpen) {
document.addEventListener('keydown', handleEscKey);
}
return () => document.removeEventListener('keydown', handleEscKey);
}, [isPhrasesOpen]);
// 使用文件上传 Hook
const {
files,
@ -144,36 +97,9 @@ export function ChatInput({
fileInputRef.current?.click();
};
// 插入快捷短语内容
const handleInsertPhrase = (content: string) => {
setMessage((prev) => {
// 如果已有内容,在末尾添加空格
if (prev.trim()) {
return prev + ' ' + content;
}
return content;
});
};
// 打开管理模态框
const handleOpenManageModal = () => {
setIsManageModalOpen(true);
};
// 关闭管理模态框
const handleCloseManageModal = () => {
setIsManageModalOpen(false);
};
// 处理添加短语(从管理模态框)
const handleAddFromModal = () => {
handleOpenManageModal();
};
return (
<div className={cn('w-full max-w-[var(--input-max-width)] mx-auto', className)}>
<div
ref={phrasesContainerRef}
className={cn(
'relative flex flex-col bg-[var(--color-bg-primary)] border border-[var(--color-border)] rounded-md p-4 shadow-[var(--shadow-input)]',
'transition-all duration-150',
@ -185,19 +111,6 @@ export function ChatInput({
onDragOver={handleDragOver}
onDrop={handleDrop}
>
{/* 快捷短语弹出层 - 横跨在输入框顶部 */}
<QuickPhrasesPopover
phrases={phrases}
isOpen={isPhrasesOpen}
onInsert={handleInsertPhrase}
onEdit={(phrase) => {
setIsManageModalOpen(true);
}}
onDelete={deletePhrase}
onManage={handleOpenManageModal}
onAdd={handleAddFromModal}
onClose={() => setIsPhrasesOpen(false)}
/>
{/* 拖拽覆盖层 */}
{isDragging && (
<div className="absolute inset-0 z-10 flex flex-col items-center justify-center bg-[var(--color-bg-primary)]/90 rounded-md border-2 border-dashed border-[var(--color-primary)]">
@ -240,7 +153,7 @@ export function ChatInput({
<button
onClick={openFileDialog}
className={cn(
'w-8 h-8 flex items-center justify-center rounded-md transition-colors cursor-pointer',
'w-8 h-8 flex items-center justify-center rounded-lg transition-colors cursor-pointer',
'text-[var(--color-text-tertiary)] hover:bg-[var(--color-bg-hover)] hover:text-[var(--color-text-secondary)]',
files.length > 0 && 'text-[var(--color-primary)]'
)}
@ -259,12 +172,6 @@ export function ChatInput({
className="hidden"
/>
{/* 快捷短语触发按钮 */}
<QuickPhrasesTrigger
phrasesCount={phrases.length}
onClick={() => setIsPhrasesOpen(!isPhrasesOpen)}
/>
{/* 工具下拉 */}
<ToolsDropdown
tools={tools}
@ -288,7 +195,7 @@ export function ChatInput({
onClick={handleSend}
disabled={!message.trim() && files.length === 0}
className={cn(
'w-[38px] h-[38px] flex items-center justify-center bg-[var(--color-primary)] text-white rounded-md transition-all duration-150 cursor-pointer',
'w-[38px] h-[38px] flex items-center justify-center bg-[var(--color-primary)] text-white rounded-xl transition-all duration-150 cursor-pointer',
message.trim() || files.length > 0
? 'hover:bg-[var(--color-primary-hover)] hover:-translate-y-0.5'
: 'opacity-50 cursor-not-allowed'
@ -305,16 +212,6 @@ export function ChatInput({
<p className="text-xs text-[var(--color-text-tertiary)] text-center mt-2">
使 Ctrl+V
</p>
{/* 快捷短语管理模态框 */}
<QuickPhrasesModal
isOpen={isManageModalOpen}
onClose={handleCloseManageModal}
phrases={phrases}
onAdd={addPhrase}
onUpdate={updatePhrase}
onDelete={deletePhrase}
/>
</div>
);
}

View File

@ -1,123 +0,0 @@
'use client';
import { useState } from 'react';
import * as LucideIcons from 'lucide-react';
import type { QuickPhrase } from '@/types';
import { cn } from '@/lib/utils';
import { Tooltip } from '@/components/ui/Tooltip';
interface QuickPhraseItemProps {
phrase: QuickPhrase;
onSelect: (phrase: QuickPhrase) => void;
onEdit?: (phrase: QuickPhrase) => void;
onDelete?: (id: string) => void;
showActions?: boolean;
}
/**
*
*/
export function QuickPhraseItem({
phrase,
onSelect,
onEdit,
onDelete,
showActions = true,
}: QuickPhraseItemProps) {
const [isHovered, setIsHovered] = useState(false);
// 获取图标组件
const IconComponent = phrase.icon
? (LucideIcons[phrase.icon as keyof typeof LucideIcons] as React.ComponentType<{ size?: number; className?: string }>)
: LucideIcons.MessageSquare;
// 截断内容预览(最多显示 50 个字符)
const contentPreview = phrase.content.length > 50
? phrase.content.substring(0, 50) + '...'
: phrase.content;
const handleSelect = () => {
onSelect(phrase);
};
const handleEdit = (e: React.MouseEvent) => {
e.stopPropagation();
onEdit?.(phrase);
};
const handleDelete = (e: React.MouseEvent) => {
e.stopPropagation();
onDelete?.(phrase.id);
};
return (
<div
className={cn(
'group relative flex items-start gap-2 px-2.5 py-1.5 rounded-md cursor-pointer transition-all duration-150',
'hover:bg-[var(--color-bg-hover)]',
'border border-transparent hover:border-[var(--color-border)]'
)}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
onClick={handleSelect}
>
{/* 图标 */}
{IconComponent && (
<div className="flex-shrink-0 mt-0.5">
<IconComponent
size={16}
className="text-[var(--color-text-tertiary)]"
/>
</div>
)}
{/* 内容区域 */}
<div className="flex-1 min-w-0">
{/* 标题 */}
<div className="text-xs font-medium text-[var(--color-text-primary)] mb-0.5 truncate">
{phrase.title}
</div>
{/* 内容预览 */}
<div className="text-[11px] text-[var(--color-text-tertiary)] line-clamp-2 leading-tight">
{contentPreview}
</div>
</div>
{/* 操作按钮 */}
{showActions && (isHovered || false) && (
<div className="flex items-center gap-1 flex-shrink-0">
{onEdit && (
<Tooltip content="编辑">
<button
onClick={handleEdit}
className={cn(
'w-6 h-6 flex items-center justify-center rounded transition-colors',
'text-[var(--color-text-tertiary)] hover:text-[var(--color-text-secondary)]',
'hover:bg-[var(--color-bg-secondary)]'
)}
>
<LucideIcons.Pencil size={14} />
</button>
</Tooltip>
)}
{onDelete && (
<Tooltip content="删除">
<button
onClick={handleDelete}
className={cn(
'w-6 h-6 flex items-center justify-center rounded transition-colors',
'text-[var(--color-text-tertiary)] hover:text-red-500',
'hover:bg-red-50 dark:hover:bg-red-500/10'
)}
>
<LucideIcons.Trash2 size={14} />
</button>
</Tooltip>
)}
</div>
)}
</div>
);
}

View File

@ -1,327 +0,0 @@
'use client';
import { useState, useEffect } from 'react';
import { Plus, Trash2, Check } from 'lucide-react';
import { Modal } from '@/components/ui/Modal';
import { IconPicker } from '@/components/ui/IconPicker';
import { cn } from '@/lib/utils';
import type { QuickPhrase } from '@/types';
interface QuickPhrasesModalProps {
isOpen: boolean;
onClose: () => void;
phrases: QuickPhrase[];
onAdd: (phrase: Omit<QuickPhrase, 'id' | 'createdAt' | 'updatedAt' | 'sortOrder'>) => void;
onUpdate: (id: string, updates: Partial<Omit<QuickPhrase, 'id' | 'createdAt'>>) => void;
onDelete: (id: string) => void;
}
/**
*
*/
export function QuickPhrasesModal({
isOpen,
onClose,
phrases,
onAdd,
onUpdate,
onDelete,
}: QuickPhrasesModalProps) {
const [selectedPhraseId, setSelectedPhraseId] = useState<string | null>(null);
const [isEditing, setIsEditing] = useState(false);
// 表单状态
const [formData, setFormData] = useState({
title: '',
content: '',
icon: 'MessageSquare' as string,
category: '',
});
// 当选中短语变化时,更新表单数据
useEffect(() => {
if (selectedPhraseId) {
const phrase = phrases.find((p) => p.id === selectedPhraseId);
if (phrase) {
setFormData({
title: phrase.title,
content: phrase.content,
icon: phrase.icon || 'MessageSquare',
category: phrase.category || '',
});
setIsEditing(true);
}
} else {
// 重置表单
setFormData({
title: '',
content: '',
icon: 'MessageSquare',
category: '',
});
setIsEditing(false);
}
}, [selectedPhraseId, phrases]);
const handleSelectPhrase = (id: string) => {
setSelectedPhraseId(id);
};
const handleNewPhrase = () => {
setSelectedPhraseId(null);
setFormData({
title: '',
content: '',
icon: 'MessageSquare',
category: '',
});
setIsEditing(false);
};
const handleSave = () => {
// 验证表单
if (!formData.title.trim() || !formData.content.trim()) {
alert('请填写标题和内容');
return;
}
if (isEditing && selectedPhraseId) {
// 更新现有短语
onUpdate(selectedPhraseId, {
title: formData.title.trim(),
content: formData.content.trim(),
icon: formData.icon,
category: formData.category.trim() || undefined,
});
} else {
// 添加新短语
onAdd({
title: formData.title.trim(),
content: formData.content.trim(),
icon: formData.icon,
category: formData.category.trim() || undefined,
});
}
// 重置表单
handleNewPhrase();
};
const handleDelete = (id: string) => {
if (confirm('确定要删除这个快捷短语吗?')) {
onDelete(id);
if (selectedPhraseId === id) {
handleNewPhrase();
}
}
};
return (
<Modal
isOpen={isOpen}
onClose={onClose}
maxWidth="1000px"
className="overflow-hidden"
>
{/* 标题 */}
<div className="px-6 py-4 border-b border-[var(--color-border)] bg-gradient-to-b from-[var(--color-bg-secondary)] to-[var(--color-bg-primary)]">
<h2 className="text-xl font-bold text-[var(--color-text-primary)]">
</h2>
</div>
{/* 内容区域 */}
<div className="flex h-[600px]">
{/* 左侧:短语列表 - 固定300px宽度 */}
<div className="w-[300px] border-r border-[var(--color-border)] flex flex-col bg-[var(--color-bg-secondary)]">
{/* 新建按钮 */}
<div className="p-4">
<button
onClick={handleNewPhrase}
className={cn(
'w-full flex items-center justify-center gap-2 px-3 py-2.5 rounded-md transition-all duration-200',
'text-sm font-semibold',
!isEditing && !selectedPhraseId
? 'bg-[var(--color-primary)] text-white hover:shadow-md'
: 'text-[var(--color-text-secondary)] bg-[var(--color-bg-primary)] hover:bg-[var(--color-bg-hover)] border border-[var(--color-border)]'
)}
>
<Plus size={16} />
<span></span>
</button>
</div>
{/* 短语列表 */}
<div className="flex-1 overflow-y-auto px-3 pb-3">
{phrases.length === 0 ? (
<div className="text-center py-8 text-[var(--color-text-tertiary)]">
<p className="text-sm"></p>
<p className="text-xs mt-1"></p>
</div>
) : (
phrases.map((phrase) => (
<div
key={phrase.id}
className={cn(
'group relative flex items-start gap-2.5 px-3 py-2.5 rounded-md cursor-pointer transition-all duration-200 mb-2',
'bg-[var(--color-bg-primary)]',
selectedPhraseId === phrase.id
? 'border-2 border-[var(--color-primary)] shadow-sm'
: 'border-2 border-transparent hover:border-[var(--color-border)]'
)}
onClick={() => handleSelectPhrase(phrase.id)}
>
<div className="flex-1 min-w-0">
<div className={cn(
"text-sm font-semibold truncate mb-0.5",
selectedPhraseId === phrase.id
? 'text-[var(--color-primary)]'
: 'text-[var(--color-text-primary)]'
)}>
{phrase.title}
</div>
<div className="text-xs text-[var(--color-text-tertiary)] truncate">
{phrase.content.substring(0, 30)}...
</div>
</div>
<button
onClick={(e) => {
e.stopPropagation();
handleDelete(phrase.id);
}}
className={cn(
'opacity-0 group-hover:opacity-100 transition-opacity',
'w-6 h-6 flex items-center justify-center rounded',
'text-[var(--color-text-tertiary)] hover:text-red-500',
'hover:bg-red-50 dark:hover:bg-red-500/10'
)}
>
<Trash2 size={14} />
</button>
</div>
))
)}
</div>
</div>
{/* 右侧:编辑表单 - flex-1占据剩余空间 */}
<div className="flex-1 flex flex-col">
<div className="flex-1 overflow-y-auto p-6">
<div className="max-w-2xl">
{/* 标题输入 */}
<div className="mb-5">
<label className="block text-sm font-semibold text-[var(--color-text-primary)] mb-2">
<span className="text-red-500">*</span>
</label>
<input
type="text"
value={formData.title}
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
placeholder="输入短语标题"
className={cn(
'w-full px-3 py-2.5 rounded border-2 border-[var(--color-border)]',
'bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]',
'placeholder:text-[var(--color-text-placeholder)]',
'focus:outline-none focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/10',
'transition-all text-sm'
)}
/>
</div>
{/* 内容输入 */}
<div className="mb-5">
<label className="block text-sm font-semibold text-[var(--color-text-primary)] mb-2">
<span className="text-red-500">*</span>
</label>
<textarea
value={formData.content}
onChange={(e) => setFormData({ ...formData, content: e.target.value })}
placeholder="输入短语内容,将插入到输入框中"
rows={5}
className={cn(
'w-full px-3 py-2.5 rounded border-2 border-[var(--color-border)]',
'bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]',
'placeholder:text-[var(--color-text-placeholder)]',
'focus:outline-none focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/10',
'transition-all resize-none text-sm leading-relaxed'
)}
/>
</div>
{/* 图标选择 */}
<div className="mb-5">
<label className="block text-sm font-semibold text-[var(--color-text-primary)] mb-2">
</label>
<IconPicker
value={formData.icon}
onChange={(icon) => setFormData({ ...formData, icon })}
/>
</div>
{/* 分类输入(可选)*/}
<div className="mb-6">
<label className="block text-sm font-semibold text-[var(--color-text-primary)] mb-2">
</label>
<input
type="text"
value={formData.category}
onChange={(e) => setFormData({ ...formData, category: e.target.value })}
placeholder="输入分类名称"
className={cn(
'w-full px-3 py-2.5 rounded border-2 border-[var(--color-border)]',
'bg-[var(--color-bg-primary)] text-[var(--color-text-primary)]',
'placeholder:text-[var(--color-text-placeholder)]',
'focus:outline-none focus:border-[var(--color-primary)] focus:ring-2 focus:ring-[var(--color-primary)]/10',
'transition-all text-sm'
)}
/>
</div>
{/* 提示信息 */}
<div className="mt-6 p-4 bg-gradient-to-r from-[var(--color-primary)]/5 to-[var(--color-primary)]/10 rounded">
<div className="text-sm font-semibold text-[var(--color-primary)] mb-1">
💡 使
</div>
<div className="text-xs text-[var(--color-text-secondary)] leading-relaxed">
</div>
</div>
</div>
</div>
{/* 底部操作按钮 */}
<div className="px-6 py-4 border-t border-[var(--color-border)] flex items-center justify-end gap-3 bg-[var(--color-bg-secondary)]">
<button
onClick={onClose}
className={cn(
'px-5 py-2.5 rounded-md transition-all',
'text-sm font-semibold text-[var(--color-text-secondary)]',
'bg-[var(--color-bg-primary)] border-2 border-[var(--color-border)]',
'hover:bg-[var(--color-bg-hover)]'
)}
>
</button>
<button
onClick={handleSave}
disabled={!formData.title.trim() || !formData.content.trim()}
className={cn(
'px-5 py-2.5 rounded-md transition-all duration-200',
'text-sm font-semibold text-white',
'flex items-center gap-2',
formData.title.trim() && formData.content.trim()
? 'bg-[var(--color-primary)] hover:bg-[var(--color-primary-hover)]'
: 'bg-gray-300 cursor-not-allowed opacity-50'
)}
>
<Check size={16} />
<span>{isEditing ? '保存修改' : '添加短语'}</span>
</button>
</div>
</div>
</div>
</Modal>
);
}

View File

@ -1,144 +0,0 @@
'use client';
import { MessageSquareQuote, Plus, Settings2 } from 'lucide-react';
import { Tooltip } from '@/components/ui/Tooltip';
import { QuickPhraseItem } from './QuickPhraseItem';
import { cn } from '@/lib/utils';
import type { QuickPhrase } from '@/types';
// 触发按钮组件
interface QuickPhrasesTriggerProps {
phrasesCount: number;
onClick: () => void;
}
export function QuickPhrasesTrigger({ phrasesCount, onClick }: QuickPhrasesTriggerProps) {
return (
<div className="relative">
<Tooltip content="快捷短语" position="top">
<button
onClick={onClick}
className="w-8 h-8 flex items-center justify-center rounded-md transition-colors cursor-pointer text-[var(--color-text-tertiary)] hover:bg-[var(--color-bg-hover)] hover:text-[var(--color-text-secondary)]"
>
<MessageSquareQuote size={20} />
</button>
</Tooltip>
{/* 数量徽章 */}
{phrasesCount > 0 && (
<span className="absolute -top-0.5 -right-0.5 min-w-4 h-4 bg-[var(--color-primary)] text-white text-[10px] font-semibold rounded-full flex items-center justify-center px-1 pointer-events-none">
{phrasesCount}
</span>
)}
</div>
);
}
// 弹出层内容组件
interface QuickPhrasesPopoverProps {
phrases: QuickPhrase[];
isOpen: boolean;
onInsert: (content: string) => void;
onEdit: (phrase: QuickPhrase) => void;
onDelete: (id: string) => void;
onManage: () => void;
onAdd: () => void;
onClose: () => void;
}
/**
*
*/
export function QuickPhrasesPopover({
phrases,
isOpen,
onInsert,
onEdit,
onDelete,
onManage,
onAdd,
onClose,
}: QuickPhrasesPopoverProps) {
const handleSelect = (phrase: QuickPhrase) => {
onInsert(phrase.content);
onClose();
};
const handleManage = () => {
onManage();
onClose();
};
return (
<div
className={cn(
'absolute bottom-full left-0 right-0 mb-2 bg-[var(--color-bg-primary)] border border-[var(--color-border)] rounded-md shadow-lg z-50',
'transition-all duration-150',
isOpen
? 'opacity-100 visible translate-y-0'
: 'opacity-0 invisible translate-y-2 pointer-events-none'
)}
>
{/* 顶部操作栏 */}
<div className="flex items-center justify-between px-3 py-2 border-b border-[var(--color-border-light)]">
<span className="text-xs font-medium text-[var(--color-text-primary)]">
</span>
<Tooltip content="添加短语">
<button
onClick={onAdd}
className={cn(
'w-6 h-6 flex items-center justify-center rounded-md transition-colors',
'text-[var(--color-text-tertiary)] hover:text-[var(--color-primary)]',
'hover:bg-[var(--color-bg-hover)]'
)}
>
<Plus size={16} />
</button>
</Tooltip>
</div>
{/* 短语列表 */}
<div className="max-h-[320px] overflow-y-auto p-1.5">
{phrases.length === 0 ? (
<div className="text-center py-8 text-[var(--color-text-tertiary)]">
<MessageSquareQuote
size={32}
className="mx-auto mb-2 opacity-50"
/>
<p className="text-sm"></p>
<p className="text-xs mt-1"> + </p>
</div>
) : (
phrases.map((phrase) => (
<QuickPhraseItem
key={phrase.id}
phrase={phrase}
onSelect={handleSelect}
onEdit={onEdit}
onDelete={onDelete}
showActions={true}
/>
))
)}
</div>
{/* 底部操作栏 */}
{phrases.length > 0 && (
<div className="border-t border-[var(--color-border-light)] p-1.5">
<button
onClick={handleManage}
className={cn(
'w-full flex items-center justify-center gap-1.5 px-2.5 py-1.5 rounded-md transition-colors',
'text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]',
'hover:bg-[var(--color-bg-hover)]'
)}
>
<Settings2 size={14} />
<span></span>
</button>
</div>
)}
</div>
);
}

View File

@ -47,7 +47,12 @@ export function ToolsDropdown({ tools, onToolToggle, onEnableAllToggle }: ToolsD
<Tooltip content="工具" position="top">
<button
onClick={() => setIsOpen(!isOpen)}
className="w-8 h-8 flex items-center justify-center rounded-md transition-colors cursor-pointer text-[var(--color-text-tertiary)] hover:bg-[var(--color-bg-hover)] hover:text-[var(--color-text-secondary)]"
className={cn(
'w-8 h-8 flex items-center justify-center rounded-lg transition-colors cursor-pointer',
enabledCount > 0
? 'text-[var(--color-primary)] bg-[var(--color-primary-light)]'
: 'text-[var(--color-text-tertiary)] hover:bg-[var(--color-bg-hover)] hover:text-[var(--color-text-secondary)]'
)}
>
<Wrench size={20} />
</button>
@ -62,7 +67,7 @@ export function ToolsDropdown({ tools, onToolToggle, onEnableAllToggle }: ToolsD
{/* 下拉菜单 */}
<div
className={cn(
'absolute bottom-full left-0 mb-2 min-w-[220px] bg-[var(--color-bg-primary)] border border-[var(--color-border)] rounded-md shadow-lg p-2 z-50',
'absolute bottom-full left-0 mb-2 min-w-[220px] bg-[var(--color-bg-primary)] border border-[var(--color-border)] rounded-xl shadow-lg p-2 z-50',
'transition-all duration-150',
isOpen
? 'opacity-100 visible translate-y-0'
@ -82,7 +87,7 @@ export function ToolsDropdown({ tools, onToolToggle, onEnableAllToggle }: ToolsD
<button
key={tool.id}
onClick={() => onToolToggle(tool.id)}
className="flex items-center gap-3 w-full px-3 py-2 rounded-md hover:bg-[var(--color-bg-hover)] transition-colors"
className="flex items-center gap-3 w-full px-3 py-2 rounded-lg hover:bg-[var(--color-bg-hover)] transition-colors"
>
{Icon && <Icon size={20} className="text-[var(--color-text-tertiary)]" />}
<span className="flex-1 text-sm text-[var(--color-text-primary)] text-left">

View File

@ -87,7 +87,7 @@ export function Modal({
<div
ref={contentRef}
className={cn(
'relative bg-[var(--color-bg-primary)] rounded-md shadow-2xl',
'relative bg-[var(--color-bg-primary)] rounded-xl shadow-2xl',
'animate-scale-in-fast',
fullScreen ? 'w-full h-full rounded-none' : 'max-h-[90vh]',
className

View File

@ -1,141 +0,0 @@
import { useState, useEffect, useCallback, useMemo } from 'react';
import type { QuickPhrase } from '@/types';
import {
loadPhrasesFromStorage,
savePhrasesToStorage,
generatePhraseId,
sortPhrases,
validatePhrase,
} from '@/lib/quick-phrases';
interface UseQuickPhrasesReturn {
phrases: QuickPhrase[];
addPhrase: (phrase: Omit<QuickPhrase, 'id' | 'createdAt' | 'updatedAt' | 'sortOrder'>) => void;
updatePhrase: (id: string, updates: Partial<Omit<QuickPhrase, 'id' | 'createdAt'>>) => void;
deletePhrase: (id: string) => void;
reorderPhrases: (reorderedPhrases: QuickPhrase[]) => void;
resetToDefaults: () => void;
}
/**
* Hook
* localStorage
*/
export function useQuickPhrases(): UseQuickPhrasesReturn {
const [phrases, setPhrases] = useState<QuickPhrase[]>([]);
// 初始化:从 localStorage 加载数据
useEffect(() => {
const loadedPhrases = loadPhrasesFromStorage();
setPhrases(loadedPhrases);
}, []);
// 保存到 localStorage 的辅助函数
const saveAndUpdate = useCallback((newPhrases: QuickPhrase[]) => {
savePhrasesToStorage(newPhrases);
setPhrases(newPhrases);
}, []);
/**
*
*/
const addPhrase = useCallback(
(phrase: Omit<QuickPhrase, 'id' | 'createdAt' | 'updatedAt' | 'sortOrder'>) => {
const now = Date.now();
const newPhrase: QuickPhrase = {
...phrase,
id: generatePhraseId(),
sortOrder: phrases.length + 1, // 添加到末尾
createdAt: now,
updatedAt: now,
};
// 验证短语数据
if (!validatePhrase(newPhrase)) {
console.error('Invalid phrase data:', newPhrase);
return;
}
const newPhrases = [...phrases, newPhrase];
saveAndUpdate(newPhrases);
},
[phrases, saveAndUpdate]
);
/**
*
*/
const updatePhrase = useCallback(
(id: string, updates: Partial<Omit<QuickPhrase, 'id' | 'createdAt'>>) => {
const newPhrases = phrases.map((phrase) => {
if (phrase.id === id) {
const updatedPhrase = {
...phrase,
...updates,
updatedAt: Date.now(),
};
return updatedPhrase;
}
return phrase;
});
saveAndUpdate(newPhrases);
},
[phrases, saveAndUpdate]
);
/**
*
*/
const deletePhrase = useCallback(
(id: string) => {
const newPhrases = phrases.filter((phrase) => phrase.id !== id);
// 重新调整 sortOrder
const reorderedPhrases = newPhrases.map((phrase, index) => ({
...phrase,
sortOrder: index + 1,
}));
saveAndUpdate(reorderedPhrases);
},
[phrases, saveAndUpdate]
);
/**
*
*/
const reorderPhrases = useCallback(
(reorderedPhrases: QuickPhrase[]) => {
// 更新 sortOrder
const phrasesWithNewOrder = reorderedPhrases.map((phrase, index) => ({
...phrase,
sortOrder: index + 1,
updatedAt: Date.now(),
}));
saveAndUpdate(phrasesWithNewOrder);
},
[saveAndUpdate]
);
/**
*
*/
const resetToDefaults = useCallback(() => {
const defaultPhrases = loadPhrasesFromStorage();
// 清除 localStorage 并重新加载默认值
localStorage.removeItem('cch_quick_phrases');
const freshDefaults = loadPhrasesFromStorage();
saveAndUpdate(freshDefaults);
}, [saveAndUpdate]);
// 返回排序后的短语列表
const sortedPhrases = useMemo(() => sortPhrases(phrases), [phrases]);
return {
phrases: sortedPhrases,
addPhrase,
updatePhrase,
deletePhrase,
reorderPhrases,
resetToDefaults,
};
}

View File

@ -1,118 +0,0 @@
import type { QuickPhrase } from '@/types';
/**
* ID
*/
export function generatePhraseId(): string {
return `phrase-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
}
/**
*
*/
export const DEFAULT_PHRASES: QuickPhrase[] = [
{
id: 'default-1',
title: '写作助手',
content: '请帮我写一篇关于',
icon: 'PenTool',
sortOrder: 1,
createdAt: Date.now(),
updatedAt: Date.now(),
},
{
id: 'default-2',
title: '代码解释',
content: '请解释这段代码的功能和工作原理:\n\n',
icon: 'Code',
sortOrder: 2,
createdAt: Date.now(),
updatedAt: Date.now(),
},
{
id: 'default-3',
title: '优化建议',
content: '请帮我优化以下内容,使其更加专业和准确:\n\n',
icon: 'Sparkles',
sortOrder: 3,
createdAt: Date.now(),
updatedAt: Date.now(),
},
{
id: 'default-4',
title: '翻译助手',
content: '请帮我翻译以下内容:\n\n',
icon: 'Languages',
sortOrder: 4,
createdAt: Date.now(),
updatedAt: Date.now(),
},
{
id: 'default-5',
title: '头脑风暴',
content: '请帮我进行头脑风暴,给我一些关于以下主题的创意想法:\n\n',
icon: 'Lightbulb',
sortOrder: 5,
createdAt: Date.now(),
updatedAt: Date.now(),
},
];
/**
* localStorage key
*/
export const STORAGE_KEY = 'cch_quick_phrases';
/**
* localStorage
*/
export function loadPhrasesFromStorage(): QuickPhrase[] {
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
const phrases = JSON.parse(stored) as QuickPhrase[];
// 验证数据格式
if (Array.isArray(phrases) && phrases.length > 0) {
return phrases;
}
}
} catch (error) {
console.error('Failed to load quick phrases from storage:', error);
}
// 如果没有存储的数据或加载失败,返回默认短语
return DEFAULT_PHRASES;
}
/**
* localStorage
*/
export function savePhrasesToStorage(phrases: QuickPhrase[]): void {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(phrases));
} catch (error) {
console.error('Failed to save quick phrases to storage:', error);
}
}
/**
*
*/
export function validatePhrase(phrase: Partial<QuickPhrase>): phrase is QuickPhrase {
return (
typeof phrase.id === 'string' &&
typeof phrase.title === 'string' &&
phrase.title.trim().length > 0 &&
typeof phrase.content === 'string' &&
phrase.content.trim().length > 0 &&
typeof phrase.sortOrder === 'number' &&
typeof phrase.createdAt === 'number' &&
typeof phrase.updatedAt === 'number'
);
}
/**
*
*/
export function sortPhrases(phrases: QuickPhrase[]): QuickPhrase[] {
return [...phrases].sort((a, b) => a.sortOrder - b.sortOrder);
}

View File

@ -125,15 +125,3 @@ export interface QuickAction {
icon: string;
prompt?: string;
}
// 快捷短语类型
export interface QuickPhrase {
id: string; // 唯一标识UUID
title: string; // 短语标题(必填,用于列表显示)
content: string; // 短语内容(必填,插入到输入框的文本)
category?: string; // 分类(可选,用于分组)
icon?: string; // 图标名称(可选,使用 lucide-react 图标)
sortOrder: number; // 排序顺序
createdAt: number; // 创建时间戳
updatedAt: number; // 更新时间戳
}