mirror of
https://github.com/logancyang/obsidian-copilot.git
synced 2026-07-22 07:50:24 +00:00
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
This commit is contained in:
parent
b83aac6d81
commit
a5300eb707
17 changed files with 599 additions and 358 deletions
|
|
@ -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<T>(endpoint: string, body: any, method = "POST"): Promise<T> {
|
||||
private async makeRequest<T>(
|
||||
endpoint: string,
|
||||
body: any,
|
||||
method = "POST",
|
||||
excludeAuthHeader = false
|
||||
): Promise<T> {
|
||||
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<boolean | undefined> {
|
||||
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<BrocaResponse> {
|
||||
const brocaResponse = await this.makeRequest<BrocaResponse>("/broca", {
|
||||
message: userMessage,
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -265,6 +265,7 @@ export default class ChatModelManager {
|
|||
|
||||
async setChatModel(model: CustomModel): Promise<void> {
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="w-full py-1 flex justify-between items-center px-1">
|
||||
<div className="flex-1">
|
||||
|
|
@ -44,8 +49,13 @@ export function ChatControls({ onNewChat, onSaveAsNote }: ChatControlsProps) {
|
|||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost2" size="fit" className="ml-1">
|
||||
{selectedChain === ChainType.LLM_CHAIN && "chat"}
|
||||
{selectedChain === ChainType.VAULT_QA_CHAIN && "vault QA (basic)"}
|
||||
{selectedChain === ChainType.COPILOT_PLUS_CHAIN && "copilot plus (beta)"}
|
||||
{selectedChain === ChainType.VAULT_QA_CHAIN && "vault QA"}
|
||||
{selectedChain === ChainType.COPILOT_PLUS_CHAIN && (
|
||||
<div className="flex items-center gap-1">
|
||||
<Sparkles className="size-4" />
|
||||
copilot plus (beta)
|
||||
</div>
|
||||
)}
|
||||
<ChevronDown className="size-5 mt-0.5" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
|
@ -54,11 +64,25 @@ export function ChatControls({ onNewChat, onSaveAsNote }: ChatControlsProps) {
|
|||
chat
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={() => setSelectedChain(ChainType.VAULT_QA_CHAIN)}>
|
||||
vault QA (basic)
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={() => setSelectedChain(ChainType.COPILOT_PLUS_CHAIN)}>
|
||||
copilot plus (beta)
|
||||
vault QA
|
||||
</DropdownMenuItem>
|
||||
{isPlusUser ? (
|
||||
<DropdownMenuItem onSelect={() => setSelectedChain(ChainType.COPILOT_PLUS_CHAIN)}>
|
||||
<div className="flex items-center gap-1">
|
||||
<Sparkles className="size-4" />
|
||||
copilot plus (beta)
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
) : (
|
||||
<DropdownMenuItem
|
||||
onSelect={() => {
|
||||
navigateToPlusPage(PLUS_UTM_MEDIUMS.CHAT_MODE_SELECT);
|
||||
}}
|
||||
>
|
||||
copilot plus (beta)
|
||||
<SquareArrowOutUpRight className="size-3" />
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
|
|
|||
71
src/components/modals/CopilotPlusExpiredModal.tsx
Normal file
71
src/components/modals/CopilotPlusExpiredModal.tsx
Normal file
|
|
@ -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 (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div>
|
||||
Your Copilot Plus license key is no longer valid. Please renew your subscription to
|
||||
continue using Copilot Plus.
|
||||
</div>
|
||||
{isUsingPlusModels && (
|
||||
<div className="text-sm text-warning">
|
||||
The Copilot Plus exclusive models will stop working. You can switch to the default
|
||||
models in the Settings.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2 justify-end w-full">
|
||||
<Button variant="ghost" onClick={onCancel}>
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => {
|
||||
navigateToPlusPage(PLUS_UTM_MEDIUMS.EXPIRED_MODAL);
|
||||
}}
|
||||
>
|
||||
Renew Now <ExternalLink className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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(<CopilotPlusExpiredModalContent onCancel={handleCancel} />);
|
||||
}
|
||||
|
||||
onClose() {
|
||||
this.root.unmount();
|
||||
}
|
||||
}
|
||||
98
src/components/modals/CopilotPlusWelcomeModal.tsx
Normal file
98
src/components/modals/CopilotPlusWelcomeModal.tsx
Normal file
|
|
@ -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 (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div>
|
||||
<p>
|
||||
Thanks for purchasing <b>Copilot Plus</b>! You have unlocked the full power of Copilot,
|
||||
featuring chat context, PDF and image support, exclusive chat and embedding models, and
|
||||
much more!
|
||||
</p>
|
||||
<p>
|
||||
Would you like to apply the Copilot Plus settings now? You can always change this later in
|
||||
Settings.
|
||||
</p>
|
||||
<ul className="pl-4">
|
||||
<li>
|
||||
Default mode: <b className="text-accent">Copilot Plus</b>
|
||||
</li>
|
||||
<li>
|
||||
Chat model: <b className="text-accent">{DEFAULT_COPILOT_PLUS_CHAT_MODEL}</b>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
Embedding model: <b className="text-accent">{DEFAULT_COPILOT_PLUS_EMBEDDING_MODEL}</b>
|
||||
</div>
|
||||
{settings.embeddingModelKey !== DEFAULT_COPILOT_PLUS_EMBEDDING_MODEL_KEY && (
|
||||
<div className="text-sm text-warning flex items-center gap-1">
|
||||
<TriangleAlert className="size-4" /> It will rebuild your embeddings for the entire
|
||||
vault
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="flex gap-2 justify-end w-full">
|
||||
<Button variant="ghost" onClick={onCancel}>
|
||||
Apply Later
|
||||
</Button>
|
||||
<Button variant="default" onClick={onConfirm}>
|
||||
Apply Now
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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(
|
||||
<CopilotPlusWelcomeModalContent onConfirm={handleConfirm} onCancel={handleCancel} />
|
||||
);
|
||||
}
|
||||
|
||||
onClose() {
|
||||
this.root.unmount();
|
||||
}
|
||||
}
|
||||
|
|
@ -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<T extends (...args: any[]) => void>(
|
||||
func: T,
|
||||
wait: number
|
||||
): (...args: Parameters<T>) => void {
|
||||
let timeout: NodeJS.Timeout;
|
||||
return (...args: Parameters<T>) => {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => func(...args), wait);
|
||||
};
|
||||
}
|
||||
|
||||
export function SettingItem(props: SettingItemProps) {
|
||||
const { title, description, className, disabled } = props;
|
||||
const { modalContainer } = useTab();
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
13
src/logger.ts
Normal file
13
src/logger.ts
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
82
src/plusUtils.ts
Normal file
82
src/plusUtils.ts
Normal file
|
|
@ -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<boolean | undefined> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
@ -61,6 +61,8 @@ export interface CopilotSettings {
|
|||
showRelevantNotes: boolean;
|
||||
numPartitions: number;
|
||||
defaultConversationNoteName: string;
|
||||
// undefined means never checked
|
||||
isPlusUser: boolean | undefined;
|
||||
}
|
||||
|
||||
export const settingsStore = createStore();
|
||||
|
|
|
|||
|
|
@ -86,28 +86,39 @@ interface SettingsMainV2Props {
|
|||
plugin: CopilotPlugin;
|
||||
}
|
||||
|
||||
const SettingsMainV2: React.FC<SettingsMainV2Props> = ({ plugin }) => (
|
||||
<TabProvider>
|
||||
<div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<h1 className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
|
||||
<div>
|
||||
Copilot Settings <span className="text-xs">v{plugin.manifest.version}</span>
|
||||
</div>
|
||||
<div className="self-end sm:self-auto">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => new ResetSettingsConfirmModal(app, () => resetSettings()).open()}
|
||||
>
|
||||
Reset Settings
|
||||
</Button>
|
||||
</div>
|
||||
</h1>
|
||||
const SettingsMainV2: React.FC<SettingsMainV2Props> = ({ 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 (
|
||||
<TabProvider>
|
||||
<div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<h1 className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
|
||||
<div>
|
||||
Copilot Settings <span className="text-xs">v{plugin.manifest.version}</span>
|
||||
</div>
|
||||
<div className="self-end sm:self-auto">
|
||||
<Button variant="outline" size="sm" onClick={handleReset}>
|
||||
Reset Settings
|
||||
</Button>
|
||||
</div>
|
||||
</h1>
|
||||
</div>
|
||||
{/* Add the key prop to force re-render */}
|
||||
<SettingsContent key={resetKey} plugin={plugin} />
|
||||
</div>
|
||||
<SettingsContent plugin={plugin} />
|
||||
</div>
|
||||
</TabProvider>
|
||||
);
|
||||
</TabProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsMainV2;
|
||||
|
|
|
|||
|
|
@ -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, string> = {
|
||||
[ChainType.LLM_CHAIN]: "Chat",
|
||||
[ChainType.VAULT_QA_CHAIN]: "Vault QA (Basic)",
|
||||
|
|
@ -26,9 +32,8 @@ interface BasicSettingsProps {
|
|||
}
|
||||
|
||||
const BasicSettings: React.FC<BasicSettingsProps> = ({ indexVaultToVectorStore }) => {
|
||||
const { setSelectedTab, modalContainer } = useTab();
|
||||
const { modalContainer } = useTab();
|
||||
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(
|
||||
|
|
@ -44,18 +49,6 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({ 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<BasicSettingsProps> = ({ indexVaultToVectorStore }
|
|||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<section>
|
||||
<div className="text-2xl font-bold mb-3">Copilot Plus (beta)</div>
|
||||
<div className="space-y-4">
|
||||
{/* copilot-plus */}
|
||||
<SettingItem
|
||||
type="password"
|
||||
title="License Key"
|
||||
description={
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="leading-none">
|
||||
Copilot Plus brings powerful AI agent capabilities
|
||||
</span>
|
||||
<Popover
|
||||
open={openPopoverIds.has("license-help")}
|
||||
onOpenChange={(open) => {
|
||||
if (open) {
|
||||
handlePopoverOpen("license-help");
|
||||
} else {
|
||||
handlePopoverClose("license-help");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<HelpCircle
|
||||
className="h-5 w-5 sm:h-4 sm:w-4 cursor-pointer text-muted hover:text-accent translate-y-[1px]"
|
||||
onMouseEnter={() => handlePopoverOpen("license-help")}
|
||||
onMouseLeave={() => handlePopoverClose("license-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("license-help")}
|
||||
onMouseLeave={() => handlePopoverClose("license-help")}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium text-normal">
|
||||
Copilot Plus brings powerful AI agent capabilities to Obsidian.
|
||||
</p>
|
||||
<p className="text-xs text-muted">
|
||||
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!
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-sm text-muted">
|
||||
Learn more at{" "}
|
||||
<a
|
||||
href="https://obsidiancopilot.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-accent hover:text-accent-hover"
|
||||
>
|
||||
obsidiancopilot.com
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
}
|
||||
value={settings.plusLicenseKey}
|
||||
onChange={(value) => {
|
||||
updateSetting("plusLicenseKey", value);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="flex justify-end -mt-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => window.open("https://obsidiancopilot.com", "_blank")}
|
||||
>
|
||||
Get Copilot Plus
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Keys Section */}
|
||||
<section>
|
||||
<div className="text-2xl font-bold mb-4">API Keys (Required)</div>
|
||||
<div className="space-y-4">
|
||||
{/* API Key Section */}
|
||||
<SettingItem
|
||||
type="custom"
|
||||
title="API Keys"
|
||||
description={
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="leading-none">Configure API keys for different AI providers</span>
|
||||
<Popover
|
||||
open={openPopoverIds.has("api-keys-help")}
|
||||
onOpenChange={(open) => {
|
||||
if (open) {
|
||||
handlePopoverOpen("api-keys-help");
|
||||
} else {
|
||||
handlePopoverClose("api-keys-help");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<HelpCircle
|
||||
className="h-5 w-5 sm:h-4 sm:w-4 cursor-pointer text-muted hover:text-accent translate-y-[1px]"
|
||||
onMouseEnter={() => handlePopoverOpen("api-keys-help")}
|
||||
onMouseLeave={() => handlePopoverClose("api-keys-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("api-keys-help")}
|
||||
onMouseLeave={() => handlePopoverClose("api-keys-help")}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium text-accent">
|
||||
API key is required for chat and QA features
|
||||
</p>
|
||||
<p className="text-xs text-muted">
|
||||
You'll need to provide an API key from your chosen provider to use the
|
||||
chat and QA functionality.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
onClick={() => setIsApiKeyDialogOpen(true)}
|
||||
variant="outline"
|
||||
className="flex items-center gap-2 w-full sm:w-auto justify-center sm:justify-start"
|
||||
>
|
||||
Set Keys
|
||||
<Key className="h-4 w-4" />
|
||||
</Button>
|
||||
</SettingItem>
|
||||
|
||||
{/* API Key Dialog */}
|
||||
<ApiKeyDialog
|
||||
open={isApiKeyDialogOpen}
|
||||
onOpenChange={setIsApiKeyDialogOpen}
|
||||
settings={settings}
|
||||
updateSetting={updateSetting}
|
||||
modalContainer={modalContainer}
|
||||
/>
|
||||
<div className="flex justify-end -mt-2">
|
||||
<Button onClick={() => setSelectedTab("model")} variant="outline">
|
||||
More Model Settings
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<PlusSettings />
|
||||
|
||||
{/* General Section */}
|
||||
<section>
|
||||
<div className="text-2xl font-bold mb-3">General</div>
|
||||
<div className="text-xl font-bold mb-3">General</div>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-4">
|
||||
{/* API Key Section */}
|
||||
<SettingItem
|
||||
type="custom"
|
||||
title="API Keys"
|
||||
description={
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="leading-none">
|
||||
Configure API keys for different AI providers
|
||||
</span>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<HelpCircle className="size-4" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-96 flex flex-col gap-2 py-4">
|
||||
<div className="text-sm font-medium text-accent">
|
||||
API key required for chat and QA features
|
||||
</div>
|
||||
<div className="text-xs text-muted">
|
||||
To enable chat and QA functionality, please provide an API key from your
|
||||
selected provider.
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
onClick={() => setIsApiKeyDialogOpen(true)}
|
||||
variant="outline"
|
||||
className="flex items-center gap-2 w-full sm:w-auto justify-center sm:justify-start"
|
||||
>
|
||||
Set Keys
|
||||
<Key className="h-4 w-4" />
|
||||
</Button>
|
||||
</SettingItem>
|
||||
|
||||
{/* API Key Dialog */}
|
||||
<ApiKeyDialog
|
||||
open={isApiKeyDialogOpen}
|
||||
onOpenChange={setIsApiKeyDialogOpen}
|
||||
settings={settings}
|
||||
updateSetting={updateSetting}
|
||||
modalContainer={modalContainer}
|
||||
/>
|
||||
</div>
|
||||
<SettingItem
|
||||
type="select"
|
||||
title="Default Chat Model"
|
||||
|
|
@ -298,46 +178,24 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({ indexVaultToVectorStore }
|
|||
<span className="leading-none font-medium text-accent">
|
||||
Core Feature: Powers Semantic Search & QA
|
||||
</span>
|
||||
<Popover
|
||||
open={openPopoverIds.has("embedding-model-help")}
|
||||
onOpenChange={(open) => {
|
||||
if (open) {
|
||||
handlePopoverOpen("embedding-model-help");
|
||||
} else {
|
||||
handlePopoverClose("embedding-model-help");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<HelpCircle
|
||||
className="h-5 w-5 sm:h-4 sm:w-4 cursor-pointer text-muted hover:text-accent translate-y-[1px]"
|
||||
onMouseEnter={() => handlePopoverOpen("embedding-model-help")}
|
||||
onMouseLeave={() => handlePopoverClose("embedding-model-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("embedding-model-help")}
|
||||
onMouseLeave={() => handlePopoverClose("embedding-model-help")}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-muted mb-3">
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<HelpCircle className="size-4" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-96 flex flex-col gap-2">
|
||||
<div className="text-sm text-muted pt-2">
|
||||
This model converts text into vector representations, essential for
|
||||
semantic search and QA functionality.
|
||||
</p>
|
||||
<p className="text-sm font-medium">Changing the embedding model will:</p>
|
||||
<ul className="text-xs text-muted list-disc pl-4 space-y-1">
|
||||
semantic search and QA functionality. Changing the embedding model will:
|
||||
</div>
|
||||
<ul className="text-sm text-muted pl-4">
|
||||
<li>Require rebuilding your vault's vector index</li>
|
||||
<li>Affect semantic search quality</li>
|
||||
<li>Impact QA feature performance</li>
|
||||
</ul>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
|
@ -357,62 +215,39 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({ indexVaultToVectorStore }
|
|||
description={
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="leading-none">Select the default chat mode</span>
|
||||
<Popover
|
||||
open={openPopoverIds.has("default-mode-help")}
|
||||
onOpenChange={(open) => {
|
||||
if (open) {
|
||||
handlePopoverOpen("default-mode-help");
|
||||
} else {
|
||||
handlePopoverClose("default-mode-help");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<HelpCircle
|
||||
className="h-5 w-5 sm:h-4 sm:w-4 cursor-pointer text-muted hover:text-accent translate-y-[1px]"
|
||||
onMouseEnter={() => handlePopoverOpen("default-mode-help")}
|
||||
onMouseLeave={() => handlePopoverClose("default-mode-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("default-mode-help")}
|
||||
onMouseLeave={() => handlePopoverClose("default-mode-help")}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1">
|
||||
<ul className="space-y-2 text-xs 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="https://obsidiancopilot.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-accent hover:text-accent-hover"
|
||||
>
|
||||
obsidiancopilot.com
|
||||
</a>{" "}
|
||||
for more details.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<HelpCircle className="size-4" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-96 flex flex-col gap-2">
|
||||
<ul className="text-sm text-muted pl-4">
|
||||
<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="text-accent hover:text-accent-hover"
|
||||
>
|
||||
obsidiancopilot.com
|
||||
</a>{" "}
|
||||
for more details.
|
||||
</li>
|
||||
</ul>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
}
|
||||
value={settings.defaultChainType}
|
||||
|
|
@ -470,39 +305,18 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({ indexVaultToVectorStore }
|
|||
<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">
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<HelpCircle className="size-4" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-96 flex flex-col gap-2 py-4">
|
||||
<div className="text-sm font-medium text-accent">
|
||||
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">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-muted">Available variables:</div>
|
||||
<ul className="text-sm text-muted pl-4">
|
||||
<li>
|
||||
<strong>{"{$date}"}</strong>: Date in YYYYMMDD format
|
||||
</li>
|
||||
|
|
@ -513,14 +327,14 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({ indexVaultToVectorStore }
|
|||
<strong>{"{$topic}"}</strong>: Chat conversation topic
|
||||
</li>
|
||||
</ul>
|
||||
<i className="text-[10px] mt-2">
|
||||
<i className="text-sm text-muted mt-2">
|
||||
Example: {"{$date}_{$time}__{$topic}"} →
|
||||
20250114_153232__polish_this_article_[[Readme]]
|
||||
</i>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ const ModelSettings: React.FC = () => {
|
|||
return (
|
||||
<div className="space-y-4">
|
||||
<section>
|
||||
<div className="text-2xl font-bold mb-3">Chat Models</div>
|
||||
<div className="text-xl font-bold mb-3">Chat Models</div>
|
||||
<ModelTable
|
||||
models={settings.activeModels}
|
||||
// onEdit={setEditingModel}
|
||||
|
|
@ -134,7 +134,7 @@ const ModelSettings: React.FC = () => {
|
|||
</section>
|
||||
|
||||
<section>
|
||||
<div className="text-2xl font-bold mb-3">Embedding Models</div>
|
||||
<div className="text-xl font-bold mb-3">Embedding Models</div>
|
||||
<ModelTable
|
||||
models={settings.activeEmbeddingModels}
|
||||
onDelete={onDeleteEmbeddingModel}
|
||||
|
|
|
|||
76
src/settings/v2/components/PlusSettings.tsx
Normal file
76
src/settings/v2/components/PlusSettings.tsx
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import { CopilotPlusWelcomeModal } from "@/components/modals/CopilotPlusWelcomeModal";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { PasswordInput } from "@/components/ui/password-input";
|
||||
import { PLUS_UTM_MEDIUMS } from "@/constants";
|
||||
import { checkIsPlusUser, navigateToPlusPage, useIsPlusUser } from "@/plusUtils";
|
||||
import { updateSetting, useSettingsValue } from "@/settings/model";
|
||||
import { ExternalLink, Loader2 } from "lucide-react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
export function PlusSettings() {
|
||||
const settings = useSettingsValue();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isChecking, setIsChecking] = useState(false);
|
||||
const isPlusUser = useIsPlusUser();
|
||||
const [localLicenseKey, setLocalLicenseKey] = useState(settings.plusLicenseKey);
|
||||
useEffect(() => {
|
||||
setLocalLicenseKey(settings.plusLicenseKey);
|
||||
}, [settings.plusLicenseKey]);
|
||||
|
||||
return (
|
||||
<section className="flex flex-col gap-4 bg-secondary p-4 rounded-lg">
|
||||
<div className="text-xl font-bold flex items-center gap-2 justify-between">
|
||||
<span>Copilot Plus (beta)</span>
|
||||
{isPlusUser && (
|
||||
<Badge variant="outline" className="text-success">
|
||||
Active
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-sm text-muted flex flex-col gap-2">
|
||||
<div>
|
||||
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.
|
||||
</div>
|
||||
<div>
|
||||
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!
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<PasswordInput
|
||||
className="w-full"
|
||||
placeholder="Enter your license key"
|
||||
value={localLicenseKey}
|
||||
onChange={(value) => {
|
||||
setLocalLicenseKey(value);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
disabled={isChecking}
|
||||
onClick={async () => {
|
||||
updateSetting("plusLicenseKey", localLicenseKey);
|
||||
setIsChecking(true);
|
||||
const result = await checkIsPlusUser();
|
||||
setIsChecking(false);
|
||||
if (!result) {
|
||||
setError("Invalid license key");
|
||||
} else {
|
||||
setError(null);
|
||||
new CopilotPlusWelcomeModal(app).open();
|
||||
}
|
||||
}}
|
||||
className="min-w-20"
|
||||
>
|
||||
{isChecking ? <Loader2 className="h-4 w-4 animate-spin" /> : "Apply"}
|
||||
</Button>
|
||||
<Button variant="default" onClick={() => navigateToPlusPage(PLUS_UTM_MEDIUMS.SETTINGS)}>
|
||||
Join Now <ExternalLink className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="text-error">{error}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
11
src/utils.ts
11
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<T extends (...args: any[]) => void>(
|
||||
func: T,
|
||||
wait: number
|
||||
): (...args: Parameters<T>) => void {
|
||||
let timeout: NodeJS.Timeout;
|
||||
return (...args: Parameters<T>) => {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => func(...args), wait);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue