mirror of
https://github.com/logancyang/obsidian-copilot.git
synced 2026-07-22 07:50:24 +00:00
* Add setting for auto text selection inclusion and add back manual command * Group chat save settings * Update constants
427 lines
17 KiB
TypeScript
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 'API keys' 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't work, check
|
|
<strong> Obsidian'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>
|
|
);
|
|
};
|