Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 24 additions & 22 deletions backend/internal/application/usersettings/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,32 +20,34 @@ func (e *ErrValidation) Error() string { return e.Msg }

// allowedKeys 是用户可配置的 key 集合及其默认值。
var allowedKeys = map[string]string{
"chat.file_mode": "auto",
"chat.send_on_enter": "enter",
"chat.show_token_usage": "true",
"chat.show_model_info": "true",
"chat.show_latency": "true",
"chat.show_billing_cost": "true",
"chat.default_model": "",
"chat.auto_generate_title": "true",
"chat.context_compact_auto": "true",
"chat.markdown_render": "true",
"chat.restore_draft_on_failure": "true",
"chat.preserve_conversation_drafts": "true",
"chat.input_height": "standard",
"chat.file_mode": "auto",
"chat.send_on_enter": "enter",
"chat.show_token_usage": "true",
"chat.show_model_info": "true",
"chat.show_latency": "true",
"chat.show_billing_cost": "true",
"chat.default_model": "",
"chat.auto_generate_title": "true",
"chat.delete_conversation_files_by_default": "false",
"chat.context_compact_auto": "true",
"chat.markdown_render": "true",
"chat.restore_draft_on_failure": "true",
"chat.preserve_conversation_drafts": "true",
"chat.input_height": "standard",
}

// boolKeys 取值只能是 "true" / "false"。
var boolKeys = map[string]bool{
"chat.show_token_usage": true,
"chat.show_model_info": true,
"chat.show_latency": true,
"chat.show_billing_cost": true,
"chat.auto_generate_title": true,
"chat.context_compact_auto": true,
"chat.markdown_render": true,
"chat.restore_draft_on_failure": true,
"chat.preserve_conversation_drafts": true,
"chat.show_token_usage": true,
"chat.show_model_info": true,
"chat.show_latency": true,
"chat.show_billing_cost": true,
"chat.auto_generate_title": true,
"chat.delete_conversation_files_by_default": true,
"chat.context_compact_auto": true,
"chat.markdown_render": true,
"chat.restore_draft_on_failure": true,
"chat.preserve_conversation_drafts": true,
}

// enumKeys 枚举 key 的合法值集合。
Expand Down
5 changes: 4 additions & 1 deletion frontend/features/chat/components/app-chat-area.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
sharePatchFromDTO,
} from "@/features/chat/components/sections/conversation-share-dialog";
import { DeleteFilesOption } from "@/features/recent/components/delete-files-option";
import { useChatPreferences } from "@/features/settings/hooks/use-chat-preferences";
import {
AlertDialog,
AlertDialogAction,
Expand Down Expand Up @@ -148,6 +149,7 @@ export function AppChatArea() {
router.push("/chat");
}, [requestNewConversation, router]);
const activeGenerationRunsRef = React.useRef<Set<string>>(new Set());
const { deleteFilesByDefault } = useChatPreferences();
const {
items,
projects,
Expand Down Expand Up @@ -496,8 +498,9 @@ export function AppChatArea() {
if (!canOperateConversation) {
return;
}
setDeleteFiles(deleteFilesByDefault);
setDeleteDialogOpen(true);
}, [canOperateConversation]);
}, [canOperateConversation, deleteFilesByDefault]);

