Skip to content

Commit 837c6cb

Browse files
authored
Merge pull request #84 from DEEIX-AI/auto_files_delete
2 parents a562d9d + b2eebc8 commit 837c6cb

14 files changed

Lines changed: 190 additions & 30 deletions

File tree

backend/internal/application/usersettings/service.go

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -20,32 +20,34 @@ func (e *ErrValidation) Error() string { return e.Msg }
2020

2121
// allowedKeys 是用户可配置的 key 集合及其默认值。
2222
var allowedKeys = map[string]string{
23-
"chat.file_mode": "auto",
24-
"chat.send_on_enter": "enter",
25-
"chat.show_token_usage": "true",
26-
"chat.show_model_info": "true",
27-
"chat.show_latency": "true",
28-
"chat.show_billing_cost": "true",
29-
"chat.default_model": "",
30-
"chat.auto_generate_title": "true",
31-
"chat.context_compact_auto": "true",
32-
"chat.markdown_render": "true",
33-
"chat.restore_draft_on_failure": "true",
34-
"chat.preserve_conversation_drafts": "true",
35-
"chat.input_height": "standard",
23+
"chat.file_mode": "auto",
24+
"chat.send_on_enter": "enter",
25+
"chat.show_token_usage": "true",
26+
"chat.show_model_info": "true",
27+
"chat.show_latency": "true",
28+
"chat.show_billing_cost": "true",
29+
"chat.default_model": "",
30+
"chat.auto_generate_title": "true",
31+
"chat.delete_conversation_files_by_default": "false",
32+
"chat.context_compact_auto": "true",
33+
"chat.markdown_render": "true",
34+
"chat.restore_draft_on_failure": "true",
35+
"chat.preserve_conversation_drafts": "true",
36+
"chat.input_height": "standard",
3637
}
3738

3839
// boolKeys 取值只能是 "true" / "false"。
3940
var boolKeys = map[string]bool{
40-
"chat.show_token_usage": true,
41-
"chat.show_model_info": true,
42-
"chat.show_latency": true,
43-
"chat.show_billing_cost": true,
44-
"chat.auto_generate_title": true,
45-
"chat.context_compact_auto": true,
46-
"chat.markdown_render": true,
47-
"chat.restore_draft_on_failure": true,
48-
"chat.preserve_conversation_drafts": true,
41+
"chat.show_token_usage": true,
42+
"chat.show_model_info": true,
43+
"chat.show_latency": true,
44+
"chat.show_billing_cost": true,
45+
"chat.auto_generate_title": true,
46+
"chat.delete_conversation_files_by_default": true,
47+
"chat.context_compact_auto": true,
48+
"chat.markdown_render": true,
49+
"chat.restore_draft_on_failure": true,
50+
"chat.preserve_conversation_drafts": true,
4951
}
5052

5153
// enumKeys 枚举 key 的合法值集合。

frontend/features/chat/components/app-chat-area.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
sharePatchFromDTO,
2626
} from "@/features/chat/components/sections/conversation-share-dialog";
2727
import { DeleteFilesOption } from "@/features/recent/components/delete-files-option";
28+
import { useChatPreferences } from "@/features/settings/hooks/use-chat-preferences";
2829
import {
2930
AlertDialog,
3031
AlertDialogAction,
@@ -148,6 +149,7 @@ export function AppChatArea() {
148149
router.push("/chat");
149150
}, [requestNewConversation, router]);
150151
const activeGenerationRunsRef = React.useRef<Set<string>>(new Set());
152+
const { deleteFilesByDefault } = useChatPreferences();
151153
const {
152154
items,
153155
projects,
@@ -496,8 +498,9 @@ export function AppChatArea() {
496498
if (!canOperateConversation) {
497499
return;
498500
}
501+
setDeleteFiles(deleteFilesByDefault);
499502
setDeleteDialogOpen(true);
500-
}, [canOperateConversation]);
503+
}, [canOperateConversation, deleteFilesByDefault]);
501504

