feat: add ConversationDeleteDialog and ConversationRenameDialog components with integration in HomePage
This commit is contained in:
112
src/pages/Home/components/ConversationDeleteDialog.tsx
Normal file
112
src/pages/Home/components/ConversationDeleteDialog.tsx
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import * as Dialog from '@radix-ui/react-dialog';
|
||||||
|
import { Loader2, Trash2, X } from 'lucide-react';
|
||||||
|
|
||||||
|
type ConversationDeleteDialogProps = {
|
||||||
|
open: boolean;
|
||||||
|
busy: boolean;
|
||||||
|
conversationTitle: string;
|
||||||
|
onClose: () => void;
|
||||||
|
onConfirm: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ConversationDeleteDialog({
|
||||||
|
open,
|
||||||
|
busy,
|
||||||
|
conversationTitle,
|
||||||
|
onClose,
|
||||||
|
onConfirm,
|
||||||
|
}: ConversationDeleteDialogProps) {
|
||||||
|
function handleOpenChange(nextOpen: boolean) {
|
||||||
|
if (!nextOpen && !busy) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog.Root open={open} onOpenChange={handleOpenChange}>
|
||||||
|
<Dialog.Portal>
|
||||||
|
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/45 backdrop-blur-[2px]" />
|
||||||
|
<Dialog.Content
|
||||||
|
className="fixed left-1/2 top-1/2 z-60 w-[calc(100vw-32px)] max-w-140 -translate-x-1/2 -translate-y-1/2 rounded-3xl bg-[#F4F3EB] p-0 shadow-[0_30px_80px_rgba(15,23,42,0.18)] outline-none dark:bg-[#1f1f22]"
|
||||||
|
onEscapeKeyDown={(event) => {
|
||||||
|
if (busy) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onPointerDownOutside={(event) => {
|
||||||
|
if (busy) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between gap-4 border-b border-black/6 px-6 py-5 dark:border-white/6">
|
||||||
|
<div className="flex min-w-0 items-start gap-4">
|
||||||
|
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-2xl border border-red-500/15 bg-red-500/10 text-red-600 dark:border-red-400/20 dark:bg-red-500/10 dark:text-red-300">
|
||||||
|
<Trash2 className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="min-w-0">
|
||||||
|
<Dialog.Title
|
||||||
|
className="text-[26px] font-normal leading-none tracking-tight text-[#171717] dark:text-[#f3f4f6]"
|
||||||
|
style={{ fontFamily: "Georgia, Cambria, 'Times New Roman', Times, serif" }}
|
||||||
|
>
|
||||||
|
删除会话
|
||||||
|
</Dialog.Title>
|
||||||
|
<Dialog.Description className="mt-3 text-[14px] leading-6 text-[#525866] dark:text-gray-400">
|
||||||
|
删除后将无法恢复,请确认是否继续。
|
||||||
|
</Dialog.Description>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="rounded-full p-1.5 text-[#99A0AE] transition-colors hover:text-[#171717] disabled:cursor-not-allowed disabled:opacity-50 dark:hover:text-[#f3f4f6]"
|
||||||
|
onClick={onClose}
|
||||||
|
disabled={busy}
|
||||||
|
aria-label="关闭弹窗"
|
||||||
|
>
|
||||||
|
<X className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-6 py-5">
|
||||||
|
<div className="rounded-[18px] border border-black/6 bg-white/75 px-4 py-3 shadow-[0_10px_30px_rgba(15,23,42,0.04)] dark:border-white/8 dark:bg-[#202024]">
|
||||||
|
<div className="text-[12px] uppercase tracking-[0.14em] text-[#99A0AE] dark:text-gray-500">
|
||||||
|
会话标题
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 truncate text-[15px] font-medium text-[#171717] dark:text-[#f3f4f6]">
|
||||||
|
{conversationTitle}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-5 flex items-center justify-end gap-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex h-10 items-center rounded-full border border-black/10 px-4 text-[13px] font-medium text-[#171717]/80 transition-colors hover:bg-black/5 hover:text-[#171717] disabled:cursor-not-allowed disabled:opacity-60 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-white/5 dark:hover:text-[#f3f4f6]"
|
||||||
|
onClick={onClose}
|
||||||
|
disabled={busy}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex h-10 items-center rounded-full bg-red-600 px-4 text-[13px] font-medium text-white transition-colors hover:bg-red-700 disabled:cursor-not-allowed disabled:opacity-60 dark:bg-red-500 dark:hover:bg-red-400"
|
||||||
|
onClick={onConfirm}
|
||||||
|
disabled={busy}
|
||||||
|
>
|
||||||
|
{busy ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
删除中...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'确认删除'
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Portal>
|
||||||
|
</Dialog.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
107
src/pages/Home/components/ConversationRenameDialog.tsx
Normal file
107
src/pages/Home/components/ConversationRenameDialog.tsx
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import * as Dialog from '@radix-ui/react-dialog';
|
||||||
|
import { PencilLine, X } from 'lucide-react';
|
||||||
|
|
||||||
|
type ConversationRenameDialogProps = {
|
||||||
|
open: boolean;
|
||||||
|
value: string;
|
||||||
|
onValueChange: (value: string) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
onConfirm: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ConversationRenameDialog({
|
||||||
|
open,
|
||||||
|
value,
|
||||||
|
onValueChange,
|
||||||
|
onClose,
|
||||||
|
onConfirm,
|
||||||
|
}: ConversationRenameDialogProps) {
|
||||||
|
const trimmedValue = value.trim();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog.Root open={open} onOpenChange={(nextOpen) => {
|
||||||
|
if (!nextOpen) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Dialog.Portal>
|
||||||
|
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/45 backdrop-blur-[2px]" />
|
||||||
|
<Dialog.Content
|
||||||
|
className="fixed left-1/2 top-1/2 z-60 w-[calc(100vw-32px)] max-w-140 -translate-x-1/2 -translate-y-1/2 rounded-2xl bg-white p-0 shadow-[0_30px_80px_rgba(15,23,42,0.18)] outline-none dark:bg-[#1f1f22]"
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between gap-4 border-b border-black/6 px-6 py-5 dark:border-white/6">
|
||||||
|
<div className="flex min-w-0 items-start gap-4">
|
||||||
|
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-2xl border border-[#8ea8ff]/20 bg-[#8ea8ff]/10 text-[#5b7cff] dark:border-[#8ea8ff]/20 dark:bg-[#8ea8ff]/10 dark:text-[#a9bbff]">
|
||||||
|
<PencilLine className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="min-w-0">
|
||||||
|
<Dialog.Title
|
||||||
|
className="text-[26px] font-normal leading-none tracking-tight text-[#171717] dark:text-[#f3f4f6]"
|
||||||
|
style={{ fontFamily: "Georgia, Cambria, 'Times New Roman', Times, serif" }}
|
||||||
|
>
|
||||||
|
重命名对话
|
||||||
|
</Dialog.Title>
|
||||||
|
<Dialog.Description className="mt-3 text-[14px] leading-6 text-[#525866] dark:text-gray-400">
|
||||||
|
为当前会话设置一个更容易辨认的名称。
|
||||||
|
</Dialog.Description>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="rounded-full p-1.5 text-[#99A0AE] transition-colors hover:text-[#171717] dark:hover:text-[#f3f4f6]"
|
||||||
|
onClick={onClose}
|
||||||
|
aria-label="关闭弹窗"
|
||||||
|
>
|
||||||
|
<X className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form
|
||||||
|
className="px-6 py-5"
|
||||||
|
onSubmit={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!trimmedValue) return;
|
||||||
|
onConfirm();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<label className="block text-[13px] font-medium text-[#525866] dark:text-gray-300">
|
||||||
|
对话标题
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
autoFocus
|
||||||
|
value={value}
|
||||||
|
onChange={(event) => onValueChange(event.target.value)}
|
||||||
|
placeholder="输入新的对话名称"
|
||||||
|
className="mt-3 h-12 w-full rounded-[16px] border border-black/10 bg-white/80 px-4 text-[14px] text-[#171717] outline-none transition-colors placeholder:text-[#99A0AE] focus:border-[#8ea8ff] dark:border-white/10 dark:bg-[#202024] dark:text-[#f3f4f6] dark:placeholder:text-gray-500"
|
||||||
|
/>
|
||||||
|
<div className="mt-3 text-[12px] leading-6 text-[#99A0AE] dark:text-gray-500">
|
||||||
|
你也可以稍后再次修改。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-5 flex items-center justify-end gap-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex h-10 items-center rounded-full border border-black/10 px-4 text-[13px] font-medium text-[#171717]/80 transition-colors hover:bg-black/5 hover:text-[#171717] dark:border-gray-700 dark:text-gray-300 dark:hover:bg-white/5 dark:hover:text-[#f3f4f6]"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="inline-flex h-10 items-center rounded-full bg-[#8ea8ff] px-4 text-[13px] font-medium text-white transition-colors hover:bg-[#7f9afb] disabled:cursor-not-allowed disabled:opacity-60"
|
||||||
|
disabled={!trimmedValue}
|
||||||
|
>
|
||||||
|
保存名称
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Portal>
|
||||||
|
</Dialog.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,2 +1,4 @@
|
|||||||
export { default as AddChannelDialog } from './AddChannelDialog';
|
export { default as AddChannelDialog } from './AddChannelDialog';
|
||||||
|
export { default as ConversationDeleteDialog } from './ConversationDeleteDialog';
|
||||||
|
export { default as ConversationRenameDialog } from './ConversationRenameDialog';
|
||||||
export { default as TaskOperationDialog } from './TaskOperationDialog';
|
export { default as TaskOperationDialog } from './TaskOperationDialog';
|
||||||
|
|||||||
@@ -28,7 +28,12 @@ import {
|
|||||||
type StagedAttachment,
|
type StagedAttachment,
|
||||||
} from '../../stores';
|
} from '../../stores';
|
||||||
import { agentsStore, useAgentsStore } from '../../stores/agents';
|
import { agentsStore, useAgentsStore } from '../../stores/agents';
|
||||||
import { AddChannelDialog, TaskOperationDialog } from './components';
|
import {
|
||||||
|
AddChannelDialog,
|
||||||
|
ConversationDeleteDialog,
|
||||||
|
ConversationRenameDialog,
|
||||||
|
TaskOperationDialog,
|
||||||
|
} from './components';
|
||||||
|
|
||||||
type SessionBucketKey = 'today' | 'yesterday' | 'withinWeek' | 'withinTwoWeeks' | 'withinMonth' | 'older';
|
type SessionBucketKey = 'today' | 'yesterday' | 'withinWeek' | 'withinTwoWeeks' | 'withinMonth' | 'older';
|
||||||
|
|
||||||
@@ -164,24 +169,6 @@ function handleSelectConversation(conversationId: string): void {
|
|||||||
chatStore.switchSession(conversationId);
|
chatStore.switchSession(conversationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleRenameConversation(conversationId: string): void {
|
|
||||||
const chat = chatStore.getState();
|
|
||||||
const currentLabel = chat.sessionLabels[conversationId]
|
|
||||||
|| chat.sessions.find((session) => session.key === conversationId)?.displayName
|
|
||||||
|| '';
|
|
||||||
const nextLabel = window.prompt('重命名对话', currentLabel);
|
|
||||||
if (nextLabel) {
|
|
||||||
chatStore.renameSession(conversationId, nextLabel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDeleteConversation(conversationId: string): void {
|
|
||||||
const confirmed = window.confirm('确定删除该会话吗?删除后将无法恢复。');
|
|
||||||
if (confirmed) {
|
|
||||||
void chatStore.deleteSession(conversationId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleRefreshConversationData(): void {
|
function handleRefreshConversationData(): void {
|
||||||
void agentsStore.load();
|
void agentsStore.load();
|
||||||
void chatStore.loadSessions();
|
void chatStore.loadSessions();
|
||||||
@@ -251,6 +238,16 @@ export default function HomePage() {
|
|||||||
const [taskCenterNotice, setTaskCenterNotice] = useState<string | null>(null);
|
const [taskCenterNotice, setTaskCenterNotice] = useState<string | null>(null);
|
||||||
const [taskDialogOpen, setTaskDialogOpen] = useState(false);
|
const [taskDialogOpen, setTaskDialogOpen] = useState(false);
|
||||||
const [addChannelDialogOpen, setAddChannelDialogOpen] = useState(false);
|
const [addChannelDialogOpen, setAddChannelDialogOpen] = useState(false);
|
||||||
|
const [renameConversationTarget, setRenameConversationTarget] = useState<{
|
||||||
|
conversationId: string;
|
||||||
|
title: string;
|
||||||
|
} | null>(null);
|
||||||
|
const [renameConversationValue, setRenameConversationValue] = useState('');
|
||||||
|
const [deleteConversationTarget, setDeleteConversationTarget] = useState<{
|
||||||
|
conversationId: string;
|
||||||
|
title: string;
|
||||||
|
} | null>(null);
|
||||||
|
const [deletingConversation, setDeletingConversation] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void agentsStore.init();
|
void agentsStore.init();
|
||||||
@@ -379,6 +376,52 @@ export default function HomePage() {
|
|||||||
setTaskCenterNotice(result.error || '重试失败,请稍后再试。');
|
setTaskCenterNotice(result.error || '重试失败,请稍后再试。');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleRenameConversation(conversationId: string): void {
|
||||||
|
const nextTitle = chatSessionLabels[conversationId]
|
||||||
|
|| chatSessions.find((session) => session.key === conversationId)?.displayName
|
||||||
|
|| '';
|
||||||
|
|
||||||
|
setRenameConversationTarget({
|
||||||
|
conversationId,
|
||||||
|
title: nextTitle,
|
||||||
|
});
|
||||||
|
setRenameConversationValue(nextTitle);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleConfirmRenameConversation(): void {
|
||||||
|
if (!renameConversationTarget) return;
|
||||||
|
|
||||||
|
const trimmedValue = renameConversationValue.trim();
|
||||||
|
if (!trimmedValue) return;
|
||||||
|
|
||||||
|
chatStore.renameSession(renameConversationTarget.conversationId, trimmedValue);
|
||||||
|
setRenameConversationTarget(null);
|
||||||
|
setRenameConversationValue('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDeleteConversation(conversationId: string): void {
|
||||||
|
const nextTitle = chatSessionLabels[conversationId]
|
||||||
|
|| chatSessions.find((session) => session.key === conversationId)?.displayName
|
||||||
|
|| '未命名会话';
|
||||||
|
|
||||||
|
setDeleteConversationTarget({
|
||||||
|
conversationId,
|
||||||
|
title: nextTitle,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleConfirmDeleteConversation(): Promise<void> {
|
||||||
|
if (!deleteConversationTarget) return;
|
||||||
|
|
||||||
|
setDeletingConversation(true);
|
||||||
|
try {
|
||||||
|
await chatStore.deleteSession(deleteConversationTarget.conversationId);
|
||||||
|
setDeleteConversationTarget(null);
|
||||||
|
} finally {
|
||||||
|
setDeletingConversation(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="h-full w-full min-h-0">
|
<section className="h-full w-full min-h-0">
|
||||||
<div className="flex h-full w-full min-h-0 flex-col gap-2 md:flex-row">
|
<div className="flex h-full w-full min-h-0 flex-col gap-2 md:flex-row">
|
||||||
@@ -535,6 +578,31 @@ export default function HomePage() {
|
|||||||
onClose={() => setAddChannelDialogOpen(false)}
|
onClose={() => setAddChannelDialogOpen(false)}
|
||||||
onConfirm={handleSaveChannels}
|
onConfirm={handleSaveChannels}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ConversationRenameDialog
|
||||||
|
open={Boolean(renameConversationTarget)}
|
||||||
|
value={renameConversationValue}
|
||||||
|
onValueChange={setRenameConversationValue}
|
||||||
|
onClose={() => {
|
||||||
|
setRenameConversationTarget(null);
|
||||||
|
setRenameConversationValue('');
|
||||||
|
}}
|
||||||
|
onConfirm={handleConfirmRenameConversation}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConversationDeleteDialog
|
||||||
|
open={Boolean(deleteConversationTarget)}
|
||||||
|
busy={deletingConversation}
|
||||||
|
conversationTitle={deleteConversationTarget?.title || '未命名会话'}
|
||||||
|
onClose={() => {
|
||||||
|
if (!deletingConversation) {
|
||||||
|
setDeleteConversationTarget(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onConfirm={() => {
|
||||||
|
void handleConfirmDeleteConversation();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user