Skip to content

Commit

Permalink
feat: Optimize UX.
Browse files Browse the repository at this point in the history
  • Loading branch information
Emt-lin committed Jan 7, 2025
1 parent 45a3acc commit d4a8623
Show file tree
Hide file tree
Showing 11 changed files with 453 additions and 341 deletions.
44 changes: 0 additions & 44 deletions src/components/ui/scroll-area.tsx

This file was deleted.

4 changes: 2 additions & 2 deletions src/components/ui/setting-switch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const SettingSwitch = React.forwardRef<HTMLDivElement, SettingSwitchProps>(
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
)}
Expand All @@ -45,7 +45,7 @@ const SettingSwitch = React.forwardRef<HTMLDivElement, SettingSwitchProps>(
>
<div
className={cn(
"pointer-events-none block h-4 w-4 rounded-full bg-primary shadow-lg ring-0 transition-transform",
"pointer-events-none block h-4 w-4 rounded-full bg-toggle-thumb shadow-lg ring-0 transition-transform",
checked ? "translate-x-5.5" : "translate-x-0.5"
)}
/>
Expand Down
13 changes: 6 additions & 7 deletions src/components/ui/setting-tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export const TabItem: React.FC<TabItemProps> = ({ 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",
Expand All @@ -46,15 +46,16 @@ export const TabItem: React.FC<TabItemProps> = ({ 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"
)}
>
<div
className={cn(
"flex items-center justify-center",
"w-4 h-4",
"transition-transform duration-200 ease-in-out"
"transition-transform duration-200 ease-in-out",
isSelected ? "opacity-100 max-w-[16px] translate-x-0" : "opacity-0 max-w-0 -translate-x-4"
)}
>
{tab.icon}
Expand All @@ -65,9 +66,7 @@ export const TabItem: React.FC<TabItemProps> = ({ 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}
Expand Down
4 changes: 2 additions & 2 deletions src/components/ui/slider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ const Slider = React.forwardRef<
className={cn("relative flex w-full touch-none select-none items-center", className)}
{...props}
>
<SliderPrimitive.Track className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-interactive-accent/20">
<SliderPrimitive.Track className="relative h-1.5 w-full grow overflow-hidden border border-solid border-interactive-accent/30 rounded-full bg-interactive-accent/20">
<SliderPrimitive.Range className="absolute h-full bg-interactive-accent" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="block h-4 w-4 rounded-full border bg-primary shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50" />
<SliderPrimitive.Thumb className="block h-4 w-4 rounded-full border bg-toggle-thumb shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50" />
</SliderPrimitive.Root>
));
Slider.displayName = SliderPrimitive.Root.displayName;
Expand Down
6 changes: 5 additions & 1 deletion src/components/ui/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import { cn } from "@/lib/utils";
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table ref={ref} className={cn("w-full caption-bottom text-sm", className)} {...props} />
<table
ref={ref}
className={cn("w-full caption-bottom text-sm border-collapse", className)}
{...props}
/>
</div>
)
);
Expand Down
8 changes: 4 additions & 4 deletions src/settings/v2/SettingsMainV2.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -14,9 +14,9 @@ type TabId = (typeof TAB_IDS)[number];

// 图标映射
const icons: Record<TabId, JSX.Element> = {
basic: <Settings className="w-5 h-5" />,
model: <Bot className="w-5 h-5" />,
advanced: <Sliders className="w-5 h-5" />,
basic: <Cog className="w-5 h-5" />,
model: <Cpu className="w-5 h-5" />,
advanced: <Wrench className="w-5 h-5" />,
};

// 组件映射
Expand Down
183 changes: 183 additions & 0 deletions src/settings/v2/components/ApiKeyDialog.tsx
Original file line number Diff line number Diff line change
@@ -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<CopilotSettings>;
updateSetting: (key: string, value: any) => void;
modalContainer: HTMLElement | null;
}

interface ProviderKeyItem {
provider: DisplayKeyProviders;
apiKey: string;
isVerified: boolean;
}

const ApiKeyDialog: React.FC<ApiKeyDialogProps> = ({
open,
onOpenChange,
settings,
updateSetting,
modalContainer,
}) => {
const [verifyingProviders, setVerifyingProviders] = useState<Set<DisplayKeyProviders>>(new Set());
const [unverifiedKeys, setUnverifiedKeys] = useState<Set<DisplayKeyProviders>>(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 (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent container={modalContainer} className="sm:max-w-[480px]">
<DialogHeader>
<DialogTitle>AI Provider Settings</DialogTitle>
<DialogDescription>
Configure your AI providers by adding their API keys.
</DialogDescription>
</DialogHeader>
<div className="space-y-6 py-4">
<div className="space-y-4">
{providers.map((item: ProviderKeyItem) => (
<div key={item.provider} className="flex items-center gap-2">
<div className="w-[120px] font-medium truncate">
{getProviderLabel(item.provider)}
</div>
<div className="flex-1 flex items-center gap-2">
<div className="flex-1 pr-2">
<PasswordInput
className="w-full"
value={item.apiKey}
onChange={(v) => handleApiKeyChange(item.provider, v)}
disabled={verifyingProviders.has(item.provider)}
/>
</div>
<div className="w-[72px]">
{!item.isVerified ? (
<Button
onClick={() => verifyApiKey(item.provider, item.apiKey)}
disabled={!item.apiKey || verifyingProviders.size > 0}
variant="outline"
size="sm"
className="w-full whitespace-nowrap"
>
{verifyingProviders.has(item.provider) ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Verify
</>
) : (
"Verify"
)}
</Button>
) : (
<span className="text-[#4CAF50] text-sm flex items-center justify-center h-9">
Verified
</span>
)}
</div>
</div>
</div>
))}
</div>
</div>
</DialogContent>
</Dialog>
);
};

export default ApiKeyDialog;
Loading

0 comments on commit d4a8623

Please sign in to comment.