feat: P3b 会话重命名 (PATCH + 双击编辑)
This commit is contained in:
@@ -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
@@ -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); }}
|
||||||
|
|||||||
Reference in New Issue
Block a user