538de50bb1
- Prisma User 模型新增 username 字段(唯一索引) - 注册先查重复再创建,返回友好中文提示(非 500) - 登录支持用户名/手机号/邮箱三种方式 - 前端注册表单增加用户名输入框,预校验 2-20 位格式 - 新增 scripts/deploy.sh:一键构建并部署主站+镜像站+重启后端+重载 Nginx - 镜像站 www.yuzhiran.com.cn Nginx 配置与主站同步 - AI 助手架构升级:用户端/管理后台均采用完整 Tool Calling 架构 - 新增 UserAiAssistantService(18 工具)+ AiAssistantController - admin 助手新增 search + mark-all-notifications-read 工具 - 修复注册 500 错误:catch Prisma P2002 → BadRequestException - Baidu Analytics Script 注入 root layout
102 lines
3.5 KiB
TypeScript
Executable File
102 lines
3.5 KiB
TypeScript
Executable File
'use client';
|
|
|
|
import { Suspense, useEffect, useState } from 'react';
|
|
import Link from 'next/link';
|
|
import { useSearchParams } from 'next/navigation';
|
|
import { Skeleton } from '@/components/ui/skeleton';
|
|
import { API_BASE } from '@/lib/config';
|
|
|
|
interface SharedMessage {
|
|
role: string;
|
|
content: string;
|
|
}
|
|
|
|
interface SharedSession {
|
|
id: number;
|
|
model: string;
|
|
title: string;
|
|
createdAt: string;
|
|
messages: SharedMessage[];
|
|
}
|
|
|
|
function SharedSessionInner() {
|
|
const searchParams = useSearchParams();
|
|
const token = searchParams.get('token');
|
|
const [session, setSession] = useState<SharedSession | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState('');
|
|
|
|
useEffect(() => {
|
|
if (!token) { setError('缺少分享参数'); setLoading(false); return; }
|
|
fetch(`${API_BASE}/sandbox/shared/${encodeURIComponent(token)}`)
|
|
.then(r => { if (!r.ok) throw new Error('分享链接无效'); return r.json(); })
|
|
.then(data => setSession(data))
|
|
.catch(e => setError(e.message))
|
|
.finally(() => setLoading(false));
|
|
}, [token]);
|
|
|
|
if (loading) return (
|
|
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
|
<Skeleton className="h-8 w-48 mb-6" />
|
|
<Skeleton className="h-4 w-72 mb-8" />
|
|
<Skeleton className="h-64 w-full rounded-2xl" />
|
|
</div>
|
|
);
|
|
|
|
if (error || !session) return (
|
|
<div className="max-w-3xl mx-auto px-4 py-20 text-center">
|
|
<div className="text-4xl mb-4">🔗</div>
|
|
<h1 className="text-xl font-bold text-foreground mb-2">分享内容不可用</h1>
|
|
<p className="text-muted-foreground mb-6">{error || '该分享链接可能已过期或不存在'}</p>
|
|
<Link href="/sandbox" className="px-4 py-2 bg-brand-600 text-white rounded-lg text-sm hover:bg-brand-700">
|
|
前往 AI 沙盒
|
|
</Link>
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
|
<div className="mb-8">
|
|
<Link href="/sandbox" className="text-sm text-muted-foreground hover:text-brand-600 mb-2 inline-block">
|
|
← AI 沙盒
|
|
</Link>
|
|
<h1 className="text-2xl font-bold text-foreground">{session.title}</h1>
|
|
<p className="text-sm text-muted-foreground mt-1">
|
|
模型: {session.model} · {new Date(session.createdAt).toLocaleDateString()}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
{session.messages.map((msg, i) => (
|
|
<div key={i} className={`flex items-start gap-3 ${msg.role === 'user' ? 'justify-end' : ''}`}>
|
|
{msg.role === 'assistant' && (
|
|
<div className="w-8 h-8 bg-brand-600 rounded-xl flex items-center justify-center text-white text-sm font-bold shrink-0">Y</div>
|
|
)}
|
|
<div className={`max-w-[80%] rounded-2xl px-4 py-2.5 text-sm leading-relaxed whitespace-pre-wrap ${
|
|
msg.role === 'user' ? 'bg-brand-600 text-white' : 'bg-muted text-foreground'
|
|
}`}>
|
|
{msg.content}
|
|
</div>
|
|
{msg.role === 'user' && (
|
|
<div className="w-8 h-8 bg-muted-foreground/20 rounded-xl flex items-center justify-center text-xs font-bold shrink-0">我</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function SharedSessionPage() {
|
|
return (
|
|
<Suspense fallback={
|
|
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
|
<Skeleton className="h-8 w-48 mb-6" />
|
|
<Skeleton className="h-64 w-full rounded-2xl" />
|
|
</div>
|
|
}>
|
|
<SharedSessionInner />
|
|
</Suspense>
|
|
);
|
|
}
|