码桶
发现社区成员的开源项目
SmartPage.tsx9.5 KB
import { useEffect, useState } from 'react'
import { api } from '@/lib/api'
import { useAuth } from '@/lib/auth'
import { Loader2, Sparkles, Check, Smile, Flame, Zap, Coffee, Briefcase, Heart, Crown } from 'lucide-react'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Switch } from '@/components/ui/switch'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { cn } from '@/lib/utils'
import { toast } from 'sonner'
import MemoryPanel from '@/pages/MemoryPanel'
const PERSONAS = [
{ id: '', name: '默认', desc: '无特殊风格,自然得体', icon: Smile, color: 'text-muted-foreground', bg: 'bg-muted/50' },
{ id: 'warm', name: '温柔体贴', desc: '像知心朋友,多用"呢""呀""哦",让人感到温暖和被理解', icon: Coffee, color: 'text-amber-500', bg: 'bg-amber-50' },
{ id: 'cool', name: '高冷干练', desc: '技术大佬风,一针见血少废话,偶尔冷幽默', icon: Zap, color: 'text-sky-500', bg: 'bg-sky-50' },
{ id: 'humorous', name: '幽默风趣', desc: '段子手朋友,俏皮话谐音梗,让人开心放松', icon: Flame, color: 'text-orange-500', bg: 'bg-orange-50' },
{ id: 'professional', name: '专业严谨', desc: '资深顾问风,条理清晰有逻辑,像和客户沟通', icon: Briefcase, color: 'text-indigo-500', bg: 'bg-indigo-50' },
{ id: 'cute', name: '软萌可爱', desc: '暖心小萌宠,叠词语气词治愈系', icon: Heart, color: 'text-pink-500', bg: 'bg-pink-50' },
{ id: 'otaku', name: '二次元萌系', desc: '元气动漫伙伴,"喵""捏""ww""诶嘿"', icon: Sparkles, color: 'text-purple-500', bg: 'bg-purple-50' },
{ id: 'tsundere', name: '傲娇系', desc: '嘴上不饶人但内心关心,"才…才不是因为关心你呢!"', icon: Crown, color: 'text-rose-500', bg: 'bg-rose-50' },
]
/** 账号级 AI 风格设置(非按机器人) */
function PersonaPanel() {
const { me, refresh } = useAuth()
const [selected, setSelected] = useState('')
const [saving, setSaving] = useState(false)
const [loading, setLoading] = useState(true)
useEffect(() => {
if (me) {
setSelected(me.ai_persona || '')
setLoading(false)
}
}, [me])
async function save() {
setSaving(true)
try {
const d = await api.post('/api/persona', { persona: selected })
if (!d.ok) throw new Error(d.msg || '保存失败')
toast.success('AI 风格已保存')
await refresh()
} catch (e: any) {
toast.error(e.message || '保存失败')
} finally {
setSaving(false)
}
}
if (loading) {
return (
<div className="flex h-64 items-center justify-center">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
)
}
return (
<div className="space-y-4">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base">AI 风格 / 人格</CardTitle>
<CardDescription>选择 AI 助手说话风格,所有机器人统一使用此人格,切换后新对话立即生效。</CardDescription>
</CardHeader>
<CardContent>
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{PERSONAS.map(p => {
const Icon = p.icon
const isActive = selected === p.id
return (
<Card
key={p.id}
className={cn(
'cursor-pointer transition-all hover:shadow-md',
isActive && 'ring-2 ring-primary border-primary'
)}
onClick={() => setSelected(p.id)}
>
<CardHeader className="pb-2">
<div className="flex items-start justify-between">
<div className={cn('flex h-10 w-10 items-center justify-center rounded-lg', p.bg, p.color)}>
<Icon className="h-5 w-5" />
</div>
{isActive && (
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-primary text-primary-foreground">
<Check className="h-3.5 w-3.5" />
</div>
)}
</div>
<CardTitle className="mt-2 text-base">{p.name}</CardTitle>
<CardDescription className="text-xs leading-relaxed">{p.desc}</CardDescription>
</CardHeader>
</Card>
)
})}
</div>
<div className="mt-6 flex items-center justify-between border-t pt-4">
<p className="text-xs text-muted-foreground">
当前:<span className="font-medium text-foreground">{PERSONAS.find(p => p.id === selected)?.name || '默认'}</span>
</p>
<Button onClick={save} disabled={saving}>
{saving ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
保存设置
</Button>
</div>
</CardContent>
</Card>
</div>
)
}
export default function SmartPage() {
const [bots, setBots] = useState<any[]>([])
const [selBotId, setSelBotId] = useState<number>(0)
useEffect(() => {
api.post('/api/me').then((d: any) => {
if (d?.ok) {
const bs: any[] = d.bots || []
setBots(bs)
if (bs[0]) setSelBotId(bs[0].id)
}
}).catch(() => {})
}, [])
return (
<div className="mx-auto max-w-none space-y-4 p-4 md:p-6">
<div className="flex items-center gap-2">
<Sparkles className="h-6 w-6 text-primary" />
<h1 className="text-xl font-semibold">智能助手</h1>
<span className="text-xs text-muted-foreground">内置 AI 助手 · 始终运行</span>
</div>
<Tabs defaultValue="model" className="w-full">
<TabsList>
<TabsTrigger value="model">模型设置</TabsTrigger>
<TabsTrigger value="memory">用户记忆</TabsTrigger>
<TabsTrigger value="companion">主动陪伴</TabsTrigger>
</TabsList>
<TabsContent value="model" className="space-y-4">
{selBotId ? (
<>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-base">AI 模型</CardTitle>
<CardDescription className="text-xs">
智能助手直接使用站点全局 AI 配置(接口、模型、图片生成等)。请在左侧「AI 配置」菜单中设置,无需在此单独配置。
</CardDescription>
</CardHeader>
</Card>
<PersonaPanel />
</>
) : (
<div className="flex items-center justify-center gap-2 py-10 text-sm text-muted-foreground">
<Loader2 className="h-4 w-4 animate-spin" /> 加载机器人中…
</div>
)}
</TabsContent>
<TabsContent value="memory">
<MemoryPanel />
</TabsContent>
<TabsContent value="companion">
<CompanionTab />
</TabsContent>
</Tabs>
</div>
)
}
/** 主动陪伴:用户主动开启后,Bot 会在白天随机发来问候或分享有趣内容 */
function CompanionTab() {
const { me, refresh } = useAuth()
const [enabled, setEnabled] = useState<boolean>(!!me?.companion_enabled)
const [saving, setSaving] = useState(false)
useEffect(() => {
setEnabled(!!me?.companion_enabled)
}, [me?.companion_enabled])
const toggle = async (v: boolean) => {
setSaving(true)
try {
const r = await api.post('/api/companion', { enabled: v })
if (r.ok) {
setEnabled(!!r.companion_enabled)
await refresh()
toast.success(v ? '已开启主动陪伴 💬' : '已关闭主动陪伴')
} else {
toast.error('设置失败:' + (r.msg || ''))
}
} catch {
toast.error('网络错误,请稍后再试')
} finally {
setSaving(false)
}
}
return (
<Card>
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2 text-base">
<Heart className="h-4 w-4 text-rose-500" /> 主动陪伴
</CardTitle>
<CardDescription className="leading-relaxed">
开启后,我会在白天(9:00–23:00)偶尔给你发一条朋友式的问候,
或者顺手分享一个有趣的内容(如金价、每日简报等),就像朋友偶尔的惦记~
同一用户至少间隔 4 小时才可能再收到,绝不会频繁打扰。
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<div className="text-sm font-medium">开启主动陪伴</div>
<div className="text-xs text-muted-foreground">
默认关闭,需你主动开启。关闭后不再主动发消息。
</div>
</div>
<Switch checked={enabled} onCheckedChange={toggle} disabled={saving} />
</div>
<div className="mt-3 text-xs text-muted-foreground">
当前状态:{enabled ? '已开启 · 我会偶尔来找你聊天 💬' : '已关闭'}
</div>
</CardContent>
</Card>
)
}