From a5300eb707b80dc0c8590dfb3325e547b16e34dd Mon Sep 17 00:00:00 2001 From: Zero Liu Date: Tue, 4 Feb 2025 00:15:45 -0800 Subject: [PATCH] Improve plus user onboarding (#1160) * Improve plus user onboarding * Update embedding switch logic * Fix reset settings and new user * make isplususer default to false * update default state --- src/LLMProviders/brevilabsClient.ts | 47 +- src/LLMProviders/chainManager.ts | 7 +- src/LLMProviders/chatModelManager.ts | 2 +- .../chat-components/ChatControls.tsx | 36 +- .../modals/CopilotPlusExpiredModal.tsx | 71 +++ .../modals/CopilotPlusWelcomeModal.tsx | 98 ++++ src/components/ui/setting-item.tsx | 12 +- src/constants.ts | 11 +- src/logger.ts | 13 + src/main.ts | 2 + src/plusUtils.ts | 82 ++++ src/settings/model.ts | 2 + src/settings/v2/SettingsMainV2.tsx | 55 ++- src/settings/v2/components/BasicSettings.tsx | 428 +++++------------- src/settings/v2/components/ModelSettings.tsx | 4 +- src/settings/v2/components/PlusSettings.tsx | 76 ++++ src/utils.ts | 11 + 17 files changed, 599 insertions(+), 358 deletions(-) create mode 100644 src/components/modals/CopilotPlusExpiredModal.tsx create mode 100644 src/components/modals/CopilotPlusWelcomeModal.tsx create mode 100644 src/logger.ts create mode 100644 src/plusUtils.ts create mode 100644 src/settings/v2/components/PlusSettings.tsx diff --git a/src/LLMProviders/brevilabsClient.ts b/src/LLMProviders/brevilabsClient.ts index d67de0ad..394e9e9f 100644 --- a/src/LLMProviders/brevilabsClient.ts +++ b/src/LLMProviders/brevilabsClient.ts @@ -1,8 +1,10 @@ import { BREVILABS_API_BASE_URL } from "@/constants"; import { getDecryptedKey } from "@/encryptionService"; +import { logInfo } from "@/logger"; import { getSettings } from "@/settings/model"; -import { safeFetch } from "@/utils"; +import { extractErrorDetail, safeFetch } from "@/utils"; import { Notice } from "obsidian"; +import { turnOnPlus, turnOffPlus } from "@/plusUtils"; export interface BrocaResponse { response: { @@ -93,7 +95,12 @@ export class BrevilabsClient { this.pluginVersion = pluginVersion; } - private async makeRequest(endpoint: string, body: any, method = "POST"): Promise { + private async makeRequest( + endpoint: string, + body: any, + method = "POST", + excludeAuthHeader = false + ): Promise { this.checkLicenseKey(); const url = new URL(`${BREVILABS_API_BASE_URL}${endpoint}`); @@ -103,12 +110,13 @@ export class BrevilabsClient { url.searchParams.append(key, value as string); }); } - const response = await safeFetch(url.toString(), { method, headers: { "Content-Type": "application/json", - Authorization: `Bearer ${await getDecryptedKey(getSettings().plusLicenseKey)}`, + ...(!excludeAuthHeader && { + Authorization: `Bearer ${await getDecryptedKey(getSettings().plusLicenseKey)}`, + }), "X-Client-Version": this.pluginVersion, }, ...(method === "POST" && { body: JSON.stringify(body) }), @@ -154,6 +162,37 @@ export class BrevilabsClient { return data; } + /** + * Validate the license key and update the isPlusUser setting. + * @returns true if the license key is valid, false if the license key is invalid, and undefined if + * unknown error. + */ + async validateLicenseKey(): Promise { + try { + logInfo("settings value", getSettings().plusLicenseKey); + const response = await this.makeRequest( + "/license", + { + license_key: await getDecryptedKey(getSettings().plusLicenseKey), + }, + "POST", + true + ); + logInfo("validateLicenseKey: true", response); + turnOnPlus(); + return true; + } catch (error) { + if (extractErrorDetail(error).reason === "Invalid license key") { + logInfo("validateLicenseKey: false"); + turnOffPlus(); + return false; + } + return; + + // Do nothing if the error is not about the invalid license key + } + } + async broca(userMessage: string): Promise { const brocaResponse = await this.makeRequest("/broca", { message: userMessage, diff --git a/src/LLMProviders/chainManager.ts b/src/LLMProviders/chainManager.ts index 9510c8cc..98fbeab1 100644 --- a/src/LLMProviders/chainManager.ts +++ b/src/LLMProviders/chainManager.ts @@ -34,6 +34,7 @@ import { App, Notice } from "obsidian"; import ChatModelManager from "./chatModelManager"; import MemoryManager from "./memoryManager"; import PromptManager from "./promptManager"; +import { logError, logInfo } from "@/logger"; export default class ChainManager { private static chain: RunnableSequence; @@ -125,10 +126,10 @@ export default class ChainManager { // retrieves the old chain without the chatModel change if it exists! // Create a new chain with the new chatModel this.setChain(getChainType()); - console.log(`Setting model to ${newModelKey}`); + logInfo(`Setting model to ${newModelKey}`); } catch (error) { - console.error("createChainWithNewModel failed: ", error); - console.log("modelKey:", newModelKey); + logError(`createChainWithNewModel failed: ${error}`); + logInfo(`modelKey: ${newModelKey}`); } } diff --git a/src/LLMProviders/chatModelManager.ts b/src/LLMProviders/chatModelManager.ts index f1d56dd8..6b560059 100644 --- a/src/LLMProviders/chatModelManager.ts +++ b/src/LLMProviders/chatModelManager.ts @@ -265,6 +265,7 @@ export default class ChatModelManager { async setChatModel(model: CustomModel): Promise { const modelKey = getModelKeyFromModel(model); + setModelKey(modelKey); if (!ChatModelManager.modelMap.hasOwnProperty(modelKey)) { throw new Error(`No model found for: ${modelKey}`); } @@ -280,7 +281,6 @@ export default class ChatModelManager { const modelConfig = await this.getModelConfig(model); - setModelKey(modelKey); try { const newModelInstance = new selectedModel.AIConstructor({ ...modelConfig, diff --git a/src/components/chat-components/ChatControls.tsx b/src/components/chat-components/ChatControls.tsx index 4d38e4b6..25e896be 100644 --- a/src/components/chat-components/ChatControls.tsx +++ b/src/components/chat-components/ChatControls.tsx @@ -7,6 +7,7 @@ import { RefreshCw, MessageCirclePlus, ChevronDown, + SquareArrowOutUpRight, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu"; @@ -18,6 +19,8 @@ import { useChainType } from "@/aiParams"; import { ChainType } from "@/chainFactory"; import { Notice } from "obsidian"; import VectorStoreManager from "@/search/vectorStoreManager"; +import { navigateToPlusPage, useIsPlusUser } from "@/plusUtils"; +import { PLUS_UTM_MEDIUMS } from "@/constants"; export async function refreshVaultIndex() { try { @@ -37,6 +40,8 @@ interface ChatControlsProps { export function ChatControls({ onNewChat, onSaveAsNote }: ChatControlsProps) { const settings = useSettingsValue(); const [selectedChain, setSelectedChain] = useChainType(); + const isPlusUser = useIsPlusUser(); + return (
@@ -44,8 +49,13 @@ export function ChatControls({ onNewChat, onSaveAsNote }: ChatControlsProps) { @@ -54,11 +64,25 @@ export function ChatControls({ onNewChat, onSaveAsNote }: ChatControlsProps) { chat setSelectedChain(ChainType.VAULT_QA_CHAIN)}> - vault QA (basic) - - setSelectedChain(ChainType.COPILOT_PLUS_CHAIN)}> - copilot plus (beta) + vault QA + {isPlusUser ? ( + setSelectedChain(ChainType.COPILOT_PLUS_CHAIN)}> +
+ + copilot plus (beta) +
+
+ ) : ( + { + navigateToPlusPage(PLUS_UTM_MEDIUMS.CHAT_MODE_SELECT); + }} + > + copilot plus (beta) + + + )}
diff --git a/src/components/modals/CopilotPlusExpiredModal.tsx b/src/components/modals/CopilotPlusExpiredModal.tsx new file mode 100644 index 00000000..539adfda --- /dev/null +++ b/src/components/modals/CopilotPlusExpiredModal.tsx @@ -0,0 +1,71 @@ +import React from "react"; +import { App, Modal } from "obsidian"; +import { createRoot } from "react-dom/client"; +import { Root } from "react-dom/client"; +import { Button } from "@/components/ui/button"; +import { isPlusModel, navigateToPlusPage } from "@/plusUtils"; +import { PLUS_UTM_MEDIUMS } from "@/constants"; +import { ExternalLink } from "lucide-react"; +import { getSettings } from "@/settings/model"; + +function CopilotPlusExpiredModalContent({ onCancel }: { onCancel: () => void }) { + const settings = getSettings(); + const isUsingPlusModels = + isPlusModel(settings.defaultModelKey) && isPlusModel(settings.embeddingModelKey); + + return ( +
+
+
+ Your Copilot Plus license key is no longer valid. Please renew your subscription to + continue using Copilot Plus. +
+ {isUsingPlusModels && ( +
+ The Copilot Plus exclusive models will stop working. You can switch to the default + models in the Settings. +
+ )} +
+
+ + +
+
+ ); +} + +export class CopilotPlusExpiredModal extends Modal { + private root: Root; + + constructor(app: App) { + super(app); + // https://docs.obsidian.md/Reference/TypeScript+API/Modal/setTitle + // @ts-ignore + this.setTitle("Thanks for being a Copilot Plus user ๐Ÿ‘‹"); + } + + onOpen() { + const { contentEl } = this; + this.root = createRoot(contentEl); + + const handleCancel = () => { + this.close(); + }; + + this.root.render(); + } + + onClose() { + this.root.unmount(); + } +} diff --git a/src/components/modals/CopilotPlusWelcomeModal.tsx b/src/components/modals/CopilotPlusWelcomeModal.tsx new file mode 100644 index 00000000..e60e7488 --- /dev/null +++ b/src/components/modals/CopilotPlusWelcomeModal.tsx @@ -0,0 +1,98 @@ +import React from "react"; +import { App, Modal } from "obsidian"; +import { createRoot } from "react-dom/client"; +import { Root } from "react-dom/client"; +import { Button } from "@/components/ui/button"; +import { + DEFAULT_COPILOT_PLUS_CHAT_MODEL, + DEFAULT_COPILOT_PLUS_EMBEDDING_MODEL, + DEFAULT_COPILOT_PLUS_EMBEDDING_MODEL_KEY, + applyPlusSettings, +} from "@/plusUtils"; +import { getSettings } from "@/settings/model"; +import { TriangleAlert } from "lucide-react"; + +function CopilotPlusWelcomeModalContent({ + onConfirm, + onCancel, +}: { + onConfirm: () => void; + onCancel: () => void; +}) { + const settings = getSettings(); + return ( +
+
+

+ Thanks for purchasing Copilot Plus! You have unlocked the full power of Copilot, + featuring chat context, PDF and image support, exclusive chat and embedding models, and + much more! +

+

+ Would you like to apply the Copilot Plus settings now? You can always change this later in + Settings. +

+
    +
  • + Default mode: Copilot Plus +
  • +
  • + Chat model: {DEFAULT_COPILOT_PLUS_CHAT_MODEL} +
  • +
  • +
    + Embedding model: {DEFAULT_COPILOT_PLUS_EMBEDDING_MODEL} +
    + {settings.embeddingModelKey !== DEFAULT_COPILOT_PLUS_EMBEDDING_MODEL_KEY && ( +
    + It will rebuild your embeddings for the entire + vault +
    + )} +
  • +
+
+
+ + +
+
+ ); +} + +export class CopilotPlusWelcomeModal extends Modal { + private root: Root; + + constructor(app: App) { + super(app); + // https://docs.obsidian.md/Reference/TypeScript+API/Modal/setTitle + // @ts-ignore + this.setTitle("Welcome to Copilot Plus ๐Ÿš€"); + } + + onOpen() { + const { contentEl } = this; + this.root = createRoot(contentEl); + + const handleConfirm = () => { + applyPlusSettings(); + this.close(); + }; + + const handleCancel = () => { + this.close(); + }; + + this.root.render( + + ); + } + + onClose() { + this.root.unmount(); + } +} diff --git a/src/components/ui/setting-item.tsx b/src/components/ui/setting-item.tsx index dd7e1485..25f55ed4 100644 --- a/src/components/ui/setting-item.tsx +++ b/src/components/ui/setting-item.tsx @@ -15,6 +15,7 @@ import { DialogTrigger, } from "@/components/ui/dialog"; import { SettingSlider } from "@/components/ui/setting-slider"; +import { debounce } from "@/utils"; // ๅฎšไน‰่พ“ๅ…ฅๆŽงไปถ็š„็ฑปๅž‹ type InputType = @@ -107,17 +108,6 @@ type SettingItemProps = | SliderSettingItemProps | DialogSettingItemProps; -function debounce void>( - func: T, - wait: number -): (...args: Parameters) => void { - let timeout: NodeJS.Timeout; - return (...args: Parameters) => { - clearTimeout(timeout); - timeout = setTimeout(() => func(...args), wait); - }; -} - export function SettingItem(props: SettingItemProps) { const { title, description, className, disabled } = props; const { modalContainer } = useTab(); diff --git a/src/constants.ts b/src/constants.ts index d4bb8c44..4569608d 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -30,6 +30,13 @@ export const LOADING_MESSAGES = { READING_FILES: "Reading files", SEARCHING_WEB: "Searching the web", }; +export const PLUS_UTM_MEDIUMS = { + SETTINGS: "settings", + EXPIRED_MODAL: "expired_modal", + CHAT_MODE_SELECT: "chat_mode_select", + MODE_SELECT_TOOLTIP: "mode_select_tooltip", +}; +export type PlusUtmMedium = (typeof PLUS_UTM_MEDIUMS)[keyof typeof PLUS_UTM_MEDIUMS]; export enum ChatModels { COPILOT_PLUS_FLASH = "copilot-plus-flash", @@ -473,6 +480,7 @@ export const PROCESS_SELECTION_COMMANDS = [ ]; export const DEFAULT_SETTINGS: CopilotSettings = { + isPlusUser: false, plusLicenseKey: "", openAIApiKey: "", openAIOrgId: "", @@ -488,8 +496,7 @@ export const DEFAULT_SETTINGS: CopilotSettings = { openRouterAiApiKey: "", defaultChainType: ChainType.LLM_CHAIN, defaultModelKey: ChatModels.GPT_4o + "|" + ChatModelProviders.OPENAI, - embeddingModelKey: - EmbeddingModels.COPILOT_PLUS_SMALL + "|" + EmbeddingModelProviders.COPILOT_PLUS, + embeddingModelKey: EmbeddingModels.OPENAI_EMBEDDING_SMALL + "|" + EmbeddingModelProviders.OPENAI, temperature: 0.1, maxTokens: 1000, contextTurns: 15, diff --git a/src/logger.ts b/src/logger.ts new file mode 100644 index 00000000..ba1fe484 --- /dev/null +++ b/src/logger.ts @@ -0,0 +1,13 @@ +import { getSettings } from "@/settings/model"; + +export function logInfo(...args: any[]) { + if (getSettings().debug) { + console.log(...args); + } +} + +export function logError(...args: any[]) { + if (getSettings().debug) { + console.error(...args); + } +} diff --git a/src/main.ts b/src/main.ts index 34daf3fd..3b2c420e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -8,6 +8,7 @@ import { LoadChatHistoryModal } from "@/components/modals/LoadChatHistoryModal"; import { CHAT_VIEWTYPE, DEFAULT_OPEN_AREA, EVENT_NAMES } from "@/constants"; import { registerContextMenu } from "@/contextMenu"; import { encryptAllKeys } from "@/encryptionService"; +import { checkIsPlusUser } from "@/plusUtils"; import { HybridRetriever } from "@/search/hybridRetriever"; import VectorStoreManager from "@/search/vectorStoreManager"; import { CopilotSettingTab } from "@/settings/SettingsPage"; @@ -63,6 +64,7 @@ export default class CopilotPlugin extends Plugin { // Initialize BrevilabsClient this.brevilabsClient = BrevilabsClient.getInstance(); this.brevilabsClient.setPluginVersion(this.manifest.version); + await checkIsPlusUser(); this.chainManager = new ChainManager(this.app, this.vectorStoreManager); diff --git a/src/plusUtils.ts b/src/plusUtils.ts new file mode 100644 index 00000000..404dbb91 --- /dev/null +++ b/src/plusUtils.ts @@ -0,0 +1,82 @@ +import { ChainType } from "@/chainFactory"; +import { + ChatModelProviders, + ChatModels, + EmbeddingModelProviders, + EmbeddingModels, + PlusUtmMedium, +} from "@/constants"; +import { BrevilabsClient } from "@/LLMProviders/brevilabsClient"; +import { getSettings, setSettings, updateSetting, useSettingsValue } from "@/settings/model"; +import { setChainType, setModelKey } from "@/aiParams"; +import { CopilotPlusExpiredModal } from "@/components/modals/CopilotPlusExpiredModal"; +import VectorStoreManager from "@/search/vectorStoreManager"; + +export const DEFAULT_COPILOT_PLUS_CHAT_MODEL = ChatModels.COPILOT_PLUS_FLASH; +export const DEFAULT_COPILOT_PLUS_CHAT_MODEL_KEY = + DEFAULT_COPILOT_PLUS_CHAT_MODEL + "|" + ChatModelProviders.COPILOT_PLUS; +export const DEFAULT_COPILOT_PLUS_EMBEDDING_MODEL = EmbeddingModels.COPILOT_PLUS_SMALL; +export const DEFAULT_COPILOT_PLUS_EMBEDDING_MODEL_KEY = + DEFAULT_COPILOT_PLUS_EMBEDDING_MODEL + "|" + EmbeddingModelProviders.COPILOT_PLUS; + +/** Check if the model key is a Copilot Plus model. */ +export function isPlusModel(modelKey: string): boolean { + return modelKey.split("|")[1] === EmbeddingModelProviders.COPILOT_PLUS; +} + +/** Hook to get the isPlusUser setting. */ +export function useIsPlusUser(): boolean | undefined { + const settings = useSettingsValue(); + return settings.isPlusUser; +} + +/** Check if the user is a Plus user. */ +export async function checkIsPlusUser(): Promise { + if (!getSettings().plusLicenseKey) { + turnOffPlus(); + return false; + } + const brevilabsClient = BrevilabsClient.getInstance(); + return await brevilabsClient.validateLicenseKey(); +} + +/** + * Apply the Copilot Plus settings. + * WARNING! If the embedding model is changed, the vault will be indexed. Use it + * with caution. + */ +export function applyPlusSettings(): void { + const defaultModelKey = DEFAULT_COPILOT_PLUS_CHAT_MODEL_KEY; + const embeddingModelKey = DEFAULT_COPILOT_PLUS_EMBEDDING_MODEL_KEY; + const previousEmbeddingModelKey = getSettings().embeddingModelKey; + setModelKey(defaultModelKey); + setChainType(ChainType.COPILOT_PLUS_CHAIN); + setSettings({ + defaultModelKey, + embeddingModelKey, + defaultChainType: ChainType.COPILOT_PLUS_CHAIN, + }); + if (previousEmbeddingModelKey !== embeddingModelKey) { + VectorStoreManager.getInstance().indexVaultToVectorStore(true); + } +} + +export function createPlusPageUrl(medium: PlusUtmMedium): string { + return `https://www.obsidiancopilot.com?utm_source=obsidian&utm_medium=${medium}`; +} + +export function navigateToPlusPage(medium: PlusUtmMedium): void { + window.open(createPlusPageUrl(medium), "_blank"); +} + +export function turnOnPlus(): void { + updateSetting("isPlusUser", true); +} + +export function turnOffPlus(): void { + const previousIsPlusUser = getSettings().isPlusUser; + updateSetting("isPlusUser", false); + if (previousIsPlusUser) { + new CopilotPlusExpiredModal(app).open(); + } +} diff --git a/src/settings/model.ts b/src/settings/model.ts index 2fed1c91..fca59f3e 100644 --- a/src/settings/model.ts +++ b/src/settings/model.ts @@ -61,6 +61,8 @@ export interface CopilotSettings { showRelevantNotes: boolean; numPartitions: number; defaultConversationNoteName: string; + // undefined means never checked + isPlusUser: boolean | undefined; } export const settingsStore = createStore(); diff --git a/src/settings/v2/SettingsMainV2.tsx b/src/settings/v2/SettingsMainV2.tsx index 773d5dd4..82ef409d 100644 --- a/src/settings/v2/SettingsMainV2.tsx +++ b/src/settings/v2/SettingsMainV2.tsx @@ -86,28 +86,39 @@ interface SettingsMainV2Props { plugin: CopilotPlugin; } -const SettingsMainV2: React.FC = ({ plugin }) => ( - -
-
-

-
- Copilot Settings v{plugin.manifest.version} -
-
- -
-

+const SettingsMainV2: React.FC = ({ plugin }) => { + // Add a key state that we'll change when resetting + const [resetKey, setResetKey] = React.useState(0); + + const handleReset = async () => { + const modal = new ResetSettingsConfirmModal(app, async () => { + resetSettings(); + // Increment the key to force re-render of all components + setResetKey((prev) => prev + 1); + }); + modal.open(); + }; + + return ( + +
+
+

+
+ Copilot Settings v{plugin.manifest.version} +
+
+ +
+

+
+ {/* Add the key prop to force re-render */} +
- -
- -); + + ); +}; export default SettingsMainV2; diff --git a/src/settings/v2/components/BasicSettings.tsx b/src/settings/v2/components/BasicSettings.tsx index d206cb9a..ea77d347 100644 --- a/src/settings/v2/components/BasicSettings.tsx +++ b/src/settings/v2/components/BasicSettings.tsx @@ -3,18 +3,24 @@ import { isCommandEnabled } from "@/commands"; import { RebuildIndexConfirmModal } from "@/components/modals/RebuildIndexConfirmModal"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { SettingItem } from "@/components/ui/setting-item"; import { SettingSwitch } from "@/components/ui/setting-switch"; -import { COMMAND_NAMES, DEFAULT_OPEN_AREA, DISABLEABLE_COMMANDS } from "@/constants"; +import { + COMMAND_NAMES, + DEFAULT_OPEN_AREA, + DISABLEABLE_COMMANDS, + PLUS_UTM_MEDIUMS, +} from "@/constants"; import { useTab } from "@/contexts/TabContext"; import { getModelKeyFromModel, updateSetting, useSettingsValue } from "@/settings/model"; import { formatDateTime, getProviderLabel } from "@/utils"; -import { ArrowRight, ExternalLink, HelpCircle, Key, Loader2 } from "lucide-react"; +import { HelpCircle, Key, Loader2 } from "lucide-react"; import { Notice } from "obsidian"; import React, { useState } from "react"; import ApiKeyDialog from "./ApiKeyDialog"; - +import { PlusSettings } from "@/settings/v2/components/PlusSettings"; +import { TooltipProvider, Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; +import { createPlusPageUrl } from "@/plusUtils"; const ChainType2Label: Record = { [ChainType.LLM_CHAIN]: "Chat", [ChainType.VAULT_QA_CHAIN]: "Vault QA (Basic)", @@ -26,9 +32,8 @@ interface BasicSettingsProps { } const BasicSettings: React.FC = ({ indexVaultToVectorStore }) => { - const { setSelectedTab, modalContainer } = useTab(); + const { modalContainer } = useTab(); const settings = useSettingsValue(); - const [openPopoverIds, setOpenPopoverIds] = useState>(new Set()); const [isApiKeyDialogOpen, setIsApiKeyDialogOpen] = useState(false); const [isChecking, setIsChecking] = useState(false); const [conversationNoteName, setConversationNoteName] = useState( @@ -44,18 +49,6 @@ const BasicSettings: React.FC = ({ indexVaultToVectorStore } } }; - const handlePopoverOpen = (id: string) => { - setOpenPopoverIds((prev) => new Set([...prev, id])); - }; - - const handlePopoverClose = (id: string) => { - setOpenPopoverIds((prev) => { - const newSet = new Set(prev); - newSet.delete(id); - return newSet; - }); - }; - const applyCustomNoteFormat = () => { setIsChecking(true); @@ -105,173 +98,60 @@ const BasicSettings: React.FC = ({ indexVaultToVectorStore } return (
-
-
Copilot Plus (beta)
-
- {/* copilot-plus */} - - - Copilot Plus brings powerful AI agent capabilities - - { - if (open) { - handlePopoverOpen("license-help"); - } else { - handlePopoverClose("license-help"); - } - }} - > - - handlePopoverOpen("license-help")} - onMouseLeave={() => handlePopoverClose("license-help")} - /> - - handlePopoverOpen("license-help")} - onMouseLeave={() => handlePopoverClose("license-help")} - > -
-
-

- Copilot Plus brings powerful AI agent capabilities to Obsidian. -

-

- Copilot Plus is currently in beta. We are actively working on improving - the product and adding more features! Join now to lock in the lowest - price! -

-
-
- Learn more at{" "} - - obsidiancopilot.com - -
-
-
-
-
- } - value={settings.plusLicenseKey} - onChange={(value) => { - updateSetting("plusLicenseKey", value); - }} - /> - -
- -
-
- - - {/* Keys Section */} -
-
API Keys (Required)
-
- {/* API Key Section */} - - Configure API keys for different AI providers - { - if (open) { - handlePopoverOpen("api-keys-help"); - } else { - handlePopoverClose("api-keys-help"); - } - }} - > - - handlePopoverOpen("api-keys-help")} - onMouseLeave={() => handlePopoverClose("api-keys-help")} - /> - - handlePopoverOpen("api-keys-help")} - onMouseLeave={() => handlePopoverClose("api-keys-help")} - > -
-
-

