feat: sync Sidebar and BottomNav, standardize user profile API

Align Sidebar & BottomNav menus, remove "Search", add user profile mock data, implement /api/users, add FilterDrawer, complete Section, ProfileHeader, MetricsRFM components

Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
This commit is contained in:
v0
2025-08-08 07:00:12 +00:00
parent 4eed69520c
commit f0a6a364f2
85 changed files with 3318 additions and 6786 deletions

View File

@@ -0,0 +1,56 @@
import { NextResponse } from "next/server"
import { analyzeUser, type AnalyzeInput } from "@/services/rfm-engine"
import { generateText } from "ai"
import { openai } from "@ai-sdk/openai"
/**
* 可选 AI 标签增强:
* - 使用 AI SDK (generateText + openai("gpt-4o")),符合统一标准 [^1]
* - 无 OPENAI_API_KEY 时自动跳过,保持稳定
*/
async function aiTagging(chat_logs?: string[]) {
const text = (chat_logs ?? []).slice(0, 8).join("。")
if (!text) return null
if (!process.env.OPENAI_API_KEY) return null
try {
const { text: out } = await generateText({
model: openai("gpt-4o"),
system:
"请根据用户聊天片段输出JSON字段emotion(积极|中性|消极), intent(弱意图|中等意图|强意图), behavior(数组中文标签最多3个)。只输出JSON。",
prompt: `聊天片段:${text}`,
// @ts-expect-error ai sdk typings are flexible in Next.js
maxTokens: 200,
})
const json = JSON.parse(out.trim())
return json
} catch {
return null
}
}
export async function POST(req: Request) {
try {
const body = await req.json()
const inputs: AnalyzeInput[] = Array.isArray(body) ? body : [body]
const useAI = (Array.isArray(body) ? (body as any).useAI : (body as any)?.useAI) ?? false
const results = []
for (const input of inputs) {
const base = analyzeUser(input)
if (useAI) {
const ai = await aiTagging(input.chat_logs)
if (ai) {
base.tags.emotion = ai.emotion ?? base.tags.emotion
base.tags.intent = ai.intent ?? base.tags.intent
if (Array.isArray(ai.behavior)) {
base.tags.behavior = Array.from(new Set([...(base.tags.behavior ?? []), ...ai.behavior]))
}
}
}
results.push(base)
}
return NextResponse.json({ success: true, data: results })
} catch (e: any) {
return NextResponse.json({ success: false, error: e?.message || "Invalid input" }, { status: 400 })
}
}

View File

@@ -0,0 +1,13 @@
import { NextResponse } from "next/server"
import { dumpCsv } from "@/services/rfm-engine"
export async function GET() {
const csv = dumpCsv()
return new NextResponse(csv, {
status: 200,
headers: {
"Content-Type": "text/csv; charset=utf-8",
"Content-Disposition": `attachment; filename="rfm_export_${Date.now()}.csv"`,
},
})
}

View File

@@ -0,0 +1,12 @@
import { NextResponse } from "next/server"
import { getUserTags } from "@/services/rfm-engine"
export async function GET(req: Request) {
const url = new URL(req.url)
const userId = url.searchParams.get("user_id")
if (!userId) {
return NextResponse.json({ success: false, error: "missing user_id" }, { status: 400 })
}
const data = getUserTags(userId)
return NextResponse.json({ success: true, data })
}

View File

@@ -0,0 +1,7 @@
import { NextResponse } from "next/server"
import { getGroupSummary } from "@/services/rfm-engine"
export async function GET() {
const data = getGroupSummary()
return NextResponse.json({ success: true, data })
}

View File

@@ -0,0 +1,12 @@
import { NextResponse } from "next/server"
import { computeRFM, type AnalyzeInput } from "@/services/rfm-engine"
export async function POST(req: Request) {
try {
const body = (await req.json()) as AnalyzeInput
const score = computeRFM(body)
return NextResponse.json({ success: true, data: score })
} catch (e: any) {
return NextResponse.json({ success: false, error: e?.message || "Invalid input" }, { status: 400 })
}
}

View File

@@ -0,0 +1,26 @@
import { NextResponse } from "next/server"
import { getWeights, setWeights } from "@/services/rfm-engine"
export async function GET() {
return NextResponse.json({ success: true, data: getWeights() })
}
export async function POST(req: Request) {
try {
const body = await req.json()
let R = Number(body.R ?? 0.5)
let F = Number(body.F ?? 0.3)
let M = Number(body.M ?? 0.2)
// 归一化
const sum = Math.max(0.000001, R + F + M)
R = R / sum
F = F / sum
M = M / sum
setWeights({ R, F, M })
return NextResponse.json({ success: true, data: { R, F, M } })
} catch (e: any) {
return NextResponse.json({ success: false, error: e?.message || "Invalid input" }, { status: 400 })
}
}