diff --git a/src/components/ui/scroll-area.tsx b/src/components/ui/scroll-area.tsx deleted file mode 100644 index e4ddd5a3..00000000 --- a/src/components/ui/scroll-area.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import * as React from "react"; -import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"; - -import { cn } from "@/lib/utils"; - -const ScrollArea = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - - - {children} - - - - -)); -ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName; - -const ScrollBar = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, orientation = "vertical", ...props }, ref) => ( - - - -)); -ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName; - -export { ScrollArea, ScrollBar }; diff --git a/src/components/ui/setting-switch.tsx b/src/components/ui/setting-switch.tsx index d5186e05..185aef97 100644 --- a/src/components/ui/setting-switch.tsx +++ b/src/components/ui/setting-switch.tsx @@ -35,7 +35,7 @@ const SettingSwitch = React.forwardRef( className={cn( "relative inline-flex h-5.5 w-10 shrink-0 cursor-pointer items-center rounded-full transition-colors", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", - checked ? "bg-interactive-accent" : "bg-interactive-accent/20", + checked ? "bg-interactive-accent" : "bg-[--background-modifier-border-hover]", disabled && "cursor-not-allowed opacity-50", className )} @@ -45,7 +45,7 @@ const SettingSwitch = React.forwardRef( >
diff --git a/src/components/ui/setting-tabs.tsx b/src/components/ui/setting-tabs.tsx index 180cca7b..6d54ea91 100644 --- a/src/components/ui/setting-tabs.tsx +++ b/src/components/ui/setting-tabs.tsx @@ -33,7 +33,7 @@ export const TabItem: React.FC = ({ tab, isSelected, onClick, isFi "whitespace-nowrap", "text-sm", "border border-border border-solid", - "rounded-t-lg rounded-b-[2px]", + "rounded-t-md rounded-b-[2px]", "bg-primary", "transition-all duration-300 ease-in-out", "hover:border-interactive-accent hover:border-b-0", @@ -46,15 +46,16 @@ export const TabItem: React.FC = ({ tab, isSelected, onClick, isFi "transition-all duration-300 ease-in-out", "delay-200", ], - "lg:max-w-12", - "md:max-w-12" + "lg:max-w-32", + "md:max-w-32" )} >
{tab.icon} @@ -65,9 +66,7 @@ export const TabItem: React.FC = ({ tab, isSelected, onClick, isFi "font-medium", "transition-all duration-200 ease-in-out", "overflow-hidden whitespace-nowrap", - isSelected - ? "opacity-100 max-w-[100px] translate-x-0" - : "opacity-0 max-w-0 -translate-x-4" + "opacity-100 max-w-[100px] translate-x-0" )} > {tab.label} diff --git a/src/components/ui/slider.tsx b/src/components/ui/slider.tsx index 717eb55a..dc8883af 100644 --- a/src/components/ui/slider.tsx +++ b/src/components/ui/slider.tsx @@ -12,10 +12,10 @@ const Slider = React.forwardRef< className={cn("relative flex w-full touch-none select-none items-center", className)} {...props} > - + - + )); Slider.displayName = SliderPrimitive.Root.displayName; diff --git a/src/components/ui/table.tsx b/src/components/ui/table.tsx index a953be86..922d510d 100644 --- a/src/components/ui/table.tsx +++ b/src/components/ui/table.tsx @@ -5,7 +5,11 @@ import { cn } from "@/lib/utils"; const Table = React.forwardRef>( ({ className, ...props }, ref) => (
- +
) ); diff --git a/src/settings/v2/SettingsMainV2.tsx b/src/settings/v2/SettingsMainV2.tsx index 1abae5d4..9529dbc9 100644 --- a/src/settings/v2/SettingsMainV2.tsx +++ b/src/settings/v2/SettingsMainV2.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Bot, Settings, Sliders } from "lucide-react"; +import { Cog, Cpu, Wrench } from "lucide-react"; import { TabContent, TabItem, type TabItem as TabItemType } from "@/components/ui/setting-tabs"; import BasicSettings from "./components/BasicSettings"; import ModelSettings from "./components/ModelSettings"; @@ -14,9 +14,9 @@ type TabId = (typeof TAB_IDS)[number]; // 图标映射 const icons: Record = { - basic: , - model: , - advanced: , + basic: , + model: , + advanced: , }; // 组件映射 diff --git a/src/settings/v2/components/ApiKeyDialog.tsx b/src/settings/v2/components/ApiKeyDialog.tsx new file mode 100644 index 00000000..0a63c2f8 --- /dev/null +++ b/src/settings/v2/components/ApiKeyDialog.tsx @@ -0,0 +1,183 @@ +import React, { useState } from "react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Loader2 } from "lucide-react"; +import { PasswordInput } from "@/components/ui/password-input"; +import { err2String, getProviderInfo, getProviderLabel } from "@/utils"; +import { + ChatModelProviders, + DisplayKeyProviders, + EmbeddingModelProviders, + Provider, + ProviderInfo, + ProviderSettingsKeyMap, +} from "@/constants"; +import { Notice } from "obsidian"; +import ChatModelManager from "@/LLMProviders/chatModelManager"; +import { CustomModel } from "@/aiParams"; +import { CopilotSettings } from "@/settings/model"; + +interface ApiKeyDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + settings: Readonly; + updateSetting: (key: string, value: any) => void; + modalContainer: HTMLElement | null; +} + +interface ProviderKeyItem { + provider: DisplayKeyProviders; + apiKey: string; + isVerified: boolean; +} + +const ApiKeyDialog: React.FC = ({ + open, + onOpenChange, + settings, + updateSetting, + modalContainer, +}) => { + const [verifyingProviders, setVerifyingProviders] = useState>(new Set()); + const [unverifiedKeys, setUnverifiedKeys] = useState>(new Set()); + + // Get API key by provider + const getApiKeyByProvider = (provider: DisplayKeyProviders): string => { + const settingKey = ProviderSettingsKeyMap[provider]; + return (settings[settingKey] ?? "") as string; + }; + + // List of providers to exclude + const excludeProviders: Provider[] = [ + ChatModelProviders.OPENAI_FORMAT, + ChatModelProviders.OLLAMA, + ChatModelProviders.LM_STUDIO, + ChatModelProviders.AZURE_OPENAI, + EmbeddingModelProviders.COPILOT_PLUS, + ]; + + const providers: ProviderKeyItem[] = Object.entries(ProviderInfo) + .filter(([key]) => !excludeProviders.includes(key as Provider)) + .map(([provider]) => { + const providerKey = provider as DisplayKeyProviders; + const apiKey = getApiKeyByProvider(providerKey); + return { + provider: providerKey, + apiKey, + isVerified: !!apiKey && !unverifiedKeys.has(providerKey), + }; + }); + + const handleApiKeyChange = (provider: DisplayKeyProviders, value: string) => { + const currentKey = getApiKeyByProvider(provider); + if (currentKey !== value) { + updateSetting(ProviderSettingsKeyMap[provider], value); + setUnverifiedKeys((prev) => new Set(prev).add(provider)); + } + }; + + const verifyApiKey = async (provider: DisplayKeyProviders, apiKey: string) => { + setVerifyingProviders((prev) => new Set(prev).add(provider)); + try { + if (settings.debug) console.log(`Verifying ${provider} API key:`, apiKey); + const defaultTestModel = getProviderInfo(provider).testModel; + + if (!defaultTestModel) { + new Notice( + "API key verification failed: No default test model found for the selected provider." + ); + return; + } + + const customModel: CustomModel = { + name: defaultTestModel, + provider: provider, + apiKey, + enabled: true, + }; + await ChatModelManager.getInstance().ping(customModel); + + new Notice("API key verified successfully!"); + setUnverifiedKeys((prev) => { + const next = new Set(prev); + next.delete(provider); + return next; + }); + } catch (error) { + console.error("API key verification failed:", error); + new Notice("API key verification failed: " + err2String(error)); + } finally { + setVerifyingProviders((prev) => { + const next = new Set(prev); + next.delete(provider); + return next; + }); + } + }; + + return ( + + + + AI Provider Settings + + Configure your AI providers by adding their API keys. + + +
+
+ {providers.map((item: ProviderKeyItem) => ( +
+
+ {getProviderLabel(item.provider)} +
+
+
+ handleApiKeyChange(item.provider, v)} + disabled={verifyingProviders.has(item.provider)} + /> +
+
+ {!item.isVerified ? ( + + ) : ( + + Verified + + )} +
+
+
+ ))} +
+
+
+
+ ); +}; + +export default ApiKeyDialog; diff --git a/src/settings/v2/components/BasicSettings.tsx b/src/settings/v2/components/BasicSettings.tsx index 40758ab9..9277bb38 100644 --- a/src/settings/v2/components/BasicSettings.tsx +++ b/src/settings/v2/components/BasicSettings.tsx @@ -1,33 +1,22 @@ import React, { useState } from "react"; import { getModelKeyFromModel, updateSetting, useSettingsValue } from "@/settings/model"; -import { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; import { SettingItem } from "@/components/ui/setting-item"; import { Button } from "@/components/ui/button"; -import { ArrowRight, HelpCircle, Loader2 } from "lucide-react"; +import { ArrowRight, ExternalLink, HelpCircle, Key } from "lucide-react"; import { useTab } from "@/contexts/TabContext"; -import { - ChatModelProviders, - DisplayKeyProviders, - EmbeddingModelProviders, - ProviderInfo, - ProviderMetadata, - ProviderSettingsKeyMap, - VAULT_VECTOR_STORE_STRATEGIES, -} from "@/constants"; +import { DEFAULT_OPEN_AREA, VAULT_VECTOR_STORE_STRATEGIES } from "@/constants"; +import { ChainType } from "@/chainFactory"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; -import { err2String, getProviderInfo, getProviderLabel, omit } from "@/utils"; -import { CustomModel } from "@/aiParams"; +import { getProviderLabel } from "@/utils"; import { RebuildIndexConfirmModal } from "@/components/modals/RebuildIndexConfirmModal"; -import { Notice } from "obsidian"; -import ChatModelManager from "@/LLMProviders/chatModelManager"; -import { PasswordInput } from "@/components/ui/password-input"; +import ApiKeyDialog from "./ApiKeyDialog"; +import { SettingSwitch } from "@/components/ui/setting-switch"; + +const ChainType2Label: Record = { + [ChainType.LLM_CHAIN]: "Chat", + [ChainType.VAULT_QA_CHAIN]: "Vault QA (Basic)", + [ChainType.COPILOT_PLUS_CHAIN]: "Copilot Plus (Alpha)", +}; interface BasicSettingsProps { indexVaultToVectorStore(overwrite?: boolean): Promise; @@ -36,61 +25,8 @@ interface BasicSettingsProps { const BasicSettings: React.FC = ({ indexVaultToVectorStore }) => { const { setSelectedTab, modalContainer } = useTab(); const settings = useSettingsValue(); - const [selectedProvider, setSelectedProvider] = useState(); - const [apiKey, setApiKey] = useState(""); - const [isVerifying, setIsVerifying] = useState(false); - - const getApiKeyByProvider = (provider: DisplayKeyProviders): string => { - const settingKey = ProviderSettingsKeyMap[provider]; - return (settings[settingKey] ?? "") as string; - }; - - const handleProviderChange = (value: DisplayKeyProviders) => { - setSelectedProvider(value); - setApiKey(getApiKeyByProvider(value)); - }; - - const verifyApiKey = async () => { - if (!selectedProvider || !apiKey) return; - - setIsVerifying(true); - try { - if (settings.debug) console.log(`Verifying ${selectedProvider} API key:`, apiKey); - const defaultTestModel = getProviderInfo(selectedProvider).testModel; - - if (!defaultTestModel) { - new Notice( - "API key verification failed: No default test model found for the selected provider." - ); - return; - } - - const customModel: CustomModel = { - name: defaultTestModel, - provider: selectedProvider, - apiKey, - enabled: true, - }; - await ChatModelManager.getInstance().ping(customModel); - updateSetting(ProviderSettingsKeyMap[selectedProvider], apiKey); - } catch (error) { - console.error("API key verification failed:", error); - new Notice("API key verification failed: " + err2String(error)); - } finally { - setIsVerifying(false); - } - }; - - const excludeProviders = [ - ChatModelProviders.OPENAI_FORMAT, - ChatModelProviders.OLLAMA, - ChatModelProviders.LM_STUDIO, - ChatModelProviders.AZURE_OPENAI, - EmbeddingModelProviders.COPILOT_PLUS, - ]; - const selectProvider: [string, ProviderMetadata][] = Object.entries( - omit(ProviderInfo, excludeProviders) - ); + const [openPopoverIds, setOpenPopoverIds] = useState>(new Set()); + const [isApiKeyDialogOpen, setIsApiKeyDialogOpen] = useState(false); const handleSetDefaultEmbeddingModel = async (modelKey: string) => { if (modelKey !== settings.embeddingModelKey) { @@ -101,93 +37,79 @@ 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; + }); + }; + return (
- {/* General Section */} + {/* Copilot Plus */}
-
General
+
Copilot Plus
- {/* API Key Section */} - -
- - - setApiKey(v)} - disabled={isVerifying} - /> - - -
-
{/* copilot-plus */} - Enter your Copilot Plus license key - + + 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. Alpha access - is limited to sponsors and early supporters. Learn more at{" "} +

+
+

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

+

+ Alpha access is limited to sponsors and early supporters. +

+
+
@@ -198,6 +120,16 @@ const BasicSettings: React.FC = ({ indexVaultToVectorStore } updateSetting("plusLicenseKey", value); }} /> + +
+ +
@@ -205,6 +137,31 @@ const BasicSettings: React.FC = ({ indexVaultToVectorStore }
Chat
+ {/* API Key Section */} + + + + + {/* API Key Dialog */} + + = ({ indexVaultToVectorStore } {/* QA Settings Section */}
-
QA Settings/Embeddings
+
Embedding
= ({ indexVaultToVectorStore } description={
Decide when you want the vault to be indexed. - + { + if (open) { + handlePopoverOpen("index-help"); + } else { + handlePopoverClose("index-help"); + } + }} + > - + handlePopoverOpen("index-help")} + onMouseLeave={() => handlePopoverClose("index-help")} + /> = ({ indexVaultToVectorStore } side="bottom" align="center" sideOffset={0} + onMouseEnter={() => handlePopoverOpen("index-help")} + onMouseLeave={() => handlePopoverClose("index-help")} >
{/* Warning Alert */} @@ -326,6 +298,127 @@ const BasicSettings: React.FC = ({ indexVaultToVectorStore } />
+ + {/* General Section */} +
+
General
+
+ {/* Basic Configuration Group */} + updateSetting("defaultChainType", value as ChainType)} + options={Object.entries(ChainType2Label).map(([key, value]) => ({ + label: value, + value: key, + }))} + /> + + updateSetting("defaultOpenArea", value as DEFAULT_OPEN_AREA)} + options={[ + { label: "Sidebar View", value: DEFAULT_OPEN_AREA.VIEW }, + { label: "Editor", value: DEFAULT_OPEN_AREA.EDITOR }, + ]} + /> + + updateSetting("defaultSaveFolder", value)} + placeholder="copilot-conversations" + /> + + updateSetting("customPromptsFolder", value)} + placeholder="copilot-custom-prompts" + /> + + updateSetting("defaultConversationTag", value)} + placeholder="ai-conversations" + /> + + {/* Feature Toggle Group */} + updateSetting("autosaveChat", checked)} + /> + + updateSetting("showSuggestedPrompts", checked)} + /> + + updateSetting("showRelevantNotes", checked)} + /> + + {/* Advanced Configuration Group */} + Manage Commands} + > +
+
+ {Object.entries(settings.enabledCommands).map(([command, commandInfo]) => ( +
+
+
{commandInfo.name}
+
+ { + const newEnabledCommands = { + ...settings.enabledCommands, + [command]: { + ...commandInfo, + enabled: checked, + }, + }; + updateSetting("enabledCommands", newEnabledCommands); + }} + /> +
+ ))} +
+
+
+
+
); }; diff --git a/src/settings/v2/components/ModelSettings.tsx b/src/settings/v2/components/ModelSettings.tsx index 16164ea5..e8dc9d62 100644 --- a/src/settings/v2/components/ModelSettings.tsx +++ b/src/settings/v2/components/ModelSettings.tsx @@ -1,12 +1,7 @@ import React, { useState } from "react"; import { SettingItem } from "@/components/ui/setting-item"; import { setSettings, updateSetting, useSettingsValue } from "@/settings/model"; -import { ChainType } from "@/chainFactory"; -import { DEFAULT_OPEN_AREA } from "@/constants"; -import { Button } from "@/components/ui/button"; -import { ScrollArea } from "@/components/ui/scroll-area"; import { CustomModel } from "@/aiParams"; -import { SettingSwitch } from "@/components/ui/setting-switch"; import { RebuildIndexConfirmModal } from "@/components/modals/RebuildIndexConfirmModal"; import ChatModelManager from "@/LLMProviders/chatModelManager"; import EmbeddingManager from "@/LLMProviders/embeddingManager"; @@ -22,15 +17,8 @@ const ModelSettings: React.FC = ({ indexVaultToVectorStore } const settings = useSettingsValue(); const [editingModel, setEditingModel] = useState(null); const [showAddDialog, setShowAddDialog] = useState(false); - // const [editingEmbeddingModel, setEditingEmbeddingModel] = useState(null); const [showAddEmbeddingDialog, setShowAddEmbeddingDialog] = useState(false); - const ChainType2Label: Record = { - [ChainType.LLM_CHAIN]: "Chat", - [ChainType.VAULT_QA_CHAIN]: "Vault QA (Basic)", - [ChainType.COPILOT_PLUS_CHAIN]: "Copilot Plus (Alpha)", - }; - const onDeleteModel = (modelKey: string) => { const [modelName, provider] = modelKey.split("|"); const updatedActiveModels = settings.activeModels.filter( @@ -86,7 +74,7 @@ const ModelSettings: React.FC = ({ indexVaultToVectorStore } return (
-
Chat Model
+
Chat Models
= ({ indexVaultToVectorStore } />
- updateSetting("defaultChainType", value as ChainType)} - options={Object.entries(ChainType2Label).map(([key, value]) => ({ - label: value, - value: key, - }))} - /> - - updateSetting("defaultSaveFolder", value)} - placeholder="copilot-conversations" - /> - - updateSetting("defaultConversationTag", value)} - placeholder="copilot-conversation" - /> - - updateSetting("autosaveChat", checked)} - /> - - updateSetting("showSuggestedPrompts", checked)} - /> - - updateSetting("showRelevantNotes", checked)} - /> - - updateSetting("defaultOpenArea", value as DEFAULT_OPEN_AREA)} - options={[ - { label: "Sidebar View", value: DEFAULT_OPEN_AREA.VIEW }, - { label: "Editor", value: DEFAULT_OPEN_AREA.EDITOR }, - ]} - /> - - updateSetting("customPromptsFolder", value)} - placeholder="copilot-custom-prompts" - /> - - Manage Commands} - > - -
- {Object.entries(settings.enabledCommands).map(([command, commandInfo]) => ( -
-
-
{commandInfo.name}
-
- { - const newEnabledCommands = { - ...settings.enabledCommands, - [command]: { - ...commandInfo, - enabled: checked, - }, - }; - updateSetting("enabledCommands", newEnabledCommands); - }} - /> -
- ))} -
-
-
- = ({ indexVaultToVectorStore }
-
Embedding Model
+
Embedding Models
= ({ indexVaultToVectorStore } title="Embedding Model" /> - {/* Embedding model edit dialog */} - {/* !open && setEditingEmbeddingModel(null)} - model={editingEmbeddingModel} - onUpdate={handleEmbeddingModelUpdate} - />*/} - {/* Embedding model add dialog */} = ({
{onEdit && ( - )} @@ -79,6 +84,7 @@ export const ModelTable: React.FC = ({ variant="ghost" size="icon" onClick={() => onDelete(getModelKeyFromModel(model))} + className="shadow-sm hover:shadow-md transition-shadow" > diff --git a/tailwind.config.js b/tailwind.config.js index 16c4123e..03514e7c 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -72,6 +72,9 @@ module.exports = { overlay: { DEFAULT: "#000", }, + toggle: { + thumb: "var(--toggle-thumb-color)", + }, }, borderColor: { inherit: colors.inherit,