feat: 重构 AI 搜索架构并优化用户体验
- 重构 AI 搜索数据流:n8n 只返回项目 ID,Next.js 后端负责组装完整数据 - 首页添加 AI 搜索功能,支持跳转到项目列表页后自动触发搜索 - 新增 HomeSearchBar 组件处理首页搜索逻辑 - 修复英文模式下搜索按钮重叠问题,使用 flex 布局确保间距 - 完善国际化支持,添加切换模式提示和加载状态的翻译 - 优化搜索体验:切换 AI/传统搜索模式时保留输入框内容 Co-Authored-By: Claude (glm-4.7) <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import { getTranslations } from 'next-intl/server'
|
import { getTranslations } from 'next-intl/server'
|
||||||
import { getProjects } from '@/hooks/useProjects'
|
import { getProjects } from '@/hooks/useProjects'
|
||||||
import { ProjectList } from '@/components/project/ProjectList'
|
import { ProjectList } from '@/components/project/ProjectList'
|
||||||
import { SearchBar } from '@/components/search/SearchBar'
|
import { HomeSearchBar } from '@/components/search/HomeSearchBar'
|
||||||
import { Suspense } from 'react'
|
import { Suspense } from 'react'
|
||||||
|
|
||||||
interface HomePageProps {
|
interface HomePageProps {
|
||||||
@@ -30,10 +30,17 @@ export default async function HomePage({ params }: HomePageProps) {
|
|||||||
{t('heroDescription')}
|
{t('heroDescription')}
|
||||||
</p>
|
</p>
|
||||||
<Suspense fallback={<div className="h-16"></div>}>
|
<Suspense fallback={<div className="h-16"></div>}>
|
||||||
<SearchBar
|
<HomeSearchBar
|
||||||
locale={locale}
|
locale={locale}
|
||||||
searchPlaceholder={t('searchPlaceholder')}
|
searchPlaceholder={t('searchPlaceholder')}
|
||||||
searchLabel={tCommon('search')}
|
searchLabel={tCommon('search')}
|
||||||
|
aiPlaceholder={t('aiSearchPlaceholder')}
|
||||||
|
aiLabel={tCommon('aiSearch')}
|
||||||
|
toggleToAI={tCommon('toggleToAI')}
|
||||||
|
toggleToTraditional={tCommon('toggleToTraditional')}
|
||||||
|
searching={tCommon('searching')}
|
||||||
|
aiHint={tCommon('aiHint')}
|
||||||
|
traditionalHint={tCommon('traditionalHint')}
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
|
import { useSearchParams } from 'next/navigation'
|
||||||
import { AISearchBar } from '@/components/search/AISearchBar'
|
import { AISearchBar } from '@/components/search/AISearchBar'
|
||||||
import { AISearchResults } from '@/components/search/AISearchResults'
|
import { AISearchResults } from '@/components/search/AISearchResults'
|
||||||
import type { ProjectWithFlatTags } from '@/hooks/useProjects'
|
import type { ProjectWithFlatTags } from '@/hooks/useProjects'
|
||||||
@@ -18,6 +19,11 @@ interface ProjectsPageClientProps {
|
|||||||
searchLabel: string
|
searchLabel: string
|
||||||
aiPlaceholder: string
|
aiPlaceholder: string
|
||||||
aiLabel: string
|
aiLabel: string
|
||||||
|
toggleToAI: string
|
||||||
|
toggleToTraditional: string
|
||||||
|
searching: string
|
||||||
|
aiHint: string
|
||||||
|
traditionalHint: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ProjectsPageClient({
|
export function ProjectsPageClient({
|
||||||
@@ -27,22 +33,31 @@ export function ProjectsPageClient({
|
|||||||
searchLabel,
|
searchLabel,
|
||||||
aiPlaceholder,
|
aiPlaceholder,
|
||||||
aiLabel,
|
aiLabel,
|
||||||
|
toggleToAI,
|
||||||
|
toggleToTraditional,
|
||||||
|
searching,
|
||||||
|
aiHint,
|
||||||
|
traditionalHint,
|
||||||
}: ProjectsPageClientProps) {
|
}: ProjectsPageClientProps) {
|
||||||
|
const searchParams = useSearchParams()
|
||||||
const [aiResults, setAiResults] = useState<AISearchResult[]>([])
|
const [aiResults, setAiResults] = useState<AISearchResult[]>([])
|
||||||
const [isAIResult, setIsAIResult] = useState(false)
|
const [isAIResult, setIsAIResult] = useState(false)
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const [initialAIHandled, setInitialAIHandled] = useState(false)
|
||||||
|
|
||||||
const handleAISearch = async (query: string, isAI: boolean) => {
|
// 检查 URL 参数,如果有 ai=1 且有搜索词,则自动触发 AI 搜索
|
||||||
if (!isAI) {
|
useEffect(() => {
|
||||||
// 传统搜索:刷新页面到 URL 参数
|
const aiParam = searchParams.get('ai')
|
||||||
const params = new URLSearchParams()
|
const searchParam = searchParams.get('search')
|
||||||
if (query) params.set('search', query)
|
|
||||||
window.location.href = `/${locale}/projects?${params.toString()}`
|
if (aiParam === '1' && searchParam && !initialAIHandled) {
|
||||||
return
|
setInitialAIHandled(true)
|
||||||
|
performAISearch(searchParam)
|
||||||
}
|
}
|
||||||
|
}, [searchParams, initialAIHandled])
|
||||||
|
|
||||||
// AI 搜索
|
const performAISearch = async (query: string) => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError(null)
|
setError(null)
|
||||||
setIsAIResult(true)
|
setIsAIResult(true)
|
||||||
@@ -63,7 +78,31 @@ export function ProjectsPageClient({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
setAiResults(data.results || [])
|
|
||||||
|
// 转换 API 返回格式为组件期望格式
|
||||||
|
// API 返回: { projects: [{ ...project, similarity }], pagination }
|
||||||
|
// 组件期望: [{ project, similarity }]
|
||||||
|
const { similarity, ...projectData } = data.projects || []
|
||||||
|
const results = (data.projects || []).map((p: any) => ({
|
||||||
|
project: {
|
||||||
|
id: p.id,
|
||||||
|
name: p.name,
|
||||||
|
nameEn: p.nameEn,
|
||||||
|
slug: p.slug,
|
||||||
|
description: p.description,
|
||||||
|
descriptionEn: p.descriptionEn,
|
||||||
|
content: p.content,
|
||||||
|
contentEn: p.contentEn,
|
||||||
|
status: p.status,
|
||||||
|
createdAt: p.createdAt,
|
||||||
|
updatedAt: p.updatedAt,
|
||||||
|
tags: p.tags || [],
|
||||||
|
links: p.links || [],
|
||||||
|
},
|
||||||
|
similarity: p.similarity,
|
||||||
|
}))
|
||||||
|
|
||||||
|
setAiResults(results)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : '未知错误')
|
setError(err instanceof Error ? err.message : '未知错误')
|
||||||
setAiResults([])
|
setAiResults([])
|
||||||
@@ -72,6 +111,19 @@ export function ProjectsPageClient({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleAISearch = async (query: string, isAI: boolean) => {
|
||||||
|
if (!isAI) {
|
||||||
|
// 传统搜索:刷新页面到 URL 参数
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (query) params.set('search', query)
|
||||||
|
window.location.href = `/${locale}/projects?${params.toString()}`
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// AI 搜索
|
||||||
|
await performAISearch(query)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* AI 搜索栏 */}
|
{/* AI 搜索栏 */}
|
||||||
@@ -81,6 +133,11 @@ export function ProjectsPageClient({
|
|||||||
searchLabel={searchLabel}
|
searchLabel={searchLabel}
|
||||||
aiPlaceholder={aiPlaceholder}
|
aiPlaceholder={aiPlaceholder}
|
||||||
aiLabel={aiLabel}
|
aiLabel={aiLabel}
|
||||||
|
toggleToAI={toggleToAI}
|
||||||
|
toggleToTraditional={toggleToTraditional}
|
||||||
|
searching={searching}
|
||||||
|
aiHint={aiHint}
|
||||||
|
traditionalHint={traditionalHint}
|
||||||
onSearch={handleAISearch}
|
onSearch={handleAISearch}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -40,6 +40,11 @@ export default async function ProjectsPage({
|
|||||||
searchLabel={tCommon('search')}
|
searchLabel={tCommon('search')}
|
||||||
aiPlaceholder={t('aiSearchPlaceholder')}
|
aiPlaceholder={t('aiSearchPlaceholder')}
|
||||||
aiLabel={tCommon('aiSearch')}
|
aiLabel={tCommon('aiSearch')}
|
||||||
|
toggleToAI={tCommon('toggleToAI')}
|
||||||
|
toggleToTraditional={tCommon('toggleToTraditional')}
|
||||||
|
searching={tCommon('searching')}
|
||||||
|
aiHint={tCommon('aiHint')}
|
||||||
|
traditionalHint={tCommon('traditionalHint')}
|
||||||
>
|
>
|
||||||
{/* Tag Cloud - rendered inside client component for non-AI mode */}
|
{/* Tag Cloud - rendered inside client component for non-AI mode */}
|
||||||
<div className="border-t-2 border-gray-100 dark:border-gray-800 pt-6 mt-8">
|
<div className="border-t-2 border-gray-100 dark:border-gray-800 pt-6 mt-8">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { ZodError } from 'zod'
|
import { ZodError, z } from 'zod'
|
||||||
import { ProjectQuerySchema } from '@/lib/validations'
|
import { ProjectQuerySchema } from '@/lib/validations'
|
||||||
|
import { getProjectsByIds, type AISearchResultItem } from '@/hooks/useProjects'
|
||||||
|
|
||||||
const N8N_WEBHOOK_URL = process.env.N8N_AI_SEARCH_WEBHOOK!
|
const N8N_WEBHOOK_URL = process.env.N8N_AI_SEARCH_WEBHOOK!
|
||||||
|
|
||||||
@@ -8,6 +9,14 @@ if (!N8N_WEBHOOK_URL) {
|
|||||||
throw new Error('N8N_AI_SEARCH_WEBHOOK environment variable is not set')
|
throw new Error('N8N_AI_SEARCH_WEBHOOK environment variable is not set')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// n8n 返回的简化搜索结果 Schema
|
||||||
|
const N8NSearchResultSchema = z.array(
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
similarity: z.number(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
try {
|
try {
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
@@ -38,9 +47,57 @@ export async function POST(request: Request) {
|
|||||||
throw new Error(`n8n webhook failed: ${n8nResponse.statusText} - ${errorText}`)
|
throw new Error(`n8n webhook failed: ${n8nResponse.statusText} - ${errorText}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = await n8nResponse.json()
|
// n8n 返回数据
|
||||||
|
const n8nData = await n8nResponse.json()
|
||||||
|
|
||||||
return NextResponse.json(results)
|
// 兼容多种返回格式
|
||||||
|
let n8nResults: Array<{ id: string; similarity: number }>
|
||||||
|
if (Array.isArray(n8nData)) {
|
||||||
|
// 数组格式: [{ id: "...", similarity: 0.59 }]
|
||||||
|
n8nResults = N8NSearchResultSchema.parse(n8nData)
|
||||||
|
} else if (n8nData.id && typeof n8nData.similarity === 'number') {
|
||||||
|
// 单个对象格式: { id: "...", similarity: 0.59 }
|
||||||
|
n8nResults = [n8nData as { id: string; similarity: number }]
|
||||||
|
} else if (n8nData.data && Array.isArray(n8nData.data)) {
|
||||||
|
// 包装在 data 字段: { data: [{ id: "...", similarity: 0.59 }] }
|
||||||
|
n8nResults = N8NSearchResultSchema.parse(n8nData.data)
|
||||||
|
} else {
|
||||||
|
throw new Error('Invalid n8n response format')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (n8nResults.length === 0) {
|
||||||
|
return NextResponse.json({ projects: [], pagination: { total: 0, page: 1, limit: validatedQuery.limit || 20 } })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取所有项目 ID
|
||||||
|
const projectIds = n8nResults.map((r) => r.id)
|
||||||
|
|
||||||
|
// 从数据库批量获取完整项目数据
|
||||||
|
const projects = await getProjectsByIds(projectIds)
|
||||||
|
|
||||||
|
// 创建 similarity 映射表(ID -> similarity)
|
||||||
|
const similarityMap = new Map(n8nResults.map((r) => [r.id, r.similarity]))
|
||||||
|
|
||||||
|
// 将 similarity 合并到项目数据中,并按 n8n 返回的顺序排序
|
||||||
|
const results: AISearchResultItem[] = n8nResults
|
||||||
|
.map((n8nItem) => {
|
||||||
|
const project = projects.find((p) => p.id === n8nItem.id)
|
||||||
|
if (!project) return null
|
||||||
|
return {
|
||||||
|
...project,
|
||||||
|
similarity: n8nItem.similarity,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((item): item is AISearchResultItem => item !== null)
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
projects: results,
|
||||||
|
pagination: {
|
||||||
|
total: results.length,
|
||||||
|
page: 1,
|
||||||
|
limit: validatedQuery.limit || 20,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('AI search error:', error)
|
console.error('AI search error:', error)
|
||||||
|
|||||||
@@ -9,6 +9,11 @@ interface AISearchBarProps {
|
|||||||
searchLabel: string
|
searchLabel: string
|
||||||
aiPlaceholder: string
|
aiPlaceholder: string
|
||||||
aiLabel: string
|
aiLabel: string
|
||||||
|
toggleToAI: string
|
||||||
|
toggleToTraditional: string
|
||||||
|
searching: string
|
||||||
|
aiHint: string
|
||||||
|
traditionalHint: string
|
||||||
onSearch: (query: string, isAI: boolean) => void
|
onSearch: (query: string, isAI: boolean) => void
|
||||||
loading?: boolean
|
loading?: boolean
|
||||||
}
|
}
|
||||||
@@ -19,6 +24,11 @@ export function AISearchBar({
|
|||||||
searchLabel,
|
searchLabel,
|
||||||
aiPlaceholder,
|
aiPlaceholder,
|
||||||
aiLabel,
|
aiLabel,
|
||||||
|
toggleToAI,
|
||||||
|
toggleToTraditional,
|
||||||
|
searching,
|
||||||
|
aiHint,
|
||||||
|
traditionalHint,
|
||||||
onSearch,
|
onSearch,
|
||||||
loading = false,
|
loading = false,
|
||||||
}: AISearchBarProps) {
|
}: AISearchBarProps) {
|
||||||
@@ -34,7 +44,6 @@ export function AISearchBar({
|
|||||||
|
|
||||||
const toggleAIMode = () => {
|
const toggleAIMode = () => {
|
||||||
setAiMode(!aiMode)
|
setAiMode(!aiMode)
|
||||||
setQuery('')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -55,40 +64,43 @@ export function AISearchBar({
|
|||||||
value={query}
|
value={query}
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
placeholder={aiMode ? aiPlaceholder : searchPlaceholder}
|
placeholder={aiMode ? aiPlaceholder : searchPlaceholder}
|
||||||
className="block w-full pl-12 pr-40 py-4 bg-surface-light dark:bg-surface-dark border-2 border-black dark:border-gray-600 text-text-light dark:text-text-dark placeholder-gray-500 focus:ring-0 focus:border-black dark:focus:border-primary font-display shadow-neo transition-all"
|
className="block w-full pl-12 pr-48 py-4 bg-surface-light dark:bg-surface-dark border-2 border-black dark:border-gray-600 text-text-light dark:text-text-dark placeholder-gray-500 focus:ring-0 focus:border-black dark:focus:border-primary font-display shadow-neo transition-all"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* AI Mode Toggle */}
|
{/* Buttons Container */}
|
||||||
<button
|
<div className="absolute inset-y-2 right-2 flex items-center gap-2">
|
||||||
type="button"
|
{/* AI Mode Toggle */}
|
||||||
onClick={toggleAIMode}
|
<button
|
||||||
className={`
|
type="button"
|
||||||
absolute inset-y-2 right-24 px-3 py-2 font-display font-bold text-sm border-2 transition-all shadow-neo-sm active:shadow-none active:translate-x-[2px] active:translate-y-[2px]
|
onClick={toggleAIMode}
|
||||||
${aiMode
|
className={`
|
||||||
? 'bg-primary text-black border-black hover:bg-yellow-400'
|
px-3 py-2 font-display font-bold text-sm border-2 transition-all shadow-neo-sm active:shadow-none active:translate-x-[2px] active:translate-y-[2px] whitespace-nowrap min-w-[44px]
|
||||||
: 'bg-white dark:bg-surface-dark text-gray-600 dark:text-gray-400 border-gray-300 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-white/10'
|
${aiMode
|
||||||
}
|
? 'bg-primary text-black border-black hover:bg-yellow-400'
|
||||||
`}
|
: 'bg-white dark:bg-surface-dark text-gray-600 dark:text-gray-400 border-gray-300 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-white/10'
|
||||||
title={aiMode ? '切换到传统搜索' : '切换到 AI 搜索'}
|
}
|
||||||
>
|
`}
|
||||||
<Sparkles className="w-4 h-4" />
|
title={aiMode ? toggleToTraditional : toggleToAI}
|
||||||
</button>
|
>
|
||||||
|
<Sparkles className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
{/* Search button */}
|
{/* Search button */}
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading || !query.trim()}
|
disabled={loading || !query.trim()}
|
||||||
className="absolute inset-y-2 right-2 px-4 bg-black dark:bg-primary text-white dark:text-black font-bold font-display text-sm border-2 border-black dark:border-primary hover:bg-gray-800 dark:hover:bg-yellow-400 disabled:opacity-50 disabled:cursor-not-allowed transition-colors shadow-neo-sm active:shadow-none active:translate-x-[2px] active:translate-y-[2px]"
|
className="px-4 py-2 bg-black dark:bg-primary text-white dark:text-black font-bold font-display text-sm border-2 border-black dark:border-primary hover:bg-gray-800 dark:hover:bg-yellow-400 disabled:opacity-50 disabled:cursor-not-allowed transition-colors shadow-neo-sm active:shadow-none active:translate-x-[2px] active:translate-y-[2px] whitespace-nowrap"
|
||||||
>
|
>
|
||||||
{loading ? '搜索中...' : (aiMode ? aiLabel : searchLabel)}
|
{loading ? searching : (aiMode ? aiLabel : searchLabel)}
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* AI Mode Hint */}
|
{/* AI Mode Hint */}
|
||||||
{aiMode && (
|
{aiMode && (
|
||||||
<div className="mt-3 text-sm text-gray-600 dark:text-gray-400 font-display">
|
<div className="mt-3 text-sm text-gray-600 dark:text-gray-400 font-display">
|
||||||
💡 {aiMode ? '试试:"帮我找能生成视频的 AI 工具"' : '输入项目名称或描述'}
|
💡 {aiHint}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { AISearchBar } from './AISearchBar'
|
||||||
|
|
||||||
|
interface HomeSearchBarProps {
|
||||||
|
locale: string
|
||||||
|
searchPlaceholder: string
|
||||||
|
searchLabel: string
|
||||||
|
aiPlaceholder: string
|
||||||
|
aiLabel: string
|
||||||
|
toggleToAI: string
|
||||||
|
toggleToTraditional: string
|
||||||
|
searching: string
|
||||||
|
aiHint: string
|
||||||
|
traditionalHint: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HomeSearchBar({
|
||||||
|
locale,
|
||||||
|
searchPlaceholder,
|
||||||
|
searchLabel,
|
||||||
|
aiPlaceholder,
|
||||||
|
aiLabel,
|
||||||
|
toggleToAI,
|
||||||
|
toggleToTraditional,
|
||||||
|
searching,
|
||||||
|
aiHint,
|
||||||
|
traditionalHint,
|
||||||
|
}: HomeSearchBarProps) {
|
||||||
|
const handleSearch = (query: string, isAI: boolean) => {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (query) params.set('search', query)
|
||||||
|
if (isAI) params.set('ai', '1')
|
||||||
|
window.location.href = `/${locale}/projects?${params.toString()}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AISearchBar
|
||||||
|
locale={locale}
|
||||||
|
searchPlaceholder={searchPlaceholder}
|
||||||
|
searchLabel={searchLabel}
|
||||||
|
aiPlaceholder={aiPlaceholder}
|
||||||
|
aiLabel={aiLabel}
|
||||||
|
toggleToAI={toggleToAI}
|
||||||
|
toggleToTraditional={toggleToTraditional}
|
||||||
|
searching={searching}
|
||||||
|
aiHint={aiHint}
|
||||||
|
traditionalHint={traditionalHint}
|
||||||
|
onSearch={handleSearch}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -181,3 +181,47 @@ export async function getTopTags(limit: number = 10): Promise<TagWithProjectCoun
|
|||||||
})
|
})
|
||||||
.slice(0, limit)
|
.slice(0, limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 定义 AI 搜索结果类型
|
||||||
|
export type AISearchResultItem = Omit<ProjectWithFlatTags, 'status'> & {
|
||||||
|
similarity: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// n8n 返回的简化搜索结果类型
|
||||||
|
export type N8NSearchResult = {
|
||||||
|
id: string
|
||||||
|
similarity: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 ID 列表批量获取项目(用于 AI 搜索结果组装)
|
||||||
|
* @param ids 项目 ID 列表
|
||||||
|
* @returns 带有相似度分数的项目列表
|
||||||
|
*/
|
||||||
|
export async function getProjectsByIds(ids: string[]): Promise<ProjectWithFlatTags[]> {
|
||||||
|
if (ids.length === 0) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const projects = await prisma.project.findMany({
|
||||||
|
where: {
|
||||||
|
id: {
|
||||||
|
in: ids,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
tags: {
|
||||||
|
include: {
|
||||||
|
tag: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
links: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Transform tags to flatten the structure
|
||||||
|
return projects.map((project) => ({
|
||||||
|
...project,
|
||||||
|
tags: project.tags.map((pt) => pt.tag),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,11 @@
|
|||||||
"common": {
|
"common": {
|
||||||
"search": "Search",
|
"search": "Search",
|
||||||
"aiSearch": "AI Search",
|
"aiSearch": "AI Search",
|
||||||
|
"searching": "Searching...",
|
||||||
|
"toggleToAI": "Switch to AI Search",
|
||||||
|
"toggleToTraditional": "Switch to Traditional Search",
|
||||||
|
"aiHint": "Try: \"Find AI tools that can generate videos\"",
|
||||||
|
"traditionalHint": "Enter project name or description",
|
||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
"noResults": "No results found",
|
"noResults": "No results found",
|
||||||
"noProjects": "No projects yet",
|
"noProjects": "No projects yet",
|
||||||
|
|||||||
@@ -2,6 +2,11 @@
|
|||||||
"common": {
|
"common": {
|
||||||
"search": "搜索",
|
"search": "搜索",
|
||||||
"aiSearch": "AI 搜索",
|
"aiSearch": "AI 搜索",
|
||||||
|
"searching": "搜索中...",
|
||||||
|
"toggleToAI": "切换到 AI 搜索",
|
||||||
|
"toggleToTraditional": "切换到传统搜索",
|
||||||
|
"aiHint": "试试:\"帮我找能生成视频的 AI 工具\"",
|
||||||
|
"traditionalHint": "输入项目名称或描述",
|
||||||
"loading": "加载中...",
|
"loading": "加载中...",
|
||||||
"noResults": "未找到结果",
|
"noResults": "未找到结果",
|
||||||
"noProjects": "暂无项目",
|
"noProjects": "暂无项目",
|
||||||
|
|||||||
Reference in New Issue
Block a user