- API key is required for chat and QA features -

-

- You'll need to provide an API key from your chosen provider to use the - chat and QA functionality. -

-
-
-
-
-
- } - > - - - - {/* API Key Dialog */} - -
- -
-
- + {/* General Section */}
-
General
+
General
+
+ {/* API Key Section */} + + + Configure API keys for different AI providers + + + + + + + +
+ API key required for chat and QA features +
+
+ To enable chat and QA functionality, please provide an API key from your + selected provider. +
+
+
+
+
+ } + > + + + + {/* API Key Dialog */} + +
= ({ indexVaultToVectorStore } Core Feature: Powers Semantic Search & QA - { - if (open) { - handlePopoverOpen("embedding-model-help"); - } else { - handlePopoverClose("embedding-model-help"); - } - }} - > - - handlePopoverOpen("embedding-model-help")} - onMouseLeave={() => handlePopoverClose("embedding-model-help")} - /> - - handlePopoverOpen("embedding-model-help")} - onMouseLeave={() => handlePopoverClose("embedding-model-help")} - > -
-

+ + + + + + +

This model converts text into vector representations, essential for - semantic search and QA functionality. -

-

Changing the embedding model will:

-
    + semantic search and QA functionality. Changing the embedding model will: +
+
  • Require rebuilding your vault's vector index
  • Affect semantic search quality
  • Impact QA feature performance
