Skip to content

Commit

Permalink
feat: Add support for customizable conversation filenames.
Browse files Browse the repository at this point in the history
  • Loading branch information
Emt-lin committed Jan 16, 2025
1 parent d307981 commit efbc2bd
Show file tree
Hide file tree
Showing 5 changed files with 176 additions and 11 deletions.
27 changes: 20 additions & 7 deletions src/components/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -268,11 +268,17 @@ const Chat: React.FC<ChatProps> = ({
.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
Expand Down Expand Up @@ -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 () => {
Expand Down
11 changes: 9 additions & 2 deletions src/components/modals/LoadChatHistoryModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,14 @@ export class LoadChatHistoryModal extends FuzzySuggestModal<TFile> {
}

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
Expand All @@ -42,7 +49,7 @@ export class LoadChatHistoryModal extends FuzzySuggestModal<TFile> {
formattedDateTime = formatDateTime(new Date(file.stat.ctime));
}

return `${title.replace(/_/g, " ").trim()} - ${formattedDateTime.display}`;
return `${title} - ${formattedDateTime.display}`;
}

onChooseItem(file: TFile, evt: MouseEvent | KeyboardEvent) {
Expand Down
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,7 @@ export const DEFAULT_SETTINGS: CopilotSettings = {
numPartitions: 1,
enabledCommands: {},
promptUsageTimestamps: {},
defaultConversationNoteName: "{$date}_{$time}__{$topic}",
};

export const EVENT_NAMES = {
Expand Down
1 change: 1 addition & 0 deletions src/settings/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export interface CopilotSettings {
showSuggestedPrompts: boolean;
showRelevantNotes: boolean;
numPartitions: number;
defaultConversationNoteName: string;
}

export const settingsStore = createStore();
Expand Down
147 changes: 145 additions & 2 deletions src/settings/v2/components/BasicSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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, string> = {
[ChainType.LLM_CHAIN]: "Chat",
Expand All @@ -28,6 +30,10 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({ indexVaultToVectorStore }
const settings = useSettingsValue();
const [openPopoverIds, setOpenPopoverIds] = useState<Set<string>>(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) {
Expand All @@ -50,6 +56,53 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({ 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 (
<div className="space-y-4">
<section>
Expand Down Expand Up @@ -362,6 +415,96 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({ indexVaultToVectorStore }
placeholder="ai-conversations"
/>

<SettingItem
type="custom"
title="Conversation Filename Template"
description={
<div className="flex items-start gap-1.5 ">
<span className="leading-none">
Customize the format of saved conversation note names.
</span>
<Popover
open={openPopoverIds.has("note-format-help")}
onOpenChange={(open) => {
if (open) {
handlePopoverOpen("note-format-help");
} else {
handlePopoverClose("note-format-help");
}
}}
>
<PopoverTrigger asChild>
<HelpCircle
className="h-5 w-5 sm:h-4 sm:w-4 cursor-pointer text-muted hover:text-accent"
onMouseEnter={() => handlePopoverOpen("note-format-help")}
onMouseLeave={() => handlePopoverClose("note-format-help")}
/>
</PopoverTrigger>
<PopoverContent
container={modalContainer}
className="w-[90vw] max-w-[400px] p-4 bg-primary border border-solid border-border shadow-sm"
side="bottom"
align="center"
sideOffset={5}
onMouseEnter={() => handlePopoverOpen("note-format-help")}
onMouseLeave={() => handlePopoverClose("note-format-help")}
>
<div className="space-y-2">
<div className="text-[10px] font-medium text-warning p-2 rounded-md">
Note: All the following variables must be included in the template.
</div>
<div className="space-y-1">
<div className="text-sm font-medium">Available variables:</div>
<ul className="space-y-2 text-xs text-muted">
<li>
<strong>{"{$date}"}</strong>: Date in YYYYMMDD format
</li>
<li>
<strong>{"{$time}"}</strong>: Time in HHMMSS format
</li>
<li>
<strong>{"{$topic}"}</strong>: Chat conversation topic
</li>
</ul>
<i className="text-[10px] mt-2">
Example: {"{$date}_{$time}__{$topic}"}
20250114_153232__polish_this_article_[[Readme]]
</i>
</div>
</div>
</PopoverContent>
</Popover>
</div>
}
>
<div className="flex items-center gap-1.5 w-[320px]">
<Input
type="text"
className={`transition-all duration-200 flex-grow min-w-[80px] ${isChecking ? "w-[80px]" : "w-[120px]"}`}
placeholder="{$date}_{$time}__{$topic}"
value={conversationNoteName}
onChange={(e) => setConversationNoteName(e.target.value)}
disabled={isChecking}
/>

<Button
onClick={() => applyCustomNoteFormat()}
disabled={isChecking}
variant="outline"
size="sm"
>
{isChecking ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Apply
</>
) : (
"Apply"
)}
</Button>
</div>
</SettingItem>

{/* Feature Toggle Group */}
<SettingItem
type="switch"
Expand Down

0 comments on commit efbc2bd

Please sign in to comment.