const onConfirmDeleteActiveConversation = React.useCallback(async () => {
if (!canOperateConversation) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import {
} from "@/features/chat/components/sections/conversation-share-dialog"
import { useConversationExportAction } from "@/features/chat/hooks/use-conversation-export-action"
import { DeleteFilesOption } from "@/features/recent/components/delete-files-option"
import { useChatPreferences } from "@/features/settings/hooks/use-chat-preferences"
import { useActiveSidebarConversation } from "@/features/layouts/hooks/use-active-sidebar-conversation"
import { SidebarConversationItem } from "@/features/layouts/components/navigation/sidebar-conversation-item"
import type {
Expand Down Expand Up @@ -203,6 +204,7 @@ export function NavProjects() {
const searchParams = useSearchParams()
const activeProjectID = searchParams.get("project") ?? ""
const activeConversationID = useActiveSidebarConversation()
const { deleteFilesByDefault: deleteConversationFilesByDefault } = useChatPreferences()
const {
items,
projects,
Expand Down Expand Up @@ -282,8 +284,9 @@ export function NavProjects() {
)

const onDeleteConversation = React.useCallback((publicID: string, title: string) => {
setDeleteConversationFiles(deleteConversationFilesByDefault)
setConversationDeleteTarget({ publicID, title })
}, [])
}, [deleteConversationFilesByDefault])

const confirmDeleteConversation = React.useCallback(async () => {
if (!conversationDeleteTarget) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
} from "@/features/chat/components/sections/conversation-share-dialog"
import { useConversationExportAction } from "@/features/chat/hooks/use-conversation-export-action"
import { DeleteFilesOption } from "@/features/recent/components/delete-files-option"
import { useChatPreferences } from "@/features/settings/hooks/use-chat-preferences"
import { useActiveSidebarConversation } from "@/features/layouts/hooks/use-active-sidebar-conversation"
import { useSidebarListFlip } from "@/features/layouts/hooks/use-sidebar-list-flip"
import type {
Expand All @@ -48,6 +49,7 @@ export function NavRecents() {
const { isMobile, setOpenMobile } = useSidebar()
const router = useRouter()
const activeConversationID = useActiveSidebarConversation()
const { deleteFilesByDefault } = useChatPreferences()

const {
recentItems,
Expand Down Expand Up @@ -127,8 +129,9 @@ export function NavRecents() {
)

const onDelete = React.useCallback((publicID: string, title: string) => {
setDeleteFiles(deleteFilesByDefault)
setDeleteTarget({ publicID, title })
}, [])
}, [deleteFilesByDefault])

const onShare = React.useCallback((publicID: string, title: string) => {
setShareTarget({ publicID, title })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
} from "@/features/chat/components/sections/conversation-share-dialog"
import { useConversationExportAction } from "@/features/chat/hooks/use-conversation-export-action"
import { DeleteFilesOption } from "@/features/recent/components/delete-files-option"
import { useChatPreferences } from "@/features/settings/hooks/use-chat-preferences"
import { useActiveSidebarConversation } from "@/features/layouts/hooks/use-active-sidebar-conversation"
import { useSidebarListFlip } from "@/features/layouts/hooks/use-sidebar-list-flip"
import { SIDEBAR_OVERFLOW_ROW_TRANSITION } from "@/features/layouts/model/sidebar-motion"
Expand Down Expand Up @@ -64,6 +65,7 @@ export function NavStarred() {
const { isMobile, setOpenMobile } = useSidebar()
const router = useRouter()
const activeConversationID = useActiveSidebarConversation()
const { deleteFilesByDefault } = useChatPreferences()

const {
starredItems,
Expand Down Expand Up @@ -195,8 +197,9 @@ export function NavStarred() {
)

const onDelete = React.useCallback((publicID: string, title: string) => {
setDeleteFiles(deleteFilesByDefault)
setDeleteTarget({ publicID, title })
}, [])
}, [deleteFilesByDefault])

const onShare = React.useCallback((publicID: string, title: string) => {
setShareTarget({ publicID, title })
Expand Down
8 changes: 6 additions & 2 deletions frontend/features/recent/hooks/use-recent-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { toast } from "sonner";
import { useLocalizedErrorMessage } from "@/i18n/use-localized-error";
import { useLoadMoreSentinel } from "@/shared/hooks/use-load-more-sentinel";
import { useSidebarRecents } from "@/features/recent/context/sidebar-recents-context";
import { useChatPreferences } from "@/features/settings/hooks/use-chat-preferences";
import { resolveAccessToken } from "@/shared/auth/resolve-access-token";
import {
exportConversation,
Expand Down Expand Up @@ -126,6 +127,7 @@ export function useRecentPage() {
const [renameValue, setRenameValue] = React.useState("");
const [deleteTarget, setDeleteTarget] = React.useState<RecentDeleteTarget>(null);
const [deleteFiles, setDeleteFiles] = React.useState(false);
const { deleteFilesByDefault } = useChatPreferences();
const [shareTarget, setShareTarget] = React.useState<ConversationDTO | null>(null);
const loadMoreRef = React.useRef<HTMLDivElement | null>(null);
const pageRef = React.useRef(1);
Expand Down Expand Up @@ -434,11 +436,12 @@ export function useRecentPage() {
);

const onDelete = React.useCallback((item: ConversationDTO) => {
setDeleteFiles(deleteFilesByDefault);
setDeleteTarget({
ids: [item.publicID],
label: t("deleteConversationLabel", { title: item.title || t("untitled") }),
});
}, [t]);
}, [deleteFilesByDefault, t]);

const onExport = React.useCallback(async (item: ConversationDTO) => {
const token = await resolveAccessToken();
Expand Down Expand Up @@ -608,11 +611,12 @@ export function useRecentPage() {
return;
}

setDeleteFiles(deleteFilesByDefault);
setDeleteTarget({
ids: [...selectedConversationIDs],
label: t("selectedConversationCountLabel", { count: selectedConversationIDs.length }),
});
}, [selectedConversationIDs, t]);
}, [deleteFilesByDefault, selectedConversationIDs, t]);

const rowStates = React.useMemo<RecentRowState[]>(
() =>
Expand Down
13 changes: 13 additions & 0 deletions frontend/features/settings/components/sections/settings-chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,19 @@ export function SettingsChat() {
/>
</SettingsFieldRow>
</div>
<div className="pt-4">
<SettingsFieldRow
title={t("input.deleteFilesDefaultTitle")}
description={t("input.deleteFilesDefaultDescription")}
>
<Switch
checked={settings.deleteFilesByDefault}
onCheckedChange={handleBool("chat.delete_conversation_files_by_default", "deleteFilesByDefault")}
disabled={loading}
aria-label={t("input.deleteFilesDefaultTitle")}
/>
</SettingsFieldRow>
</div>
</SettingsFieldList>
</SettingsSection>

Expand Down
10 changes: 10 additions & 0 deletions frontend/features/settings/events/user-settings-events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { UserSettingsMap } from "@/shared/api/user-settings";

export const USER_SETTINGS_UPDATED_EVENT = "deeix-chat:user-settings-updated";

export function dispatchUserSettingsUpdated(settings: UserSettingsMap) {
if (typeof window === "undefined") {
return;
}
window.dispatchEvent(new CustomEvent<UserSettingsMap>(USER_SETTINGS_UPDATED_EVENT, { detail: settings }));
}
110 changes: 110 additions & 0 deletions frontend/features/settings/hooks/use-chat-preferences.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
"use client";

import * as React from "react";

import { USER_SETTINGS_UPDATED_EVENT } from "@/features/settings/events/user-settings-events";
import { getUserSettings } from "@/shared/api/user-settings";
import { useAuthSession } from "@/shared/auth/auth-session-context";

type ChatPreferences = {
autoGenerateTitle: boolean;
deleteFilesByDefault: boolean;
};

const DEFAULT_CHAT_PREFERENCES: ChatPreferences = {
autoGenerateTitle: true,
deleteFilesByDefault: false,
};

let cachedAccessToken: string | null = null;
let cachedPreferences = DEFAULT_CHAT_PREFERENCES;
let pendingAccessToken: string | null = null;
let pendingPreferences: Promise<ChatPreferences> | null = null;

function resolveChatPreferences(settings: Record<string, string>): ChatPreferences {
return {
autoGenerateTitle: settings["chat.auto_generate_title"] !== "false",
deleteFilesByDefault: settings["chat.delete_conversation_files_by_default"] === "true",
};
}

function resetChatPreferencesCache() {
cachedAccessToken = null;
cachedPreferences = DEFAULT_CHAT_PREFERENCES;
pendingAccessToken = null;
pendingPreferences = null;
}

function loadChatPreferences(accessToken: string): Promise<ChatPreferences> {
if (cachedAccessToken === accessToken) {
return Promise.resolve(cachedPreferences);
}
if (pendingPreferences && pendingAccessToken === accessToken) {
return pendingPreferences;
}

pendingAccessToken = accessToken;
pendingPreferences = getUserSettings(accessToken)
.then(resolveChatPreferences)
.then((preferences) => {
if (pendingAccessToken === accessToken) {
cachedAccessToken = accessToken;
cachedPreferences = preferences;
}
return preferences;
})
.catch(() => DEFAULT_CHAT_PREFERENCES)
.finally(() => {
if (pendingAccessToken === accessToken) {
pendingAccessToken = null;
pendingPreferences = null;
}
});

return pendingPreferences;
}

export function useChatPreferences(): ChatPreferences {
const { accessToken } = useAuthSession();
const [preferences, setPreferences] = React.useState<ChatPreferences>(() =>
accessToken && cachedAccessToken === accessToken ? cachedPreferences : DEFAULT_CHAT_PREFERENCES,
);

React.useEffect(() => {
let cancelled = false;

void (async () => {
if (!accessToken) {
resetChatPreferencesCache();
setPreferences(DEFAULT_CHAT_PREFERENCES);
return;
}
const nextPreferences = await loadChatPreferences(accessToken);
if (!cancelled) {
setPreferences(nextPreferences);
}
})();

return () => {
cancelled = true;
};
}, [accessToken]);

React.useEffect(() => {
const handleUserSettingsUpdated = (event: Event) => {
const settings = (event as CustomEvent<Record<string, string>>).detail;
if (settings && typeof settings === "object") {
const nextPreferences = resolveChatPreferences(settings);
cachedPreferences = nextPreferences;
setPreferences(nextPreferences);
}
};

window.addEventListener(USER_SETTINGS_UPDATED_EVENT, handleUserSettingsUpdated);
return () => {
window.removeEventListener(USER_SETTINGS_UPDATED_EVENT, handleUserSettingsUpdated);
};
}, []);

return preferences;
}
2 changes: 2 additions & 0 deletions frontend/features/settings/hooks/use-settings-chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
groupModelsByVendor,
parseChatSettings,
} from "@/features/settings/utils/chat-settings";
import { dispatchUserSettingsUpdated } from "@/features/settings/events/user-settings-events";
import { useAuthSession } from "@/shared/auth/auth-session-context";
import { listPublicModels } from "@/shared/api/model";
import { getBillingConfig } from "@/shared/api/billing";
Expand Down Expand Up @@ -81,6 +82,7 @@ export function useSettingsChat(): UseSettingsChatResult {
}
const saved = parseChatSettings(map);
setSettings((current) => ({ ...current, [field]: saved[field] }));
dispatchUserSettingsUpdated(map);
})
.catch((error) => {
if (settingRequestSeqRef.current[key] !== requestSeq) {
Expand Down
1 change: 1 addition & 0 deletions frontend/features/settings/types/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export type ChatSettings = {
showBillingCost: boolean;
markdownRender: boolean;
autoGenerateTitle: boolean;
deleteFilesByDefault: boolean;
contextCompactAuto: boolean;
restoreDraftOnFailure: boolean;
preserveConversationDrafts: boolean;
Expand Down
2 changes: 2 additions & 0 deletions frontend/features/settings/utils/chat-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const DEFAULT_CHAT_SETTINGS: ChatSettings = {
showBillingCost: true,
markdownRender: true,
autoGenerateTitle: true,
deleteFilesByDefault: false,
contextCompactAuto: false,
restoreDraftOnFailure: true,
preserveConversationDrafts: true,
Expand All @@ -37,6 +38,7 @@ export function parseChatSettings(map: UserSettingsMap): ChatSettings {
showBillingCost: map["chat.show_billing_cost"] !== "false",
markdownRender: map["chat.markdown_render"] !== "false",
autoGenerateTitle: map["chat.auto_generate_title"] !== "false",
deleteFilesByDefault: map["chat.delete_conversation_files_by_default"] === "true",
contextCompactAuto: map["chat.context_compact_auto"] === "true",
restoreDraftOnFailure: map["chat.restore_draft_on_failure"] !== "false",
preserveConversationDrafts: map["chat.preserve_conversation_drafts"] !== "false",
Expand Down
Loading
Loading