From efbc2bd55fad8e860d1730c972b4c83efb3cc55e Mon Sep 17 00:00:00 2001 From: wyh Date: Thu, 16 Jan 2025 17:33:15 +0800 Subject: [PATCH] feat: Add support for customizable conversation filenames. --- src/components/Chat.tsx | 27 +++- .../modals/LoadChatHistoryModal.tsx | 11 +- src/constants.ts | 1 + src/settings/model.ts | 1 + src/settings/v2/components/BasicSettings.tsx | 147 +++++++++++++++++- 5 files changed, 176 insertions(+), 11 deletions(-) diff --git a/src/components/Chat.tsx b/src/components/Chat.tsx index 05b9d5c5..652337e4 100644 --- a/src/components/Chat.tsx +++ b/src/components/Chat.tsx @@ -14,7 +14,7 @@ import { Mention } from "@/mentions/Mention"; import { getSettings, useSettingsValue } from "@/settings/model"; import SharedState, { ChatMessage, useSharedState } from "@/sharedState"; import { FileParserManager } from "@/tools/FileParserManager"; -import { formatDateTime } from "@/utils"; +import { err2String, formatDateTime } from "@/utils"; import { Notice, TFile } from "obsidian"; import React, { useCallback, useContext, useEffect, useRef, useState } from "react"; @@ -268,11 +268,17 @@ const Chat: React.FC = ({ .trim() : "Untitled Chat"; + // Parse the custom format and replace variables + let customFileName = settings.defaultConversationNoteName || "{$date}_{$time}__{$topic}"; + // Create the file name (limit to 100 characters to avoid excessively long names) - const sanitizedFileName = `${firstTenWords.slice(0, 100)}@${timestampFileName}`.replace( - /\s+/g, - "_" - ); + customFileName = customFileName + .replace("{$topic}", firstTenWords.slice(0, 100).replace(/\s+/g, "_")) + .replace("{$date}", timestampFileName.split("_")[0]) + .replace("{$time}", timestampFileName.split("_")[1]); + + // Sanitize the final filename + const sanitizedFileName = customFileName.replace(/[\\/:*?"<>|]/g, "_"); const noteFileName = `${settings.defaultSaveFolder}/${sanitizedFileName}.md`; // Add the timestamp and model properties to the note content @@ -305,11 +311,18 @@ ${chatContent}`; } } } catch (error) { - console.error("Error saving chat as note:", error); + console.error("Error saving chat as note:", err2String(error)); new Notice("Failed to save chat as note. Check console for details."); } }, - [app, chatHistory, currentModelKey, settings.defaultConversationTag, settings.defaultSaveFolder] + [ + app, + chatHistory, + currentModelKey, + settings.defaultConversationTag, + settings.defaultSaveFolder, + settings.defaultConversationNoteName, + ] ); const refreshVaultContext = useCallback(async () => { diff --git a/src/components/modals/LoadChatHistoryModal.tsx b/src/components/modals/LoadChatHistoryModal.tsx index a5558d4b..1f514b24 100644 --- a/src/components/modals/LoadChatHistoryModal.tsx +++ b/src/components/modals/LoadChatHistoryModal.tsx @@ -29,7 +29,14 @@ export class LoadChatHistoryModal extends FuzzySuggestModal { } getItemText(file: TFile): string { - const [title] = file.basename.split("@"); + // Remove {$date} and {$time} parts from the filename + const title = file.basename + .replace(/\{\$date}|\d{8}/g, "") // Remove {$date} or date in format YYYYMMDD + .replace(/\{\$time}|\d{6}/g, "") // Remove {$time} or time in format HHMMSS + .replace(/[@_]/g, " ") // Replace @ and _ with spaces + .replace(/\s+/g, " ") // Replace multiple spaces with single space + .trim(); + let formattedDateTime: FormattedDateTime; // Read the file's front matter @@ -42,7 +49,7 @@ export class LoadChatHistoryModal extends FuzzySuggestModal { formattedDateTime = formatDateTime(new Date(file.stat.ctime)); } - return `${title.replace(/_/g, " ").trim()} - ${formattedDateTime.display}`; + return `${title} - ${formattedDateTime.display}`; } onChooseItem(file: TFile, evt: MouseEvent | KeyboardEvent) { diff --git a/src/constants.ts b/src/constants.ts index d9710e6a..2c6baf3f 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -502,6 +502,7 @@ export const DEFAULT_SETTINGS: CopilotSettings = { numPartitions: 1, enabledCommands: {}, promptUsageTimestamps: {}, + defaultConversationNoteName: "{$date}_{$time}__{$topic}", }; export const EVENT_NAMES = { diff --git a/src/settings/model.ts b/src/settings/model.ts index f6ee032e..2fed1c91 100644 --- a/src/settings/model.ts +++ b/src/settings/model.ts @@ -60,6 +60,7 @@ export interface CopilotSettings { showSuggestedPrompts: boolean; showRelevantNotes: boolean; numPartitions: number; + defaultConversationNoteName: string; } export const settingsStore = createStore(); diff --git a/src/settings/v2/components/BasicSettings.tsx b/src/settings/v2/components/BasicSettings.tsx index 03f3b3bd..852a2d3d 100644 --- a/src/settings/v2/components/BasicSettings.tsx +++ b/src/settings/v2/components/BasicSettings.tsx @@ -8,10 +8,12 @@ import { SettingSwitch } from "@/components/ui/setting-switch"; import { COMMAND_NAMES, DEFAULT_OPEN_AREA, DISABLEABLE_COMMANDS } from "@/constants"; import { useTab } from "@/contexts/TabContext"; import { getModelKeyFromModel, updateSetting, useSettingsValue } from "@/settings/model"; -import { getProviderLabel } from "@/utils"; -import { ArrowRight, ExternalLink, HelpCircle, Key } from "lucide-react"; +import { formatDateTime, getProviderLabel } from "@/utils"; +import { ArrowRight, ExternalLink, HelpCircle, Key, Loader2 } from "lucide-react"; import React, { useState } from "react"; import ApiKeyDialog from "./ApiKeyDialog"; +import { Input } from "@/components/ui/input"; +import { Notice } from "obsidian"; const ChainType2Label: Record = { [ChainType.LLM_CHAIN]: "Chat", @@ -28,6 +30,10 @@ const BasicSettings: React.FC = ({ indexVaultToVectorStore } const settings = useSettingsValue(); const [openPopoverIds, setOpenPopoverIds] = useState>(new Set()); const [isApiKeyDialogOpen, setIsApiKeyDialogOpen] = useState(false); + const [isChecking, setIsChecking] = useState(false); + const [conversationNoteName, setConversationNoteName] = useState( + settings.defaultConversationNoteName || "{$date}_{$time}__{$topic}" + ); const handleSetDefaultEmbeddingModel = async (modelKey: string) => { if (modelKey !== settings.embeddingModelKey) { @@ -50,6 +56,53 @@ const BasicSettings: React.FC = ({ indexVaultToVectorStore } }); }; + const applyCustomNoteFormat = () => { + setIsChecking(true); + + try { + // Check required variables + const format = conversationNoteName || "{$date}_{$time}__{$topic}"; + const requiredVars = ["{$date}", "{$time}", "{$topic}"]; + const missingVars = requiredVars.filter((v) => !format.includes(v)); + + if (missingVars.length > 0) { + new Notice(`Error: Missing required variables: ${missingVars.join(", ")}`, 4000); + return; + } + + // Check illegal characters (excluding variable placeholders) + const illegalChars = /[\\/:*?"<>|]/; + const formatWithoutVars = format + .replace(/\{\$date}/g, "") + .replace(/\{\$time}/g, "") + .replace(/\{\$topic}/g, ""); + + if (illegalChars.test(formatWithoutVars)) { + new Notice(`Error: Format contains illegal characters (\\/:*?"<>|)`, 4000); + return; + } + + // Generate example filename + const { fileName: timestampFileName } = formatDateTime(new Date()); + const firstTenWords = "test topic name"; + + // Create example filename + const customFileName = format + .replace("{$topic}", firstTenWords.slice(0, 100).replace(/\s+/g, "_")) + .replace("{$date}", timestampFileName.split("_")[0]) + .replace("{$time}", timestampFileName.split("_")[1]); + + // Save settings + updateSetting("defaultConversationNoteName", format); + setConversationNoteName(format); + new Notice(`Format applied successfully! Example: ${customFileName}`, 4000); + } catch (error) { + new Notice(`Error applying format: ${error.message}`, 4000); + } finally { + setIsChecking(false); + } + }; + return (
@@ -362,6 +415,96 @@ const BasicSettings: React.FC = ({ indexVaultToVectorStore } placeholder="ai-conversations" /> + + + Customize the format of saved conversation note names. + + { + if (open) { + handlePopoverOpen("note-format-help"); + } else { + handlePopoverClose("note-format-help"); + } + }} + > + + handlePopoverOpen("note-format-help")} + onMouseLeave={() => handlePopoverClose("note-format-help")} + /> + + handlePopoverOpen("note-format-help")} + onMouseLeave={() => handlePopoverClose("note-format-help")} + > +
+
+ Note: All the following variables must be included in the template. +
+
+
Available variables:
+
    +
  • + {"{$date}"}: Date in YYYYMMDD format +
  • +
  • + {"{$time}"}: Time in HHMMSS format +
  • +
  • + {"{$topic}"}: Chat conversation topic +
  • +
+ + Example: {"{$date}_{$time}__{$topic}"} → + 20250114_153232__polish_this_article_[[Readme]] + +
+
+
+
+
+ } + > +
+ setConversationNoteName(e.target.value)} + disabled={isChecking} + /> + + +
+ + {/* Feature Toggle Group */}