Skip to content

Commit

Permalink
Improve plus user onboarding
Browse files Browse the repository at this point in the history
  • Loading branch information
zeroliu committed Feb 4, 2025
1 parent ef3b000 commit a4cd109
Show file tree
Hide file tree
Showing 16 changed files with 520 additions and 335 deletions.
47 changes: 43 additions & 4 deletions src/LLMProviders/brevilabsClient.ts
Original file line number Diff line number Diff line change
@@ -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: {
Expand Down Expand Up @@ -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}`);
Expand All @@ -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) }),
Expand All @@ -121,6 +129,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,
Expand Down
7 changes: 4 additions & 3 deletions src/LLMProviders/chainManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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}`);
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/LLMProviders/chatModelManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
}
Expand All @@ -280,7 +281,6 @@ export default class ChatModelManager {

const modelConfig = await this.getModelConfig(model);

setModelKey(modelKey);
try {
const newModelInstance = new selectedModel.AIConstructor({
...modelConfig,
Expand Down
36 changes: 30 additions & 6 deletions src/components/chat-components/ChatControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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 {
Expand All @@ -37,15 +40,22 @@ 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">
<DropdownMenu>
<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>
Expand All @@ -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>
Expand Down
58 changes: 58 additions & 0 deletions src/components/modals/CopilotPlusExpiredModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
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 { navigateToPlusPage } from "@/plusUtils";
import { PLUS_UTM_MEDIUMS } from "@/constants";
import { ExternalLink } from "lucide-react";

function CopilotPlusExpiredModalContent({ onCancel }: { onCancel: () => void }) {
return (
<div className="flex flex-col gap-4">
<p>
Your Copilot Plus license key is no longer valid. Please renew your subscription to continue
using Copilot Plus.
</p>
<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();
}
}
71 changes: 71 additions & 0 deletions src/components/modals/CopilotPlusWelcomeModal.tsx
Original file line number Diff line number Diff line change
@@ -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 { switchToPlusModels } from "@/plusUtils";

function CopilotPlusWelcomeModalContent({
onConfirm,
onCancel,
}: {
onConfirm: () => void;
onCancel: () => void;
}) {
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 switch to the exclusive models now? You can always change this later in
Settings.
</p>
</div>
<div className="flex gap-2 justify-end w-full">
<Button variant="ghost" onClick={onCancel}>
Switch Later
</Button>
<Button variant="default" onClick={onConfirm}>
Switch 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 = () => {
switchToPlusModels();
this.close();
};

const handleCancel = () => {
this.close();
};

this.root.render(
<CopilotPlusWelcomeModalContent onConfirm={handleConfirm} onCancel={handleCancel} />
);
}

onClose() {
this.root.unmount();
}
}
12 changes: 1 addition & 11 deletions src/components/ui/setting-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
DialogTrigger,
} from "@/components/ui/dialog";
import { SettingSlider } from "@/components/ui/setting-slider";
import { debounce } from "@/utils";

// 定义输入控件的类型
type InputType =
Expand Down Expand Up @@ -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();
Expand Down
11 changes: 9 additions & 2 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -473,6 +480,7 @@ export const PROCESS_SELECTION_COMMANDS = [
];

export const DEFAULT_SETTINGS: CopilotSettings = {
isPlusUser: undefined,
plusLicenseKey: "",
openAIApiKey: "",
openAIOrgId: "",
Expand All @@ -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,
Expand Down
13 changes: 13 additions & 0 deletions src/logger.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading

0 comments on commit a4cd109

Please sign in to comment.