-
-
-
+ + +
} @@ -357,62 +215,39 @@ const BasicSettings: React.FC = ({ indexVaultToVectorStore } description={
Select the default chat mode - { - if (open) { - handlePopoverOpen("default-mode-help"); - } else { - handlePopoverClose("default-mode-help"); - } - }} - > - - handlePopoverOpen("default-mode-help")} - onMouseLeave={() => handlePopoverClose("default-mode-help")} - /> - - handlePopoverOpen("default-mode-help")} - onMouseLeave={() => handlePopoverClose("default-mode-help")} - > -
-
-
    -
  • - Chat: Regular chat mode for general conversations and - tasks. Free to use with your own API key. -
  • -
  • - Vault QA (Basic): Ask questions about your vault - content with semantic search. Free to use with your own API key. -
  • -
  • - Copilot Plus: Covers all features of the 2 free modes, - plus advanced paid features including chat context menu, advanced - search, AI agents, and more. Check out{" "} - - obsidiancopilot.com - {" "} - for more details. -
  • -
-
-
-
-
+ + + + + + +
    +
  • + Chat: Regular chat mode for general conversations and + tasks. Free to use with your own API key. +
  • +
  • + Vault QA (Basic): Ask questions about your vault content + with semantic search. Free to use with your own API key. +
  • +
  • + Copilot Plus: Covers all features of the 2 free modes, + plus advanced paid features including chat context menu, advanced search, + AI agents, and more. Check out{" "} + + obsidiancopilot.com + {" "} + for more details. +
  • +
+
+
+
} value={settings.defaultChainType} @@ -470,39 +305,18 @@ const BasicSettings: React.FC = ({ indexVaultToVectorStore } 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:
-
    +
    +
    Available variables:
    +
    • {"{$date}"}: Date in YYYYMMDD format
    • @@ -513,14 +327,14 @@ const BasicSettings: React.FC = ({ indexVaultToVectorStore } {"{$topic}"}: Chat conversation topic
    - + Example: {"{$date}_{$time}__{$topic}"} โ†’ 20250114_153232__polish_this_article_[[Readme]]
    -
- - +
+
+
} > diff --git a/src/settings/v2/components/ModelSettings.tsx b/src/settings/v2/components/ModelSettings.tsx index 475c4341..7850d92a 100644 --- a/src/settings/v2/components/ModelSettings.tsx +++ b/src/settings/v2/components/ModelSettings.tsx @@ -58,7 +58,7 @@ const ModelSettings: React.FC = () => { return (
-
Chat Models
+
Chat Models
{
-
Embedding Models
+
Embedding Models
(null); + const [isChecking, setIsChecking] = useState(false); + const isPlusUser = useIsPlusUser(); + const [localLicenseKey, setLocalLicenseKey] = useState(settings.plusLicenseKey); + useEffect(() => { + setLocalLicenseKey(settings.plusLicenseKey); + }, [settings.plusLicenseKey]); + + return ( +
+
+ Copilot Plus (beta) + {isPlusUser && ( + + Active + + )} +
+
+
+ Copilot Plus takes your Obsidian experience to the next level with cutting-edge AI + capabilities. This premium tier unlocks advanced features, including chat context, PDF and + image support, web search integration, exclusive chat and embedding models, and much more. +
+
+ Currently in beta, Copilot Plus is evolving fast, with new features and improvements + rolling out regularly. Join now to secure the lowest price and get early access! +
+
+
+ { + setLocalLicenseKey(value); + }} + /> + + +
+
{error}
+
+ ); +} diff --git a/src/utils.ts b/src/utils.ts index d0961a34..395d0f8c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -812,3 +812,14 @@ export async function insertIntoEditor(message: string, replace: boolean = false } new Notice("Message inserted into the active note."); } + +export function debounce void>( + func: T, + wait: number +): (...args: Parameters) => void { + let timeout: NodeJS.Timeout; + return (...args: Parameters) => { + clearTimeout(timeout); + timeout = setTimeout(() => func(...args), wait); + }; +}