feat: P3b 会话重命名 (PATCH + 双击编辑)

This commit is contained in:
yuzhiran-dev
2026-05-18 12:08:32 +08:00
parent 4de042abcd
commit ed003d0ef0
4 changed files with 43 additions and 4 deletions
@@ -46,4 +46,9 @@ export class SandboxController {
async share(@Req() req: any, @Param('id', ParseIntPipe) id: number) { async share(@Req() req: any, @Param('id', ParseIntPipe) id: number) {
return this.sandboxService.generateShareToken(req.user.userId, id); return this.sandboxService.generateShareToken(req.user.userId, id);
} }
@Patch('sessions/:id/rename')
async rename(@Req() req: any, @Param('id', ParseIntPipe) id: number, @Body() body: { title: string }) {
return this.sandboxService.renameSession(req.user.userId, id, body.title);
}
} }
@@ -126,6 +126,15 @@ export class SandboxService {
return { success: true }; return { success: true };
} }
async renameSession(userId: number, id: number, title: string) {
const session = await this.prisma.sandboxSession.findFirst({
where: { id, userId },
});
if (!session) throw new HttpException('会话不存在', HttpStatus.NOT_FOUND);
await this.prisma.sandboxSession.update({ where: { id }, data: { title } });
return { success: true };
}
async getHistory(userId: number, params: { page?: number; pageSize?: number }) { async getHistory(userId: number, params: { page?: number; pageSize?: number }) {
const page = Number(params.page ?? 1); const page = Number(params.page ?? 1);
const pageSize = Number(params.pageSize ?? 20); const pageSize = Number(params.pageSize ?? 20);
File diff suppressed because one or more lines are too long
+27 -2
View File
@@ -73,6 +73,8 @@ function SandboxPage() {
const [conversationId, setConversationId] = useState(() => crypto.randomUUID()); const [conversationId, setConversationId] = useState(() => crypto.randomUUID());
const [sessionFeedback, setSessionFeedback] = useState<Record<number, string | null>>({}); const [sessionFeedback, setSessionFeedback] = useState<Record<number, string | null>>({});
const [currentSessionId, setCurrentSessionId] = useState<number | null>(null); const [currentSessionId, setCurrentSessionId] = useState<number | null>(null);
const [renamingId, setRenamingId] = useState<number | null>(null);
const [renameValue, setRenameValue] = useState('');
const messagesEndRef = useRef<HTMLDivElement>(null); const messagesEndRef = useRef<HTMLDivElement>(null);
const messagesContainerRef = useRef<HTMLDivElement>(null); const messagesContainerRef = useRef<HTMLDivElement>(null);
const { isLoggedIn } = useAuth(); const { isLoggedIn } = useAuth();
@@ -212,6 +214,20 @@ function SandboxPage() {
} catch {} } catch {}
} }
async function renameSession(sessionId: number, title: string) {
const tk = getToken();
if (!tk || !title.trim()) return;
try {
await fetch(`${API_BASE}/sandbox/sessions/${sessionId}/rename`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${tk}` },
body: JSON.stringify({ title: title.trim() }),
});
setSessions(prev => prev.map(s => s.id === sessionId ? { ...s, title: title.trim() } : s));
setRenamingId(null);
} catch {}
}
function newChat() { function newChat() {
setConversationId(crypto.randomUUID()); setConversationId(crypto.randomUUID());
setCurrentSessionId(null); setCurrentSessionId(null);
@@ -322,9 +338,18 @@ function SandboxPage() {
{sessions.length === 0 ? ( {sessions.length === 0 ? (
<div className="p-4 text-center text-xs text-muted-foreground">{t.sandbox.noHistory}</div> <div className="p-4 text-center text-xs text-muted-foreground">{t.sandbox.noHistory}</div>
) : sessions.map(s => ( ) : sessions.map(s => (
<div key={s.id} onClick={() => { loadSession(s.id); setSessionsOpen(false); }} <div key={s.id} onClick={() => { if (renamingId !== s.id) { loadSession(s.id); setSessionsOpen(false); } }}
className="group px-3 py-2.5 hover:bg-accent cursor-pointer border-b border-border/50"> className="group px-3 py-2.5 hover:bg-accent cursor-pointer border-b border-border/50"
onDoubleClick={() => { setRenamingId(s.id); setRenameValue(s.title); }}>
{renamingId === s.id ? (
<input type="text" value={renameValue} onChange={e => setRenameValue(e.target.value)}
autoFocus onClick={e => e.stopPropagation()}
onBlur={() => renameSession(s.id, renameValue)}
onKeyDown={e => { if (e.key === 'Enter') { e.stopPropagation(); renameSession(s.id, renameValue); } if (e.key === 'Escape') setRenamingId(null); }}
className="w-full px-1 py-0.5 text-xs font-medium bg-background border border-border rounded focus:outline-none" />
) : (
<div className="text-xs font-medium text-foreground truncate">{s.title}</div> <div className="text-xs font-medium text-foreground truncate">{s.title}</div>
)}
<div className="flex items-center justify-between mt-1"> <div className="flex items-center justify-between mt-1">
<span className="text-[10px] text-muted-foreground">{formatTime(s.updatedAt)} · {s.model}</span> <span className="text-[10px] text-muted-foreground">{formatTime(s.updatedAt)} · {s.model}</span>
<button onClick={e => { e.stopPropagation(); deleteSession(s.id); }} <button onClick={e => { e.stopPropagation(); deleteSession(s.id); }}