diff --git a/src/hooks/useTags.ts b/src/hooks/useTags.ts new file mode 100644 index 0000000..7624a5d --- /dev/null +++ b/src/hooks/useTags.ts @@ -0,0 +1,229 @@ +'use client'; + +import { useState, useEffect, useCallback } from 'react'; +import type { TagStats, SuggestedTag, AutoTagResponse, AllTagsResponse } from '@/types/tags'; + +/** + * 全局标签管理 Hook - 获取用户所有标签统计 + */ +export function useTags() { + const [tags, setTags] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchTags = useCallback(async () => { + try { + setLoading(true); + setError(null); + const response = await fetch('/api/tags'); + if (!response.ok) { + throw new Error('获取标签失败'); + } + const data: AllTagsResponse = await response.json(); + setTags(data.tags); + } catch (err) { + setError(err instanceof Error ? err.message : '未知错误'); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchTags(); + }, [fetchTags]); + + return { + tags, + loading, + error, + refetch: fetchTags, + }; +} + +/** + * 对话标签管理 Hook - 管理单个对话的标签 + */ +export function useConversationTags(conversationId: string | null) { + const [tags, setTags] = useState([]); + const [loading, setLoading] = useState(false); + const [updating, setUpdating] = useState(false); + const [generating, setGenerating] = useState(false); + const [suggestedTags, setSuggestedTags] = useState([]); + const [error, setError] = useState(null); + + // 获取对话标签 + const fetchTags = useCallback(async () => { + if (!conversationId) { + setTags([]); + return; + } + + try { + setLoading(true); + setError(null); + const response = await fetch(`/api/conversations/${conversationId}/tags`); + if (!response.ok) { + throw new Error('获取标签失败'); + } + const data = await response.json(); + setTags(data.tags || []); + } catch (err) { + setError(err instanceof Error ? err.message : '未知错误'); + } finally { + setLoading(false); + } + }, [conversationId]); + + // 更新标签 + const updateTags = useCallback(async (newTags: string[]) => { + if (!conversationId) return; + + try { + setUpdating(true); + setError(null); + const response = await fetch(`/api/conversations/${conversationId}/tags`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ tags: newTags }), + }); + if (!response.ok) { + const data = await response.json(); + throw new Error(data.error || '更新标签失败'); + } + const data = await response.json(); + setTags(data.tags || []); + return data.tags; + } catch (err) { + setError(err instanceof Error ? err.message : '未知错误'); + throw err; + } finally { + setUpdating(false); + } + }, [conversationId]); + + // 添加单个标签 + const addTag = useCallback(async (tag: string) => { + const trimmedTag = tag.trim(); + if (!trimmedTag || tags.includes(trimmedTag)) return; + const newTags = [...tags, trimmedTag]; + return updateTags(newTags); + }, [tags, updateTags]); + + // 移除单个标签 + const removeTag = useCallback(async (tag: string) => { + const newTags = tags.filter((t) => t !== tag); + return updateTags(newTags); + }, [tags, updateTags]); + + // AI 自动生成标签 + const generateTags = useCallback(async () => { + if (!conversationId) return; + + try { + setGenerating(true); + setError(null); + setSuggestedTags([]); + + const response = await fetch(`/api/conversations/${conversationId}/tags/auto`, { + method: 'POST', + }); + + if (!response.ok) { + const data = await response.json(); + throw new Error(data.error || '生成标签失败'); + } + + const data: AutoTagResponse = await response.json(); + setSuggestedTags(data.suggestedTags); + return data.suggestedTags; + } catch (err) { + setError(err instanceof Error ? err.message : '未知错误'); + throw err; + } finally { + setGenerating(false); + } + }, [conversationId]); + + // 应用选中的推荐标签 + const applySuggestedTags = useCallback(async (selectedTags: string[]) => { + // 合并现有标签和选中的推荐标签,去重 + const mergedTags = [...new Set([...tags, ...selectedTags])]; + const result = await updateTags(mergedTags); + setSuggestedTags([]); // 清空推荐列表 + return result; + }, [tags, updateTags]); + + // 清空推荐标签 + const clearSuggestedTags = useCallback(() => { + setSuggestedTags([]); + }, []); + + useEffect(() => { + fetchTags(); + }, [fetchTags]); + + return { + tags, + loading, + updating, + generating, + suggestedTags, + error, + refetch: fetchTags, + updateTags, + addTag, + removeTag, + generateTags, + applySuggestedTags, + clearSuggestedTags, + }; +} + +/** + * 标签筛选 Hook - 用于侧边栏筛选对话 + */ +export function useTagFilter() { + const [selectedTags, setSelectedTags] = useState([]); + + // 切换标签选中状态 + const toggleTag = useCallback((tag: string) => { + setSelectedTags((prev) => { + if (prev.includes(tag)) { + return prev.filter((t) => t !== tag); + } + return [...prev, tag]; + }); + }, []); + + // 选中单个标签(单选模式) + const selectTag = useCallback((tag: string) => { + setSelectedTags((prev) => { + if (prev.includes(tag) && prev.length === 1) { + return []; // 如果已选中且只有一个,则取消选中 + } + return [tag]; + }); + }, []); + + // 清除所有筛选 + const clearFilter = useCallback(() => { + setSelectedTags([]); + }, []); + + // 检查对话是否匹配筛选条件 + const matchesFilter = useCallback((conversationTags: string[] | null) => { + if (selectedTags.length === 0) return true; + if (!conversationTags || conversationTags.length === 0) return false; + // 对话必须包含所有选中的标签 + return selectedTags.every((tag) => conversationTags.includes(tag)); + }, [selectedTags]); + + return { + selectedTags, + isFiltering: selectedTags.length > 0, + toggleTag, + selectTag, + clearFilter, + matchesFilter, + }; +}