diff --git a/src/components/ui/FontSizePicker.tsx b/src/components/ui/FontSizePicker.tsx
new file mode 100644
index 0000000..9bcad9d
--- /dev/null
+++ b/src/components/ui/FontSizePicker.tsx
@@ -0,0 +1,138 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { Minus, Plus } from 'lucide-react';
+import { cn } from '@/lib/utils';
+
+interface FontSizePickerProps {
+ value: number;
+ onChange: (size: number) => void;
+ min?: number;
+ max?: number;
+ disabled?: boolean;
+}
+
+export function FontSizePicker({
+ value,
+ onChange,
+ min = 12,
+ max = 20,
+ disabled = false,
+}: FontSizePickerProps) {
+ const [previewVisible, setPreviewVisible] = useState(false);
+ const [localValue, setLocalValue] = useState(value);
+
+ // 同步外部值
+ useEffect(() => {
+ setLocalValue(value);
+ }, [value]);
+
+ // 实时预览 - 修改 CSS 变量
+ useEffect(() => {
+ document.documentElement.style.setProperty('--font-size-base', `${localValue}px`);
+ }, [localValue]);
+
+ const handleDecrease = () => {
+ if (localValue > min && !disabled) {
+ const newValue = localValue - 1;
+ setLocalValue(newValue);
+ onChange(newValue);
+ }
+ };
+
+ const handleIncrease = () => {
+ if (localValue < max && !disabled) {
+ const newValue = localValue + 1;
+ setLocalValue(newValue);
+ onChange(newValue);
+ }
+ };
+
+ const togglePreview = () => {
+ setPreviewVisible(!previewVisible);
+ };
+
+ return (
+
+
+ {/* 减号按钮 */}
+
+
+ {/* 数字显示 */}
+
+
+ {localValue}
+
+ px
+
+
+ {/* 加号按钮 */}
+
+
+ {/* Preview 按钮 */}
+
+
+
+ {/* 预览区域 */}
+ {previewVisible && (
+
+
+ 这是一段预览文字,用于展示当前字体大小的效果。
+
+ The quick brown fox jumps over the lazy dog.
+
+
+ )}
+
+ );
+}
diff --git a/src/components/ui/ModelCardSelector.tsx b/src/components/ui/ModelCardSelector.tsx
new file mode 100644
index 0000000..a1c9bd2
--- /dev/null
+++ b/src/components/ui/ModelCardSelector.tsx
@@ -0,0 +1,108 @@
+'use client';
+
+import { cn } from '@/lib/utils';
+
+// 模型卡片配置
+const MODEL_CARDS = [
+ {
+ id: 'haiku',
+ name: 'Haiku',
+ description: 'Fast & efficient',
+ modelIdPattern: 'haiku',
+ },
+ {
+ id: 'sonnet',
+ name: 'Sonnet',
+ description: 'Balanced',
+ modelIdPattern: 'sonnet',
+ },
+ {
+ id: 'opus',
+ name: 'Opus',
+ description: 'Most capable',
+ modelIdPattern: 'opus',
+ },
+];
+
+interface ModelCardSelectorProps {
+ value: string;
+ onChange: (modelId: string) => void;
+ disabled?: boolean;
+ models?: { modelId: string; displayName: string }[];
+}
+
+export function ModelCardSelector({
+ value,
+ onChange,
+ disabled = false,
+ models = [],
+}: ModelCardSelectorProps) {
+ // 根据当前选中的模型ID判断选中的卡片
+ const getSelectedCard = (modelId: string): string => {
+ const lowerModelId = modelId.toLowerCase();
+ if (lowerModelId.includes('haiku')) return 'haiku';
+ if (lowerModelId.includes('opus')) return 'opus';
+ return 'sonnet'; // 默认 sonnet
+ };
+
+ // 根据卡片类型找到对应的实际模型ID
+ const findModelIdByCard = (cardId: string): string => {
+ const model = models.find((m) =>
+ m.modelId.toLowerCase().includes(cardId)
+ );
+ return model?.modelId || value;
+ };
+
+ const selectedCard = getSelectedCard(value);
+
+ return (
+
+ {MODEL_CARDS.map((card) => {
+ const isSelected = selectedCard === card.id;
+ return (
+
+ );
+ })}
+
+ );
+}
diff --git a/src/components/ui/UserMenu.tsx b/src/components/ui/UserMenu.tsx
new file mode 100644
index 0000000..a3add28
--- /dev/null
+++ b/src/components/ui/UserMenu.tsx
@@ -0,0 +1,183 @@
+'use client';
+
+import { useState, useRef, useEffect } from 'react';
+import { useRouter } from 'next/navigation';
+import { Moon, Sun, Settings, LogOut, ChevronUp, ChevronDown } from 'lucide-react';
+import { Avatar } from '@/components/ui/Avatar';
+import { useSettings } from '@/hooks/useSettings';
+import { cn } from '@/lib/utils';
+import type { User } from '@/types';
+
+interface UserMenuProps {
+ user: User;
+}
+
+export function UserMenu({ user }: UserMenuProps) {
+ const [isOpen, setIsOpen] = useState(false);
+ const menuRef = useRef(null);
+ const router = useRouter();
+ const { settings, updateSettings } = useSettings();
+
+ // 点击外部关闭菜单
+ useEffect(() => {
+ function handleClickOutside(event: MouseEvent) {
+ if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
+ setIsOpen(false);
+ }
+ }
+
+ if (isOpen) {
+ document.addEventListener('mousedown', handleClickOutside);
+ }
+
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, [isOpen]);
+
+ // ESC 键关闭菜单
+ useEffect(() => {
+ function handleEscKey(event: KeyboardEvent) {
+ if (event.key === 'Escape') {
+ setIsOpen(false);
+ }
+ }
+
+ if (isOpen) {
+ document.addEventListener('keydown', handleEscKey);
+ }
+
+ return () => {
+ document.removeEventListener('keydown', handleEscKey);
+ };
+ }, [isOpen]);
+
+ // 切换主题
+ const handleToggleTheme = async () => {
+ const newTheme = settings.theme === 'dark' ? 'light' : 'dark';
+ try {
+ await updateSettings({ theme: newTheme });
+ // 可选:更新 document 的 class 来应用主题
+ document.documentElement.setAttribute('data-theme', newTheme);
+ } catch (error) {
+ console.error('Failed to update theme:', error);
+ }
+ };
+
+ // 进入设置页面
+ const handleGoToSettings = () => {
+ setIsOpen(false);
+ router.push('/settings');
+ };
+
+ // 登出
+ const handleLogout = () => {
+ setIsOpen(false);
+ // TODO: 实现实际的登出逻辑
+ // 暂时跳转到首页
+ console.log('Logging out...');
+ router.push('/');
+ };
+
+ const isDarkMode = settings.theme === 'dark';
+
+ return (
+
+ {/* 触发器 - 用户信息区域 */}
+
+
+ {/* 弹出菜单 - 向上弹出 */}
+ {isOpen && (
+
+ {/* 菜单项 */}
+
+ {/* Dark mode 切换 */}
+
+
+ {/* Settings */}
+
+
+ {/* 分隔线 */}
+
+
+ {/* Log out */}
+
+
+
+ )}
+
+ );
+}