logancyang_obsidian-copilot/src/settings/v2/components/BasicSettings.tsx
Logan Yang ff278c4d3e
Add auto selection to context setting (#2018)
* Add setting for auto text selection inclusion and add back manual command

* Group chat save settings

* Update constants
2025-11-09 19:56:07 -08:00

427 lines
17 KiB
TypeScript

import { ChainType } from "@/chainFactory";
import { Button } from "@/components/ui/button";
import { HelpTooltip } from "@/components/ui/help-tooltip";
import { Input } from "@/components/ui/input";
import { getModelDisplayWithIcons } from "@/components/ui/model-display";
import { SettingItem } from "@/components/ui/setting-item";
import { DEFAULT_OPEN_AREA, PLUS_UTM_MEDIUMS, SEND_SHORTCUT } from "@/constants";
import { cn } from "@/lib/utils";
import { createPlusPageUrl } from "@/plusUtils";
import { getModelKeyFromModel, updateSetting, useSettingsValue } from "@/settings/model";
import { PlusSettings } from "@/settings/v2/components/PlusSettings";
import { checkModelApiKey, formatDateTime } from "@/utils";
import { Key, Loader2 } from "lucide-react";
import { Notice } from "obsidian";
import React, { useState } from "react";
import { ApiKeyDialog } from "./ApiKeyDialog";
const ChainType2Label: Record<ChainType, string> = {
[ChainType.LLM_CHAIN]: "Chat",
[ChainType.VAULT_QA_CHAIN]: "Vault QA (Basic)",
[ChainType.COPILOT_PLUS_CHAIN]: "Copilot Plus",
[ChainType.PROJECT_CHAIN]: "Projects (alpha)",
};
export const BasicSettings: React.FC = () => {
const settings = useSettingsValue();
const [isChecking, setIsChecking] = useState(false);
const [conversationNoteName, setConversationNoteName] = useState(
settings.defaultConversationNoteName || "{$date}_{$time}__{$topic}"
);
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);
}
};
const defaultModelActivated = !!settings.activeModels.find(
(m) => m.enabled && getModelKeyFromModel(m) === settings.defaultModelKey
);
const enableActivatedModels = settings.activeModels
.filter((m) => m.enabled)
.map((model) => ({
label: getModelDisplayWithIcons(model),
value: getModelKeyFromModel(model),
}));
return (
<div className="tw-space-y-4">
<PlusSettings />
{/* General Section */}
<section>
<div className="tw-mb-3 tw-text-xl tw-font-bold">General</div>
<div className="tw-space-y-4">
<div className="tw-space-y-4">
{/* API Key Section */}
<SettingItem
type="custom"
title="API Keys"
description={
<div className="tw-flex tw-items-center tw-gap-1.5">
<span className="tw-leading-none">
Configure API keys for different AI providers
</span>
<HelpTooltip
content={
<div className="tw-flex tw-max-w-96 tw-flex-col tw-gap-2 tw-py-4">
<div className="tw-text-sm tw-font-medium tw-text-accent">
API key required for chat and QA features
</div>
<div className="tw-text-xs tw-text-muted">
To enable chat and QA functionality, please provide an API key from your
selected provider.
</div>
</div>
}
/>
</div>
}
>
<Button
onClick={() => {
new ApiKeyDialog(app).open();
}}
variant="secondary"
className="tw-flex tw-w-full tw-items-center tw-justify-center tw-gap-2 sm:tw-w-auto sm:tw-justify-start"
>
Set Keys
<Key className="tw-size-4" />
</Button>
</SettingItem>
</div>
<SettingItem
type="select"
title="Default Chat Model"
description={
<div className="tw-flex tw-items-center tw-gap-1.5">
<span className="tw-leading-none">Select the Chat model to use</span>
<HelpTooltip
content={
<div className="tw-flex tw-max-w-96 tw-flex-col tw-gap-2 tw-py-4">
<div className="tw-text-sm tw-font-medium tw-text-accent">
Default model is OpenRouter Gemini 2.5 Flash
</div>
<div className="tw-text-xs tw-text-muted">
Set your OpenRouter API key in &apos;API keys&apos; to use this model, or
select a different model from another provider.
</div>
</div>
}
/>
</div>
}
value={defaultModelActivated ? settings.defaultModelKey : "Select Model"}
onChange={(value) => {
const selectedModel = settings.activeModels.find(
(m) => m.enabled && getModelKeyFromModel(m) === value
);
if (!selectedModel) return;
const { hasApiKey, errorNotice } = checkModelApiKey(selectedModel, settings);
if (!hasApiKey && errorNotice) {
new Notice(errorNotice);
// Allow the selection to proceed despite missing API key
}
updateSetting("defaultModelKey", value);
}}
options={
defaultModelActivated
? enableActivatedModels
: [{ label: "Select Model", value: "Select Model" }, ...enableActivatedModels]
}
placeholder="Model"
/>
{/* Basic Configuration Group */}
<SettingItem
type="select"
title="Default Mode"
description={
<div className="tw-flex tw-items-center tw-gap-1.5">
<span className="tw-leading-none">Select the default chat mode</span>
<HelpTooltip
content={
<div className="tw-flex tw-max-w-96 tw-flex-col tw-gap-2">
<ul className="tw-pl-4 tw-text-sm tw-text-muted">
<li>
<strong>Chat:</strong> Regular chat mode for general conversations and
tasks. <i>Free to use with your own API key.</i>
</li>
<li>
<strong>Vault QA (Basic):</strong> Ask questions about your vault content
with semantic search. <i>Free to use with your own API key.</i>
</li>
<li>
<strong>Copilot Plus:</strong> Covers all features of the 2 free modes,
plus advanced paid features including chat context menu, advanced search,
AI agents, and more. Check out{" "}
<a
href={createPlusPageUrl(PLUS_UTM_MEDIUMS.MODE_SELECT_TOOLTIP)}
target="_blank"
rel="noopener noreferrer"
className="tw-text-accent hover:tw-text-accent-hover"
>
obsidiancopilot.com
</a>{" "}
for more details.
</li>
</ul>
</div>
}
/>
</div>
}
value={settings.defaultChainType}
onChange={(value) => updateSetting("defaultChainType", value as ChainType)}
options={Object.entries(ChainType2Label).map(([key, value]) => ({
label: value,
value: key,
}))}
/>
<SettingItem
type="select"
title="Open Plugin In"
description="Choose where to open the plugin"
value={settings.defaultOpenArea}
onChange={(value) => updateSetting("defaultOpenArea", value as DEFAULT_OPEN_AREA)}
options={[
{ label: "Sidebar View", value: DEFAULT_OPEN_AREA.VIEW },
{ label: "Editor", value: DEFAULT_OPEN_AREA.EDITOR },
]}
/>
<SettingItem
type="select"
title="Send Shortcut"
description={
<div className="tw-flex tw-items-center tw-gap-1.5">
<span className="tw-leading-none">Choose keyboard shortcut to send messages</span>
<HelpTooltip
content={
<div className="tw-flex tw-max-w-96 tw-flex-col tw-gap-2 tw-py-4">
<div className="tw-text-sm tw-font-medium tw-text-accent">
Shortcut not working?
</div>
<div className="tw-text-xs tw-text-muted">
If your selected shortcut doesn&#39;t work, check
<strong> Obsidian&#39;s Settings Hotkeys</strong> to see if another
command is using the same key combination. <br />
You may need to remove or change the conflicting hotkey first.
</div>
</div>
}
/>
</div>
}
value={settings.defaultSendShortcut}
onChange={(value) => updateSetting("defaultSendShortcut", value as SEND_SHORTCUT)}
options={[
{ label: "Enter", value: SEND_SHORTCUT.ENTER },
{ label: "Shift + Enter", value: SEND_SHORTCUT.SHIFT_ENTER },
]}
/>
<SettingItem
type="switch"
title="Include Current Note in Context Menu"
description="Automatically include the current note in the chat context menu by default when sending messages to the AI."
checked={settings.includeActiveNoteAsContext}
onCheckedChange={(checked) => {
updateSetting("includeActiveNoteAsContext", checked);
}}
/>
<SettingItem
type="switch"
title="Auto-Add Text Selection to Context"
description="Automatically add selected text to chat context when you make a text selection in markdown notes. Disable to use manual command instead."
checked={settings.autoIncludeTextSelection}
onCheckedChange={(checked) => {
updateSetting("autoIncludeTextSelection", checked);
}}
/>
<SettingItem
type="switch"
title="Images in Markdown"
description="Pass embedded images in markdown to the AI along with the text. Only works with multimodal models."
checked={settings.passMarkdownImages}
onCheckedChange={(checked) => {
updateSetting("passMarkdownImages", checked);
}}
/>
<SettingItem
type="switch"
title="Suggested Prompts"
description="Show suggested prompts in the chat view"
checked={settings.showSuggestedPrompts}
onCheckedChange={(checked) => updateSetting("showSuggestedPrompts", checked)}
/>
<SettingItem
type="switch"
title="Relevant Notes"
description="Show relevant notes in the chat view"
checked={settings.showRelevantNotes}
onCheckedChange={(checked) => updateSetting("showRelevantNotes", checked)}
/>
</div>
</section>
{/* Saving Conversations Section */}
<section>
<div className="tw-mb-3 tw-text-xl tw-font-bold">Saving Conversations</div>
<div className="tw-space-y-4">
<SettingItem
type="switch"
title="Autosave Chat"
description="Automatically saves the chat after every user message and AI response."
checked={settings.autosaveChat}
onCheckedChange={(checked) => updateSetting("autosaveChat", checked)}
/>
<SettingItem
type="switch"
title="Generate AI Chat Title on Save"
description="When enabled, uses an AI model to generate a concise title for saved chat notes. When disabled, uses the first 10 words of the first user message."
checked={settings.generateAIChatTitleOnSave}
onCheckedChange={(checked) => updateSetting("generateAIChatTitleOnSave", checked)}
/>
<SettingItem
type="text"
title="Default Conversation Folder Name"
description="The default folder name where chat conversations will be saved. Default is 'copilot/copilot-conversations'"
value={settings.defaultSaveFolder}
onChange={(value) => updateSetting("defaultSaveFolder", value)}
placeholder="copilot/copilot-conversations"
/>
<SettingItem
type="text"
title="Default Conversation Tag"
description="The default tag to be used when saving a conversation. Default is 'ai-conversations'"
value={settings.defaultConversationTag}
onChange={(value) => updateSetting("defaultConversationTag", value)}
placeholder="ai-conversations"
/>
<SettingItem
type="custom"
title="Conversation Filename Template"
description={
<div className="tw-flex tw-items-start tw-gap-1.5 ">
<span className="tw-leading-none">
Customize the format of saved conversation note names.
</span>
<HelpTooltip
content={
<div className="tw-flex tw-max-w-96 tw-flex-col tw-gap-2 tw-py-4">
<div className="tw-text-sm tw-font-medium tw-text-accent">
Note: All the following variables must be included in the template.
</div>
<div>
<div className="tw-text-sm tw-font-medium tw-text-muted">
Available variables:
</div>
<ul className="tw-pl-4 tw-text-sm tw-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="tw-mt-2 tw-text-sm tw-text-muted">
Example: {"{$date}_{$time}__{$topic}"}
20250114_153232__polish_this_article_[[Readme]]
</i>
</div>
</div>
}
/>
</div>
}
>
<div className="tw-flex tw-w-[320px] tw-items-center tw-gap-1.5">
<Input
type="text"
className={cn(
"tw-min-w-[80px] tw-grow tw-transition-all tw-duration-200",
isChecking ? "tw-w-[80px]" : "tw-w-[120px]"
)}
placeholder="{$date}_{$time}__{$topic}"
value={conversationNoteName}
onChange={(e) => setConversationNoteName(e.target.value)}
disabled={isChecking}
/>
<Button
onClick={() => applyCustomNoteFormat()}
disabled={isChecking}
variant="secondary"
>
{isChecking ? (
<>
<Loader2 className="tw-mr-2 tw-size-4 tw-animate-spin" />
Apply
</>
) : (
"Apply"
)}
</Button>
</div>
</SettingItem>
</div>
</section>
</div>
);
};