diff --git a/src/app/api/search/ai/route.ts b/src/app/api/search/ai/route.ts
index 62bfd5a..5097063 100644
--- a/src/app/api/search/ai/route.ts
+++ b/src/app/api/search/ai/route.ts
@@ -1,6 +1,7 @@
import { NextResponse } from 'next/server'
-import { ZodError } from 'zod'
+import { ZodError, z } from 'zod'
import { ProjectQuerySchema } from '@/lib/validations'
+import { getProjectsByIds, type AISearchResultItem } from '@/hooks/useProjects'
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')
}
+// n8n 返回的简化搜索结果 Schema
+const N8NSearchResultSchema = z.array(
+ z.object({
+ id: z.string(),
+ similarity: z.number(),
+ })
+)
+
export async function POST(request: Request) {
try {
const body = await request.json()
@@ -38,9 +47,57 @@ export async function POST(request: Request) {
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) {
console.error('AI search error:', error)
diff --git a/src/components/search/AISearchBar.tsx b/src/components/search/AISearchBar.tsx
index bc2924a..c9f32a6 100644
--- a/src/components/search/AISearchBar.tsx
+++ b/src/components/search/AISearchBar.tsx
@@ -9,6 +9,11 @@ interface AISearchBarProps {
searchLabel: string
aiPlaceholder: string
aiLabel: string
+ toggleToAI: string
+ toggleToTraditional: string
+ searching: string
+ aiHint: string
+ traditionalHint: string
onSearch: (query: string, isAI: boolean) => void
loading?: boolean
}
@@ -19,6 +24,11 @@ export function AISearchBar({
searchLabel,
aiPlaceholder,
aiLabel,
+ toggleToAI,
+ toggleToTraditional,
+ searching,
+ aiHint,
+ traditionalHint,
onSearch,
loading = false,
}: AISearchBarProps) {
@@ -34,7 +44,6 @@ export function AISearchBar({
const toggleAIMode = () => {
setAiMode(!aiMode)
- setQuery('')
}
return (
@@ -55,40 +64,43 @@ export function AISearchBar({
value={query}
onChange={(e) => setQuery(e.target.value)}
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 */}
+
+ {/* AI Mode Toggle */}
+
- {/* Search button */}
-
+ {/* Search button */}
+
+
{/* AI Mode Hint */}
{aiMode && (
- 💡 {aiMode ? '试试:"帮我找能生成视频的 AI 工具"' : '输入项目名称或描述'}
+ 💡 {aiHint}
)}
diff --git a/src/components/search/HomeSearchBar.tsx b/src/components/search/HomeSearchBar.tsx
new file mode 100644
index 0000000..329a61f
--- /dev/null
+++ b/src/components/search/HomeSearchBar.tsx
@@ -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 (
+