502505
const onConfirmDeleteActiveConversation = React.useCallback(async () => {
503506
if (!canOperateConversation) {

frontend/features/layouts/components/navigation/nav-projects.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import {
5555
} from "@/features/chat/components/sections/conversation-share-dialog"
5656
import { useConversationExportAction } from "@/features/chat/hooks/use-conversation-export-action"
5757
import { DeleteFilesOption } from "@/features/recent/components/delete-files-option"
58+
import { useChatPreferences } from "@/features/settings/hooks/use-chat-preferences"
5859
import { useActiveSidebarConversation } from "@/features/layouts/hooks/use-active-sidebar-conversation"
5960
import { SidebarConversationItem } from "@/features/layouts/components/navigation/sidebar-conversation-item"
6061
import type {
@@ -203,6 +204,7 @@ export function NavProjects() {
203204
const searchParams = useSearchParams()
204205
const activeProjectID = searchParams.get("project") ?? ""
205206
const activeConversationID = useActiveSidebarConversation()
207+
const { deleteFilesByDefault: deleteConversationFilesByDefault } = useChatPreferences()
206208
const {
207209
items,
208210
projects,
@@ -282,8 +284,9 @@ export function NavProjects() {
282284
)
283285

284286
const onDeleteConversation = React.useCallback((publicID: string, title: string) => {
287+
setDeleteConversationFiles(deleteConversationFilesByDefault)
285288
setConversationDeleteTarget({ publicID, title })
286-
}, [])
289+
}, [deleteConversationFilesByDefault])
287290

288291
const confirmDeleteConversation = React.useCallback(async () => {
289292
if (!conversationDeleteTarget) {

frontend/features/layouts/components/navigation/nav-recents.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
} from "@/features/chat/components/sections/conversation-share-dialog"
3232
import { useConversationExportAction } from "@/features/chat/hooks/use-conversation-export-action"
3333
import { DeleteFilesOption } from "@/features/recent/components/delete-files-option"
34+
import { useChatPreferences } from "@/features/settings/hooks/use-chat-preferences"
3435
import { useActiveSidebarConversation } from "@/features/layouts/hooks/use-active-sidebar-conversation"
3536
import { useSidebarListFlip } from "@/features/layouts/hooks/use-sidebar-list-flip"
3637
import type {
@@ -48,6 +49,7 @@ export function NavRecents() {
4849
const { isMobile, setOpenMobile } = useSidebar()
4950
const router = useRouter()
5051
const activeConversationID = useActiveSidebarConversation()
52+
const { deleteFilesByDefault } = useChatPreferences()
5153

5254
const {
5355
recentItems,
@@ -127,8 +129,9 @@ export function NavRecents() {
127129
)
128130

129131
const onDelete = React.useCallback((publicID: string, title: string) => {
132+
setDeleteFiles(deleteFilesByDefault)
130133
setDeleteTarget({ publicID, title })
131-
}, [])
134+
}, [deleteFilesByDefault])
132135

133136
const onShare = React.useCallback((publicID: string, title: string) => {
134137
setShareTarget({ publicID, title })

frontend/features/layouts/components/navigation/nav-starred.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
} from "@/features/chat/components/sections/conversation-share-dialog"
3535
import { useConversationExportAction } from "@/features/chat/hooks/use-conversation-export-action"
3636
import { DeleteFilesOption } from "@/features/recent/components/delete-files-option"
37+
import { useChatPreferences } from "@/features/settings/hooks/use-chat-preferences"
3738
import { useActiveSidebarConversation } from "@/features/layouts/hooks/use-active-sidebar-conversation"
3839
import { useSidebarListFlip } from "@/features/layouts/hooks/use-sidebar-list-flip"
3940
import { SIDEBAR_OVERFLOW_ROW_TRANSITION } from "@/features/layouts/model/sidebar-motion"
@@ -64,6 +65,7 @@ export function NavStarred() {
6465
const { isMobile, setOpenMobile } = useSidebar()
6566
const router = useRouter()
6667
const activeConversationID = useActiveSidebarConversation()
68+
const { deleteFilesByDefault } = useChatPreferences()
6769

6870
const {
6971
starredItems,
@@ -195,8 +197,9 @@ export function NavStarred() {
195197
)
196198

197199
const onDelete = React.useCallback((publicID: string, title: string) => {
200+
setDeleteFiles(deleteFilesByDefault)
198201
setDeleteTarget({ publicID, title })
199-
}, [])
202+
}, [deleteFilesByDefault])
200203

201204
const onShare = React.useCallback((publicID: string, title: string) => {
202205
setShareTarget({ publicID, title })

frontend/features/recent/hooks/use-recent-page.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { toast } from "sonner";
88
import { useLocalizedErrorMessage } from "@/i18n/use-localized-error";
99
import { useLoadMoreSentinel } from "@/shared/hooks/use-load-more-sentinel";
1010
import { useSidebarRecents } from "@/features/recent/context/sidebar-recents-context";
11+
import { useChatPreferences } from "@/features/settings/hooks/use-chat-preferences";
1112
import { resolveAccessToken } from "@/shared/auth/resolve-access-token";
1213
import {
1314
exportConversation,
@@ -126,6 +127,7 @@ export function useRecentPage() {
126127
const [renameValue, setRenameValue] = React.useState("");
127128
const [deleteTarget, setDeleteTarget] = React.useState<RecentDeleteTarget>(null);
128129
const [deleteFiles, setDeleteFiles] = React.useState(false);
130+
const { deleteFilesByDefault } = useChatPreferences();
129131
const [shareTarget, setShareTarget] = React.useState<ConversationDTO | null>(null);
130132
const loadMoreRef = React.useRef<HTMLDivElement | null>(null);
131133
const pageRef = React.useRef(1);
@@ -434,11 +436,12 @@ export function useRecentPage() {
434436
);
435437

436438
const onDelete = React.useCallback((item: ConversationDTO) => {
439+
setDeleteFiles(deleteFilesByDefault);
437440
setDeleteTarget({
438441
ids: [item.publicID],
439442
label: t("deleteConversationLabel", { title: item.title || t("untitled") }),
440443
});
441-
}, [t]);
444+
}, [deleteFilesByDefault, t]);
442445

443446
const onExport = React.useCallback(async (item: ConversationDTO) => {
444447
const token = await resolveAccessToken();
@@ -608,11 +611,12 @@ export function useRecentPage() {
608611
return;
609612
}
610613

614+
setDeleteFiles(deleteFilesByDefault);
611615
setDeleteTarget({
612616
ids: [...selectedConversationIDs],
613617
label: t("selectedConversationCountLabel", { count: selectedConversationIDs.length }),
614618
});
615-
}, [selectedConversationIDs, t]);
619+
}, [deleteFilesByDefault, selectedConversationIDs, t]);
616620

617621
const rowStates = React.useMemo<RecentRowState[]>(
618622
() =>

frontend/features/settings/components/sections/settings-chat.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,19 @@ export function SettingsChat() {
541541
/>
542542
</SettingsFieldRow>
543543
</div>
544+
<div className="pt-4">
545+
<SettingsFieldRow
546+
title={t("input.deleteFilesDefaultTitle")}
547+
description={t("input.deleteFilesDefaultDescription")}
548+
>
549+
<Switch
550+
checked={settings.deleteFilesByDefault}
551+
onCheckedChange={handleBool("chat.delete_conversation_files_by_default", "deleteFilesByDefault")}
552+
disabled={loading}
553+
aria-label={t("input.deleteFilesDefaultTitle")}
554+
/>
555+
</SettingsFieldRow>
556+
</div>
544557
</SettingsFieldList>
545558
</SettingsSection>
546559

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import type { UserSettingsMap } from "@/shared/api/user-settings";
2+
3+
export const USER_SETTINGS_UPDATED_EVENT = "deeix-chat:user-settings-updated";
4+
5+
export function dispatchUserSettingsUpdated(settings: UserSettingsMap) {
6+
if (typeof window === "undefined") {
7+
return;
8+
}
9+
window.dispatchEvent(new CustomEvent<UserSettingsMap>(USER_SETTINGS_UPDATED_EVENT, { detail: settings }));
10+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
"use client";
2+
3+
import * as React from "react";
4+
5+
import { USER_SETTINGS_UPDATED_EVENT } from "@/features/settings/events/user-settings-events";
6+
import { getUserSettings } from "@/shared/api/user-settings";
7+
import { useAuthSession } from "@/shared/auth/auth-session-context";
8+
9+
type ChatPreferences = {
10+
autoGenerateTitle: boolean;
11+
deleteFilesByDefault: boolean;
12+
};
13+
14+
const DEFAULT_CHAT_PREFERENCES: ChatPreferences = {
15+
autoGenerateTitle: true,
16+
deleteFilesByDefault: false,
17+
};
18+
19+
let cachedAccessToken: string | null = null;
20+
let cachedPreferences = DEFAULT_CHAT_PREFERENCES;
21+
let pendingAccessToken: string | null = null;
22+
let pendingPreferences: Promise<ChatPreferences> | null = null;
23+
24+
function resolveChatPreferences(settings: Record<string, string>): ChatPreferences {
25+
return {
26+
autoGenerateTitle: settings["chat.auto_generate_title"] !== "false",
27+
deleteFilesByDefault: settings["chat.delete_conversation_files_by_default"] === "true",
28+
};
29+
}
30+
31+
function resetChatPreferencesCache() {
32+
cachedAccessToken = null;
33+
cachedPreferences = DEFAULT_CHAT_PREFERENCES;
34+
pendingAccessToken = null;
35+
pendingPreferences = null;
36+
}
37+
38+
function loadChatPreferences(accessToken: string): Promise<ChatPreferences> {
39+
if (cachedAccessToken === accessToken) {
40+
return Promise.resolve(cachedPreferences);
41+
}
42+
if (pendingPreferences && pendingAccessToken === accessToken) {
43+
return pendingPreferences;
44+
}
45+
46+
pendingAccessToken = accessToken;
47+
pendingPreferences = getUserSettings(accessToken)
48+
.then(resolveChatPreferences)
49+
.then((preferences) => {
50+
if (pendingAccessToken === accessToken) {
51+
cachedAccessToken = accessToken;
52+
cachedPreferences = preferences;
53+
}
54+
return preferences;
55+
})
56+
.catch(() => DEFAULT_CHAT_PREFERENCES)
57+
.finally(() => {
58+
if (pendingAccessToken === accessToken) {
59+
pendingAccessToken = null;
60+
pendingPreferences = null;
61+
}
62+
});
63+
64+
return pendingPreferences;
65+
}
66+
67+
export function useChatPreferences(): ChatPreferences {
68+
const { accessToken } = useAuthSession();
69+
const [preferences, setPreferences] = React.useState<ChatPreferences>(() =>
70+
accessToken && cachedAccessToken === accessToken ? cachedPreferences : DEFAULT_CHAT_PREFERENCES,
71+
);
72+
73+
React.useEffect(() => {
74+
let cancelled = false;
75+
76+
void (async () => {
77+
if (!accessToken) {
78+
resetChatPreferencesCache();
79+
setPreferences(DEFAULT_CHAT_PREFERENCES);
80+
return;
81+
}
82+
const nextPreferences = await loadChatPreferences(accessToken);
83+
if (!cancelled) {
84+
setPreferences(nextPreferences);
85+
}
86+
})();
87+
88+
return () => {
89+
cancelled = true;
90+
};
91+
}, [accessToken]);
92+
93+
React.useEffect(() => {
94+
const handleUserSettingsUpdated = (event: Event) => {
95+
const settings = (event as CustomEvent<Record<string, string>>).detail;
96+
if (settings && typeof settings === "object") {
97+
const nextPreferences = resolveChatPreferences(settings);
98+
cachedPreferences = nextPreferences;
99+
setPreferences(nextPreferences);
100+
}
101+
};
102+
103+
window.addEventListener(USER_SETTINGS_UPDATED_EVENT, handleUserSettingsUpdated);
104+
return () => {
105+
window.removeEventListener(USER_SETTINGS_UPDATED_EVENT, handleUserSettingsUpdated);
106+
};
107+
}, []);
108+
109+
return preferences;
110+
}

frontend/features/settings/hooks/use-settings-chat.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
groupModelsByVendor,
1111
parseChatSettings,
1212
} from "@/features/settings/utils/chat-settings";
13+
import { dispatchUserSettingsUpdated } from "@/features/settings/events/user-settings-events";
1314
import { useAuthSession } from "@/shared/auth/auth-session-context";
1415
import { listPublicModels } from "@/shared/api/model";
1516
import { getBillingConfig } from "@/shared/api/billing";
@@ -81,6 +82,7 @@ export function useSettingsChat(): UseSettingsChatResult {
8182
}
8283
const saved = parseChatSettings(map);
8384
setSettings((current) => ({ ...current, [field]: saved[field] }));
85+
dispatchUserSettingsUpdated(map);
8486
})
8587
.catch((error) => {
8688
if (settingRequestSeqRef.current[key] !== requestSeq) {

0 commit comments

Comments
 (0)