mirror of
https://github.com/logancyang/obsidian-copilot.git
synced 2026-07-22 07:50:24 +00:00
feat: Support custom model displayNames and reorderable Model list. (#1225)
This commit is contained in:
parent
45838c4754
commit
899133166e
15 changed files with 950 additions and 239 deletions
56
package-lock.json
generated
56
package-lock.json
generated
|
|
@ -9,6 +9,9 @@
|
|||
"version": "2.8.4",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@google/generative-ai": "^0.21.0",
|
||||
"@huggingface/inference": "^2.6.4",
|
||||
"@koa/cors": "^5.0.0",
|
||||
|
|
@ -2116,6 +2119,59 @@
|
|||
"w3c-keyname": "^2.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/accessibility": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
|
||||
"integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/core": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
|
||||
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@dnd-kit/accessibility": "^3.1.1",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/sortable": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz",
|
||||
"integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@dnd-kit/core": "^6.3.0",
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/utilities": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
|
||||
"integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@electron/get": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz",
|
||||
|
|
|
|||
|
|
@ -70,6 +70,9 @@
|
|||
"web-streams-polyfill": "^3.3.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@google/generative-ai": "^0.21.0",
|
||||
"@huggingface/inference": "^2.6.4",
|
||||
"@koa/cors": "^5.0.0",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { ModelCapability } from "@/aiParams";
|
||||
import { getStandaloneQuestion } from "@/chainUtils";
|
||||
import {
|
||||
ABORT_REASON,
|
||||
|
|
@ -6,6 +5,7 @@ import {
|
|||
EMPTY_INDEX_ERROR_MESSAGE,
|
||||
LOADING_MESSAGES,
|
||||
MAX_CHARS_FOR_LOCAL_SEARCH_CONTEXT,
|
||||
ModelCapability,
|
||||
} from "@/constants";
|
||||
import { BrevilabsClient } from "@/LLMProviders/brevilabsClient";
|
||||
import { getSystemPrompt } from "@/settings/model";
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { ChatPromptTemplate } from "@langchain/core/prompts";
|
|||
|
||||
import { settingsAtom, settingsStore } from "@/settings/model";
|
||||
import { atom, useAtom } from "jotai";
|
||||
import { ModelCapability } from "@/constants";
|
||||
|
||||
const userModelKeyAtom = atom<string | null>(null);
|
||||
const modelKeyAtom = atom(
|
||||
|
|
@ -65,12 +66,6 @@ export interface SetChainOptions {
|
|||
refreshIndex?: boolean;
|
||||
}
|
||||
|
||||
export enum ModelCapability {
|
||||
REASONING = "reasoning",
|
||||
VISION = "vision",
|
||||
WEB_SEARCH = "websearch",
|
||||
}
|
||||
|
||||
export interface CustomModel {
|
||||
name: string;
|
||||
provider: string;
|
||||
|
|
@ -87,6 +82,8 @@ export interface CustomModel {
|
|||
context?: number;
|
||||
believerExclusive?: boolean;
|
||||
capabilities?: ModelCapability[];
|
||||
displayName?: string;
|
||||
|
||||
// Embedding models only (Jina at the moment)
|
||||
dimensions?: number;
|
||||
// OpenAI specific fields
|
||||
|
|
|
|||
|
|
@ -8,8 +8,7 @@ import { COPILOT_TOOL_NAMES } from "@/LLMProviders/intentAnalyzer";
|
|||
import { Mention } from "@/mentions/Mention";
|
||||
import { getModelKeyFromModel, useSettingsValue } from "@/settings/model";
|
||||
import { getToolDescription } from "@/tools/toolManager";
|
||||
import { err2String, extractNoteTitles } from "@/utils";
|
||||
import { DisplayKeyProviders, ProviderSettingsKeyMap } from "@/constants";
|
||||
import { err2String, extractNoteTitles, checkModelApiKey } from "@/utils";
|
||||
import {
|
||||
ArrowBigUp,
|
||||
ChevronDown,
|
||||
|
|
@ -39,6 +38,7 @@ import {
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import { AddContextNoteModal } from "@/components/modals/AddContextNoteModal";
|
||||
import { ModelDisplay } from "@/components/ui/model-display";
|
||||
import { Notice } from "obsidian";
|
||||
|
||||
interface ChatInputProps {
|
||||
|
|
@ -488,10 +488,20 @@ const ChatInput = forwardRef<{ focus: () => void }, ChatInputProps>(
|
|||
<Button variant="ghost2" size="fit">
|
||||
{modelError ? (
|
||||
<span className="text-error">Model Load Failed</span>
|
||||
) : (
|
||||
settings.activeModels.find(
|
||||
) : settings.activeModels.find(
|
||||
(model) => model.enabled && getModelKeyFromModel(model) === currentModelKey
|
||||
)?.name || "Select Model"
|
||||
) ? (
|
||||
<ModelDisplay
|
||||
model={
|
||||
settings.activeModels.find(
|
||||
(model) =>
|
||||
model.enabled && getModelKeyFromModel(model) === currentModelKey
|
||||
)!
|
||||
}
|
||||
iconSize={8}
|
||||
/>
|
||||
) : (
|
||||
"Select Model"
|
||||
)}
|
||||
<ChevronDown className="size-5 mt-0.5" />
|
||||
</Button>
|
||||
|
|
@ -501,19 +511,14 @@ const ChatInput = forwardRef<{ focus: () => void }, ChatInputProps>(
|
|||
{settings.activeModels
|
||||
.filter((model) => model.enabled)
|
||||
.map((model) => {
|
||||
const providerKeyName =
|
||||
ProviderSettingsKeyMap[model.provider as DisplayKeyProviders];
|
||||
const hasNoApiKey = !model.apiKey && !settings[providerKeyName];
|
||||
const { hasApiKey, errorNotice } = checkModelApiKey(model, settings);
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={getModelKeyFromModel(model)}
|
||||
onSelect={async (event) => {
|
||||
if (hasNoApiKey) {
|
||||
if (!hasApiKey && errorNotice) {
|
||||
event.preventDefault();
|
||||
const notice =
|
||||
`Please configure API Key for ${model.name} in settings first.` +
|
||||
"\nPath: Settings > copilot plugin > Basic Tab > Set Keys";
|
||||
new Notice(notice);
|
||||
new Notice(errorNotice);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -526,16 +531,16 @@ const ChatInput = forwardRef<{ focus: () => void }, ChatInputProps>(
|
|||
new Notice(msg);
|
||||
// Restore to the last valid model
|
||||
const lastValidModel = settings.activeModels.find(
|
||||
(m) => getModelKeyFromModel(m) === currentModelKey
|
||||
(m) => m.enabled && getModelKeyFromModel(m) === currentModelKey
|
||||
);
|
||||
if (lastValidModel) {
|
||||
setCurrentModelKey(getModelKeyFromModel(lastValidModel));
|
||||
}
|
||||
}
|
||||
}}
|
||||
className={hasNoApiKey ? "opacity-50 cursor-not-allowed" : ""}
|
||||
className={!hasApiKey ? "opacity-50 cursor-not-allowed" : ""}
|
||||
>
|
||||
{model.name}
|
||||
<ModelDisplay model={model} iconSize={12} />
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
31
src/components/ui/form-field.tsx
Normal file
31
src/components/ui/form-field.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import React from "react";
|
||||
import { Label } from "./label";
|
||||
|
||||
interface FormFieldProps {
|
||||
label: string | React.ReactNode;
|
||||
required?: boolean;
|
||||
error?: boolean;
|
||||
description?: string;
|
||||
errorMessage?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const FormField: React.FC<FormFieldProps> = ({
|
||||
label,
|
||||
required = false,
|
||||
error = false,
|
||||
description,
|
||||
errorMessage = "This field is required",
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Label className={error ? "text-error" : ""}>
|
||||
{label} {required && <span className="text-error">*</span>}
|
||||
</Label>
|
||||
{children}
|
||||
{error && <p className="text-xs text-error">{errorMessage}</p>}
|
||||
{description && <p className="text-sm text-muted">{description}</p>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
101
src/components/ui/model-display.tsx
Normal file
101
src/components/ui/model-display.tsx
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
import React from "react";
|
||||
import { CustomModel } from "@/aiParams";
|
||||
import { getProviderLabel } from "@/utils";
|
||||
import { Lightbulb, Eye, Globe } from "lucide-react";
|
||||
import { ModelCapability } from "@/constants";
|
||||
|
||||
interface ModelDisplayProps {
|
||||
model: CustomModel;
|
||||
iconSize?: number;
|
||||
}
|
||||
|
||||
interface ModelCapabilityIconsProps {
|
||||
capabilities?: ModelCapability[];
|
||||
iconSize?: number;
|
||||
}
|
||||
|
||||
export const ModelCapabilityIcons: React.FC<ModelCapabilityIconsProps> = ({
|
||||
capabilities = [],
|
||||
iconSize = 16,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{capabilities.map((cap, index) => {
|
||||
switch (cap) {
|
||||
case ModelCapability.REASONING:
|
||||
return (
|
||||
<Lightbulb
|
||||
key={index}
|
||||
className="text-model-capabilities-blue"
|
||||
style={{ width: iconSize, height: iconSize }}
|
||||
/>
|
||||
);
|
||||
case ModelCapability.VISION:
|
||||
return (
|
||||
<Eye
|
||||
key={index}
|
||||
className="text-model-capabilities-green"
|
||||
style={{ width: iconSize, height: iconSize }}
|
||||
/>
|
||||
);
|
||||
case ModelCapability.WEB_SEARCH:
|
||||
return (
|
||||
<Globe
|
||||
key={index}
|
||||
className="text-model-capabilities-blue"
|
||||
style={{ width: iconSize, height: iconSize }}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const ModelDisplay: React.FC<ModelDisplayProps> = ({ model, iconSize = 14 }) => {
|
||||
const displayName = model.displayName || model.name;
|
||||
const provider = `(${getProviderLabel(model.provider)})`;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
<span>
|
||||
{displayName}
|
||||
<span className="text-[10px]">{provider}</span>
|
||||
</span>
|
||||
{model.capabilities && model.capabilities.length > 0 && (
|
||||
<div className="flex gap-0.5 items-center">
|
||||
<ModelCapabilityIcons capabilities={model.capabilities} iconSize={iconSize} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const getModelDisplayText = (model: CustomModel): string => {
|
||||
const displayName = model.displayName || model.name;
|
||||
const provider = `(${getProviderLabel(model.provider)})`;
|
||||
return `${displayName}${provider}`;
|
||||
};
|
||||
|
||||
export const getModelDisplayWithIcons = (model: CustomModel): string => {
|
||||
const displayName = model.displayName || model.name;
|
||||
const provider = `(${getProviderLabel(model.provider, model)})`;
|
||||
const icons =
|
||||
model.capabilities
|
||||
?.map((cap) => {
|
||||
switch (cap) {
|
||||
case ModelCapability.REASONING:
|
||||
return "Reasoning";
|
||||
case ModelCapability.VISION:
|
||||
return "Vision";
|
||||
case ModelCapability.WEB_SEARCH:
|
||||
return "Websearch";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
})
|
||||
.join("|") || "";
|
||||
return `${displayName}${provider} ${icons}`;
|
||||
};
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { CustomModel, ModelCapability } from "@/aiParams";
|
||||
import { CustomModel } from "@/aiParams";
|
||||
import { type CopilotSettings } from "@/settings/model";
|
||||
import { ChainType } from "./chainFactory";
|
||||
|
||||
|
|
@ -72,6 +72,18 @@ export enum ChatModelProviders {
|
|||
MISTRAL = "mistralai",
|
||||
}
|
||||
|
||||
export enum ModelCapability {
|
||||
REASONING = "reasoning",
|
||||
VISION = "vision",
|
||||
WEB_SEARCH = "websearch",
|
||||
}
|
||||
|
||||
export const MODEL_CAPABILITIES: Record<ModelCapability, string> = {
|
||||
reasoning: "This model supports general reasoning tasks.",
|
||||
vision: "This model supports image inputs.",
|
||||
websearch: "This model can access the internet.",
|
||||
};
|
||||
|
||||
export const BUILTIN_CHAT_MODELS: CustomModel[] = [
|
||||
{
|
||||
name: ChatModels.COPILOT_PLUS_FLASH,
|
||||
|
|
@ -95,6 +107,7 @@ export const BUILTIN_CHAT_MODELS: CustomModel[] = [
|
|||
enabled: true,
|
||||
isBuiltIn: true,
|
||||
core: true,
|
||||
capabilities: [ModelCapability.VISION],
|
||||
},
|
||||
{
|
||||
name: ChatModels.O1_mini,
|
||||
|
|
|
|||
|
|
@ -9,13 +9,11 @@ import {
|
|||
COMMAND_NAMES,
|
||||
DEFAULT_OPEN_AREA,
|
||||
DISABLEABLE_COMMANDS,
|
||||
DisplayKeyProviders,
|
||||
PLUS_UTM_MEDIUMS,
|
||||
ProviderSettingsKeyMap,
|
||||
} from "@/constants";
|
||||
import { useTab } from "@/contexts/TabContext";
|
||||
import { getModelKeyFromModel, updateSetting, useSettingsValue } from "@/settings/model";
|
||||
import { formatDateTime, getProviderLabel } from "@/utils";
|
||||
import { formatDateTime, checkModelApiKey } from "@/utils";
|
||||
import { HelpCircle, Key, Loader2 } from "lucide-react";
|
||||
import { Notice } from "obsidian";
|
||||
import React, { useState } from "react";
|
||||
|
|
@ -23,6 +21,8 @@ import ApiKeyDialog from "./ApiKeyDialog";
|
|||
import { PlusSettings } from "@/settings/v2/components/PlusSettings";
|
||||
import { TooltipProvider, Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { createPlusPageUrl } from "@/plusUtils";
|
||||
import { getModelDisplayWithIcons } from "@/components/ui/model-display";
|
||||
|
||||
const ChainType2Label: Record<ChainType, string> = {
|
||||
[ChainType.LLM_CHAIN]: "Chat",
|
||||
[ChainType.VAULT_QA_CHAIN]: "Vault QA (Basic)",
|
||||
|
|
@ -163,24 +163,19 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({ indexVaultToVectorStore }
|
|||
const selectedModel = settings.activeModels.find(
|
||||
(m) => m.enabled && getModelKeyFromModel(m) === value
|
||||
);
|
||||
if (selectedModel) {
|
||||
const providerKeyName =
|
||||
ProviderSettingsKeyMap[selectedModel.provider as DisplayKeyProviders];
|
||||
const hasNoApiKey = !selectedModel.apiKey && !settings[providerKeyName];
|
||||
if (hasNoApiKey) {
|
||||
const notice =
|
||||
`Please configure API Key for ${selectedModel.name} in settings first.` +
|
||||
"\nPath: Settings > copilot plugin > Basic Tab > Set Keys";
|
||||
new Notice(notice);
|
||||
return;
|
||||
}
|
||||
if (!selectedModel) return;
|
||||
|
||||
const { hasApiKey, errorNotice } = checkModelApiKey(selectedModel, settings);
|
||||
if (!hasApiKey && errorNotice) {
|
||||
new Notice(errorNotice);
|
||||
return;
|
||||
}
|
||||
updateSetting("defaultModelKey", value);
|
||||
}}
|
||||
options={settings.activeModels
|
||||
.filter((m) => m.enabled)
|
||||
.map((model) => ({
|
||||
label: `${model.name} (${getProviderLabel(model.provider, model)})`,
|
||||
label: getModelDisplayWithIcons(model),
|
||||
value: getModelKeyFromModel(model),
|
||||
}))}
|
||||
placeholder="Model"
|
||||
|
|
@ -219,7 +214,7 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({ indexVaultToVectorStore }
|
|||
value={settings.embeddingModelKey}
|
||||
onChange={handleSetDefaultEmbeddingModel}
|
||||
options={settings.activeEmbeddingModels.map((model) => ({
|
||||
label: `${model.name} (${getProviderLabel(model.provider, model)})`,
|
||||
label: getModelDisplayWithIcons(model),
|
||||
value: getModelKeyFromModel(model),
|
||||
}))}
|
||||
placeholder="Model"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,24 @@
|
|||
import React, { useState } from "react";
|
||||
import { useTab } from "@/contexts/TabContext";
|
||||
import { getSettings } from "@/settings/model";
|
||||
import {
|
||||
ChatModelProviders,
|
||||
DisplayKeyProviders,
|
||||
EmbeddingModelProviders,
|
||||
MODEL_CAPABILITIES,
|
||||
ModelCapability,
|
||||
Provider,
|
||||
ProviderMetadata,
|
||||
ProviderSettingsKeyMap,
|
||||
} from "@/constants";
|
||||
import { CustomModel } from "@/aiParams";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { err2String, getProviderInfo, getProviderLabel, omit } from "@/utils";
|
||||
import { Notice } from "obsidian";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ChevronDown, Loader2, HelpCircle } from "lucide-react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
|
|
@ -9,9 +26,6 @@ import {
|
|||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { PasswordInput } from "@/components/ui/password-input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
|
|
@ -19,49 +33,10 @@ import {
|
|||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
ChatModelProviders,
|
||||
DisplayKeyProviders,
|
||||
EmbeddingModelProviders,
|
||||
Provider,
|
||||
ProviderMetadata,
|
||||
ProviderSettingsKeyMap,
|
||||
} from "@/constants";
|
||||
import { useTab } from "@/contexts/TabContext";
|
||||
import { getSettings } from "@/settings/model";
|
||||
import { err2String, getProviderInfo, getProviderLabel, omit } from "@/utils";
|
||||
import { ChevronDown, Loader2 } from "lucide-react";
|
||||
import { Notice } from "obsidian";
|
||||
import React, { useState } from "react";
|
||||
|
||||
interface FormFieldProps {
|
||||
label: string;
|
||||
required?: boolean;
|
||||
error?: boolean;
|
||||
description?: string;
|
||||
errorMessage?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const FormField: React.FC<FormFieldProps> = ({
|
||||
label,
|
||||
required = false,
|
||||
error = false,
|
||||
description,
|
||||
errorMessage = "This field is required",
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Label className={error ? "text-error" : ""}>
|
||||
{label} {required && <span className="text-error">*</span>}
|
||||
</Label>
|
||||
{children}
|
||||
{error && <p className="text-xs text-error">{errorMessage}</p>}
|
||||
{description && <p className="text-sm text-muted">{description}</p>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
import { PasswordInput } from "@/components/ui/password-input";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { FormField } from "@/components/ui/form-field";
|
||||
|
||||
interface FormErrors {
|
||||
name: boolean;
|
||||
|
|
@ -69,6 +44,7 @@ interface FormErrors {
|
|||
deploymentName: boolean;
|
||||
embeddingDeploymentName: boolean;
|
||||
apiVersion: boolean;
|
||||
displayName: boolean;
|
||||
}
|
||||
|
||||
interface ModelAddDialogProps {
|
||||
|
|
@ -101,6 +77,7 @@ export const ModelAddDialog: React.FC<ModelAddDialogProps> = ({
|
|||
deploymentName: false,
|
||||
embeddingDeploymentName: false,
|
||||
apiVersion: false,
|
||||
displayName: false,
|
||||
});
|
||||
|
||||
const setError = (field: keyof FormErrors, value: boolean) => {
|
||||
|
|
@ -114,6 +91,7 @@ export const ModelAddDialog: React.FC<ModelAddDialogProps> = ({
|
|||
deploymentName: false,
|
||||
embeddingDeploymentName: false,
|
||||
apiVersion: false,
|
||||
displayName: false,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -160,6 +138,7 @@ export const ModelAddDialog: React.FC<ModelAddDialogProps> = ({
|
|||
baseUrl: "",
|
||||
apiKey: getDefaultApiKey(provider),
|
||||
isEmbeddingModel,
|
||||
capabilities: [],
|
||||
};
|
||||
|
||||
if (!isEmbeddingModel) {
|
||||
|
|
@ -173,6 +152,23 @@ export const ModelAddDialog: React.FC<ModelAddDialogProps> = ({
|
|||
};
|
||||
|
||||
const [model, setModel] = useState<CustomModel>(getInitialModel());
|
||||
|
||||
// Clean up model data by trimming whitespace
|
||||
const getCleanedModel = (modelData: CustomModel): CustomModel => {
|
||||
return {
|
||||
...modelData,
|
||||
name: modelData.name?.trim(),
|
||||
baseUrl: modelData.baseUrl?.trim(),
|
||||
apiKey: modelData.apiKey?.trim(),
|
||||
openAIOrgId: modelData.openAIOrgId?.trim(),
|
||||
azureOpenAIApiInstanceName: modelData.azureOpenAIApiInstanceName?.trim(),
|
||||
azureOpenAIApiDeploymentName: modelData.azureOpenAIApiDeploymentName?.trim(),
|
||||
azureOpenAIApiEmbeddingDeploymentName:
|
||||
modelData.azureOpenAIApiEmbeddingDeploymentName?.trim(),
|
||||
azureOpenAIApiVersion: modelData.azureOpenAIApiVersion?.trim(),
|
||||
};
|
||||
};
|
||||
|
||||
const [providerInfo, setProviderInfo] = useState<ProviderMetadata>(
|
||||
getProviderInfo(defaultProvider)
|
||||
);
|
||||
|
|
@ -193,7 +189,8 @@ export const ModelAddDialog: React.FC<ModelAddDialogProps> = ({
|
|||
return;
|
||||
}
|
||||
|
||||
onAdd(model);
|
||||
const cleanedModel = getCleanedModel(model);
|
||||
onAdd(cleanedModel);
|
||||
onOpenChange(false);
|
||||
setModel(getInitialModel());
|
||||
clearErrors();
|
||||
|
|
@ -233,7 +230,8 @@ export const ModelAddDialog: React.FC<ModelAddDialogProps> = ({
|
|||
|
||||
setIsVerifying(true);
|
||||
try {
|
||||
await ping(model);
|
||||
const cleanedModel = getCleanedModel(model);
|
||||
await ping(cleanedModel);
|
||||
new Notice("Model verification successful!");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
|
@ -381,6 +379,12 @@ export const ModelAddDialog: React.FC<ModelAddDialogProps> = ({
|
|||
return `https://${instanceName}.openai.azure.com/openai/deployments/${deploymentName}/${endpoint}?api-version=${apiVersion}`;
|
||||
};
|
||||
|
||||
const capabilityOptions = Object.entries(MODEL_CAPABILITIES).map(([id, description]) => ({
|
||||
id,
|
||||
label: id.charAt(0).toUpperCase() + id.slice(1),
|
||||
description,
|
||||
})) as Array<{ id: ModelCapability; label: string; description: string }>;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogContent
|
||||
|
|
@ -393,7 +397,7 @@ export const ModelAddDialog: React.FC<ModelAddDialogProps> = ({
|
|||
<DialogDescription>Add a new model to your collection.</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-3">
|
||||
<FormField
|
||||
label="Model Name"
|
||||
required
|
||||
|
|
@ -413,6 +417,42 @@ export const ModelAddDialog: React.FC<ModelAddDialogProps> = ({
|
|||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
label={
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="leading-none">Display Name</span>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<HelpCircle className="size-4" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent align="start" className="max-w-96" side="bottom">
|
||||
<div className="text-sm text-muted flex flex-col gap-0.5">
|
||||
<div className="text-[12px] font-bold">Suggested format:</div>
|
||||
<div className="text-accent">[Source]-[Payment]:[Pretty Model Name]</div>
|
||||
<div className="text-[12px]">
|
||||
Example:
|
||||
<li>Direct-Paid:Ds-r1</li>
|
||||
<li>OpenRouter-Paid:Ds-r1</li>
|
||||
<li>Perplexity-Paid:lg</li>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Custom display name (optional)"
|
||||
value={model.displayName || ""}
|
||||
onChange={(e) => {
|
||||
setModel({ ...model, displayName: e.target.value });
|
||||
}}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Provider">
|
||||
<Select value={model.provider} onValueChange={handleProviderChange}>
|
||||
<SelectTrigger>
|
||||
|
|
@ -456,6 +496,38 @@ export const ModelAddDialog: React.FC<ModelAddDialogProps> = ({
|
|||
)}
|
||||
</FormField>
|
||||
|
||||
<FormField label="Model Capabilities">
|
||||
<div className="flex gap-4 items-center">
|
||||
{capabilityOptions.map(({ id, label, description }) => (
|
||||
<div key={id} className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id={id}
|
||||
checked={model.capabilities?.includes(id)}
|
||||
onCheckedChange={(checked) => {
|
||||
const newCapabilities = model.capabilities || [];
|
||||
setModel({
|
||||
...model,
|
||||
capabilities: checked
|
||||
? [...newCapabilities, id]
|
||||
: newCapabilities.filter((cap) => cap !== id),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor={id} className="text-sm">
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span>{label}</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">{description}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</FormField>
|
||||
|
||||
{renderProviderSpecificFields()}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -9,8 +9,13 @@ import {
|
|||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { HelpCircle } from "lucide-react";
|
||||
import { SettingSlider } from "@/components/ui/setting-slider";
|
||||
import { SettingSwitch } from "@/components/ui/setting-switch";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { FormField } from "@/components/ui/form-field";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { MODEL_CAPABILITIES, ModelCapability } from "@/constants";
|
||||
|
||||
interface ModelEditDialogProps {
|
||||
open: boolean;
|
||||
|
|
@ -43,6 +48,12 @@ export const ModelEditDialog: React.FC<ModelEditDialogProps> = ({
|
|||
onUpdate(updatedModel);
|
||||
};
|
||||
|
||||
const capabilityOptions = Object.entries(MODEL_CAPABILITIES).map(([id, description]) => ({
|
||||
id,
|
||||
label: id.charAt(0).toUpperCase() + id.slice(1),
|
||||
description,
|
||||
})) as Array<{ id: ModelCapability; label: string; description: string }>;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[425px]" container={modalContainer}>
|
||||
|
|
@ -52,46 +63,152 @@ export const ModelEditDialog: React.FC<ModelEditDialogProps> = ({
|
|||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
Temperature
|
||||
<HelpCircle className="h-4 w-4 text-muted" />
|
||||
</div>
|
||||
</div>
|
||||
<SettingSlider
|
||||
value={localModel.temperature ?? 0.1}
|
||||
onChange={(value) => handleUpdate("temperature", value)}
|
||||
max={2}
|
||||
min={0}
|
||||
step={0.1}
|
||||
<FormField label="Model Name" required>
|
||||
<Input
|
||||
type="text"
|
||||
disabled={localModel.core}
|
||||
value={localModel.name}
|
||||
onChange={(e) => handleUpdate("name", e.target.value)}
|
||||
placeholder="Enter model name"
|
||||
/>
|
||||
</div>
|
||||
</FormField>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
Context
|
||||
<HelpCircle className="h-4 w-4 text-muted" />
|
||||
<FormField
|
||||
label={
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="leading-none">Display Name</span>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<HelpCircle className="size-4" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent align="start" className="max-w-96" side="bottom">
|
||||
<div className="text-sm text-muted flex flex-col gap-0.5">
|
||||
<div className="text-[12px] font-bold">Suggested format:</div>
|
||||
<div className="text-accent">[Source]-[Payment]:[Pretty Model Name]</div>
|
||||
<div className="text-[12px]">
|
||||
Example:
|
||||
<li>Direct-Paid:Ds-r1</li>
|
||||
<li>OpenRouter-Paid:Ds-r1</li>
|
||||
<li>Perplexity-Paid:lg</li>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
<SettingSlider
|
||||
value={localModel.context ?? 1000}
|
||||
onChange={(value) => handleUpdate("context", value)}
|
||||
max={16000}
|
||||
min={0}
|
||||
step={100}
|
||||
}
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Custom display name (optional)"
|
||||
value={localModel.displayName || ""}
|
||||
onChange={(e) => handleUpdate("displayName", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</FormField>
|
||||
|
||||
<div className="py-2 border-b">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">Stream output</div>
|
||||
<SettingSwitch
|
||||
checked={localModel.stream ?? true}
|
||||
onCheckedChange={(checked) => handleUpdate("stream", checked)}
|
||||
<FormField label="Model Capabilities">
|
||||
<div className="flex gap-4 items-center">
|
||||
{capabilityOptions.map(({ id, label, description }) => (
|
||||
<div key={id} className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id={id}
|
||||
checked={localModel.capabilities?.includes(id)}
|
||||
onCheckedChange={(checked) => {
|
||||
const newCapabilities = localModel.capabilities || [];
|
||||
handleUpdate(
|
||||
"capabilities",
|
||||
checked
|
||||
? [...newCapabilities, id]
|
||||
: newCapabilities.filter((cap) => cap !== id)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor={id} className="text-sm">
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span>{label}</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">{description}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</FormField>
|
||||
|
||||
{/* <FormField
|
||||
label={
|
||||
<div className="flex items-center gap-2">
|
||||
Temperature
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<HelpCircle className="h-4 w-4 text-muted" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
Controls randomness: 0 is focused and deterministic, 2 is more creative
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<SettingSlider
|
||||
value={localModel.temperature ?? 0.1}
|
||||
onChange={(value) => handleUpdate("temperature", value)}
|
||||
max={2}
|
||||
min={0}
|
||||
step={0.1}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
label={
|
||||
<div className="flex items-center gap-2">
|
||||
Context
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<HelpCircle className="h-4 w-4 text-muted" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
Maximum number of tokens to use for context
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<SettingSlider
|
||||
value={localModel.context ?? 1000}
|
||||
onChange={(value) => handleUpdate("context", value)}
|
||||
max={16000}
|
||||
min={0}
|
||||
step={100}
|
||||
/>
|
||||
</FormField>*/}
|
||||
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium">Stream output</span>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<HelpCircle className="h-4 w-4 text-muted" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
Enable streaming responses from the model
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<SettingSwitch
|
||||
checked={localModel.stream ?? true}
|
||||
onCheckedChange={(checked) => handleUpdate("stream", checked)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
|
|
|||
|
|
@ -6,10 +6,11 @@ import ChatModelManager from "@/LLMProviders/chatModelManager";
|
|||
import EmbeddingManager from "@/LLMProviders/embeddingManager";
|
||||
import { ModelAddDialog } from "@/settings/v2/components/ModelAddDialog";
|
||||
import { ModelTable } from "@/settings/v2/components/ModelTable";
|
||||
import { ModelEditDialog } from "@/settings/v2/components/ModelEditDialog";
|
||||
|
||||
const ModelSettings: React.FC = () => {
|
||||
const settings = useSettingsValue();
|
||||
// const [editingModel, setEditingModel] = useState<CustomModel | null>(null);
|
||||
const [editingModel, setEditingModel] = useState<CustomModel | null>(null);
|
||||
const [showAddDialog, setShowAddDialog] = useState(false);
|
||||
const [showAddEmbeddingDialog, setShowAddEmbeddingDialog] = useState(false);
|
||||
|
||||
|
|
@ -40,6 +41,10 @@ const ModelSettings: React.FC = () => {
|
|||
updateSetting("activeModels", updatedModels);
|
||||
};
|
||||
|
||||
const handleModelReorder = (newModels: CustomModel[]) => {
|
||||
updateSetting("activeModels", newModels);
|
||||
};
|
||||
|
||||
const onDeleteEmbeddingModel = (modelKey: string) => {
|
||||
const [modelName, provider] = modelKey.split("|");
|
||||
const updatedModels = settings.activeEmbeddingModels.filter(
|
||||
|
|
@ -55,26 +60,31 @@ const ModelSettings: React.FC = () => {
|
|||
updateSetting("activeEmbeddingModels", updatedModels);
|
||||
};
|
||||
|
||||
const handleEmbeddingModelReorder = (newModels: CustomModel[]) => {
|
||||
updateSetting("activeEmbeddingModels", newModels);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<section>
|
||||
<div className="text-xl font-bold mb-3">Chat Models</div>
|
||||
<ModelTable
|
||||
models={settings.activeModels}
|
||||
// onEdit={setEditingModel}
|
||||
onEdit={setEditingModel}
|
||||
onDelete={onDeleteModel}
|
||||
onAdd={() => setShowAddDialog(true)}
|
||||
onUpdateModel={handleModelUpdate}
|
||||
onReorderModels={handleModelReorder}
|
||||
title="Chat Model"
|
||||
/>
|
||||
|
||||
{/* model edit dialog*/}
|
||||
{/* <ModelEditDialog
|
||||
<ModelEditDialog
|
||||
open={!!editingModel}
|
||||
onOpenChange={(open) => !open && setEditingModel(null)}
|
||||
model={editingModel}
|
||||
onUpdate={handleModelUpdate}
|
||||
/>*/}
|
||||
/>
|
||||
|
||||
{/* model add dialog */}
|
||||
<ModelAddDialog
|
||||
|
|
@ -140,6 +150,7 @@ const ModelSettings: React.FC = () => {
|
|||
onDelete={onDeleteEmbeddingModel}
|
||||
onAdd={() => setShowAddEmbeddingDialog(true)}
|
||||
onUpdateModel={handleEmbeddingModelUpdate}
|
||||
onReorderModels={handleEmbeddingModelReorder}
|
||||
title="Embedding Model"
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -13,8 +13,87 @@ import {
|
|||
import { cn } from "@/lib/utils";
|
||||
import { getModelKeyFromModel } from "@/settings/model";
|
||||
import { getProviderLabel } from "@/utils";
|
||||
import { ChevronDown, ChevronRight, Pencil, Plus, Trash2 } from "lucide-react";
|
||||
import React from "react";
|
||||
import {
|
||||
Move,
|
||||
Pencil,
|
||||
Plus,
|
||||
Trash2,
|
||||
Lightbulb,
|
||||
Eye,
|
||||
Globe,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
LucideProps,
|
||||
} from "lucide-react";
|
||||
import React, { ForwardRefExoticComponent, RefAttributes } from "react";
|
||||
import {
|
||||
DndContext,
|
||||
closestCenter,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
DragEndEvent,
|
||||
} from "@dnd-kit/core";
|
||||
import {
|
||||
arrayMove,
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
useSortable,
|
||||
verticalListSortingStrategy,
|
||||
} from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { ModelCapabilityIcons } from "@/components/ui/model-display";
|
||||
import { MODEL_CAPABILITIES, ModelCapability } from "@/constants";
|
||||
|
||||
const CAPABILITY_ICONS: Record<
|
||||
ModelCapability,
|
||||
{
|
||||
icon: ForwardRefExoticComponent<Omit<LucideProps, "ref"> & RefAttributes<SVGSVGElement>>;
|
||||
color: string;
|
||||
tooltip: string;
|
||||
}
|
||||
> = {
|
||||
[ModelCapability.REASONING]: {
|
||||
icon: Lightbulb,
|
||||
color: "text-model-capabilities-blue",
|
||||
tooltip: MODEL_CAPABILITIES.reasoning,
|
||||
},
|
||||
[ModelCapability.VISION]: {
|
||||
icon: Eye,
|
||||
color: "text-model-capabilities-green",
|
||||
tooltip: MODEL_CAPABILITIES.vision,
|
||||
},
|
||||
[ModelCapability.WEB_SEARCH]: {
|
||||
icon: Globe,
|
||||
color: "text-model-capabilities-blue",
|
||||
tooltip: MODEL_CAPABILITIES.websearch,
|
||||
},
|
||||
} as const;
|
||||
|
||||
const renderCapabilities = (model: CustomModel) => {
|
||||
return (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<div className="flex gap-2 items-center justify-center">
|
||||
{model.capabilities?.map((capability) => {
|
||||
const config = CAPABILITY_ICONS[capability];
|
||||
if (!config) return null;
|
||||
|
||||
const Icon = config.icon;
|
||||
return (
|
||||
<Tooltip key={capability}>
|
||||
<TooltipTrigger asChild>
|
||||
<Icon className={`h-4 w-4 ${config.color}`} />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">{config.tooltip}</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
);
|
||||
};
|
||||
|
||||
interface ModelTableProps {
|
||||
models: CustomModel[];
|
||||
|
|
@ -22,42 +101,88 @@ interface ModelTableProps {
|
|||
onDelete: (modelKey: string) => void;
|
||||
onAdd: () => void;
|
||||
onUpdateModel: (model: CustomModel) => void;
|
||||
onReorderModels?: (newModels: CustomModel[]) => void;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const ModelCard: React.FC<{
|
||||
interface ModelCardProps {
|
||||
model: CustomModel;
|
||||
onEdit?: () => void;
|
||||
onDelete?: () => void;
|
||||
onUpdateModel: (model: CustomModel) => void;
|
||||
}> = ({ model, onEdit, onDelete, onUpdateModel }) => {
|
||||
id: string;
|
||||
}
|
||||
|
||||
const ModelCard: React.FC<ModelCardProps> = ({ model, onEdit, onDelete, onUpdateModel, id }) => {
|
||||
const [isExpanded, setIsExpanded] = React.useState(false);
|
||||
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||
id,
|
||||
disabled: model.core,
|
||||
});
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="mb-2">
|
||||
<Card
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
className={cn(
|
||||
"mb-2",
|
||||
isDragging && [
|
||||
"opacity-90",
|
||||
"shadow-lg",
|
||||
"border-2",
|
||||
"border-accent/50",
|
||||
"relative",
|
||||
"z-[9999]",
|
||||
"bg-background",
|
||||
"rounded-lg",
|
||||
"transform-gpu",
|
||||
],
|
||||
!model.core && "touch-none"
|
||||
)}
|
||||
>
|
||||
<CardHeader className="p-3">
|
||||
<div
|
||||
className="flex items-center justify-between cursor-pointer border-l-[0] border-r-[0] border-t-[0] border-b border-solid border-border pb-2"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 flex items-center justify-center">
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="h-3 w-3 stroke-[7]" />
|
||||
) : (
|
||||
<ChevronRight className="h-3 w-3 stroke-[7]" />
|
||||
)}
|
||||
<div className="flex items-center justify-between">
|
||||
{!model.core && (
|
||||
<div
|
||||
className="mr-2 touch-none cursor-grab active:cursor-grabbing"
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
>
|
||||
<Move className="h-4 w-4 text-muted-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium">{model.name}</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-muted bg-secondary">
|
||||
{getProviderLabel(model.provider, model)}
|
||||
</span>
|
||||
{model.believerExclusive && <span className="text-sm text-accent">(Believer)</span>}
|
||||
)}
|
||||
|
||||
<div className="flex-1 cursor-pointer" onClick={() => setIsExpanded(!isExpanded)}>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 flex items-center justify-center">
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="h-3 w-3 stroke-[7]" />
|
||||
) : (
|
||||
<ChevronRight className="h-3 w-3 stroke-[7]" />
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="font-medium">{model.displayName || model.name}</span>
|
||||
{model.capabilities && model.capabilities.length > 0 && (
|
||||
<ModelCapabilityIcons capabilities={model.capabilities} iconSize={14} />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-muted bg-secondary">
|
||||
{getProviderLabel(model.provider, model)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{onEdit && (
|
||||
<Button
|
||||
|
|
@ -121,104 +246,260 @@ const ModelCard: React.FC<{
|
|||
);
|
||||
};
|
||||
|
||||
const SortableTableRow: React.FC<{
|
||||
model: CustomModel;
|
||||
onEdit?: (model: CustomModel) => void;
|
||||
onDelete: (modelKey: string) => void;
|
||||
onUpdateModel: (model: CustomModel) => void;
|
||||
isEmbeddingModel: boolean;
|
||||
}> = ({ model, onEdit, onDelete, onUpdateModel, isEmbeddingModel }) => {
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||
id: getModelKeyFromModel(model),
|
||||
disabled: model.core,
|
||||
});
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
};
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
className={cn(
|
||||
"hover:bg-interactive-accent/10 transition-colors duration-200",
|
||||
isDragging &&
|
||||
"shadow-lg bg-background/90 backdrop-blur-sm relative z-[100] cursor-grabbing border-2 border-accent/50",
|
||||
!isDragging && "z-auto"
|
||||
)}
|
||||
>
|
||||
<TableCell className="w-6 px-2">
|
||||
{!model.core && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="cursor-grab touch-none hover:cursor-grab active:cursor-grabbing p-0 h-6 w-6"
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
>
|
||||
<Move className="h-4 w-4 text-muted-foreground hover:text-foreground transition-colors" />
|
||||
</Button>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="pl-0">{model.displayName || model.name}</TableCell>
|
||||
<TableCell>{getProviderLabel(model.provider, model)}</TableCell>
|
||||
<TableCell className="text-center">{renderCapabilities(model)}</TableCell>
|
||||
{!isEmbeddingModel && (
|
||||
<TableCell className="text-center">
|
||||
<Checkbox
|
||||
id={`${getModelKeyFromModel(model)}-enabled`}
|
||||
checked={model.enabled}
|
||||
onCheckedChange={(checked: boolean) => onUpdateModel({ ...model, enabled: checked })}
|
||||
className="mx-auto"
|
||||
/>
|
||||
</TableCell>
|
||||
)}
|
||||
<TableCell className="text-center">
|
||||
<Checkbox
|
||||
id={`${getModelKeyFromModel(model)}-enableCors`}
|
||||
checked={model.enableCors}
|
||||
onCheckedChange={(checked: boolean) => onUpdateModel({ ...model, enableCors: checked })}
|
||||
className="mx-auto"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<div className="flex justify-center gap-2">
|
||||
{onEdit && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => onEdit(model)}
|
||||
className="shadow-sm hover:shadow-md transition-shadow"
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
{!model.core && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => onDelete(getModelKeyFromModel(model))}
|
||||
className="shadow-sm hover:shadow-md transition-shadow"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
};
|
||||
|
||||
export const ModelTable: React.FC<ModelTableProps> = ({
|
||||
models,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onAdd,
|
||||
onUpdateModel,
|
||||
onReorderModels,
|
||||
title,
|
||||
}) => {
|
||||
const isEmbeddingModel = models.length > 0 && models[0].isEmbeddingModel;
|
||||
const isEmbeddingModel = !!(models.length > 0 && models[0].isEmbeddingModel);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
})
|
||||
);
|
||||
|
||||
// Find the index of the first non-core model
|
||||
const firstDraggableIndex = models.findIndex((model) => !model.core);
|
||||
|
||||
// Create unified modifier logic
|
||||
const createDragModifier = (isMobile: boolean) => (args: any) => {
|
||||
const { transform, active, activeNodeRect, over } = args;
|
||||
if (!active || !activeNodeRect) return transform;
|
||||
|
||||
// Get the index of current dragging item
|
||||
const currentIndex = models.findIndex((model) => getModelKeyFromModel(model) === active.id);
|
||||
|
||||
// Calculate the number of non-core items
|
||||
const draggableItemsCount = models.filter((model) => !model.core).length;
|
||||
|
||||
// Calculate row height
|
||||
const rowHeight = activeNodeRect.height;
|
||||
|
||||
// Calculate draggable range
|
||||
const minY = (firstDraggableIndex - currentIndex) * rowHeight;
|
||||
const maxY = (firstDraggableIndex + draggableItemsCount - 1 - currentIndex) * rowHeight;
|
||||
|
||||
// For mobile view, check if hovering over a core model
|
||||
if (isMobile && over) {
|
||||
const overIndex = models.findIndex((model) => getModelKeyFromModel(model) === over.id);
|
||||
const overModel = models[overIndex];
|
||||
|
||||
// If hovering over a core model, return to original position
|
||||
if (overModel.core || overIndex < firstDraggableIndex) {
|
||||
return {
|
||||
...transform,
|
||||
x: 0,
|
||||
y: currentIndex * rowHeight,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Restrict within draggable range
|
||||
return {
|
||||
...transform,
|
||||
x: 0,
|
||||
y: Math.min(Math.max(minY, transform.y), maxY),
|
||||
};
|
||||
};
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
|
||||
if (over && active.id !== over.id) {
|
||||
const oldIndex = models.findIndex((model) => getModelKeyFromModel(model) === active.id);
|
||||
const newIndex = models.findIndex((model) => getModelKeyFromModel(model) === over.id);
|
||||
|
||||
// Get target model
|
||||
const targetModel = models[newIndex];
|
||||
|
||||
// 1. Prevent moving to core model positions
|
||||
if (newIndex < firstDraggableIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Prevent moving to other non-draggable model positions
|
||||
if (targetModel.core) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newModels = arrayMove(models, oldIndex, newIndex);
|
||||
onReorderModels?.(newModels);
|
||||
}
|
||||
};
|
||||
|
||||
// Mobile view rendering
|
||||
const renderMobileView = () => (
|
||||
<div className="md:hidden relative">
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleDragEnd}
|
||||
modifiers={[createDragModifier(true)]}
|
||||
>
|
||||
<SortableContext
|
||||
items={models.map((model) => getModelKeyFromModel(model))}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<div className="space-y-2 relative">
|
||||
{models.map((model) => (
|
||||
<ModelCard
|
||||
key={getModelKeyFromModel(model)}
|
||||
id={getModelKeyFromModel(model)}
|
||||
model={model}
|
||||
onEdit={onEdit ? () => onEdit(model) : undefined}
|
||||
onDelete={() => onDelete(getModelKeyFromModel(model))}
|
||||
onUpdateModel={onUpdateModel}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
{/* Desktop View */}
|
||||
{/* Desktop view */}
|
||||
<div className="hidden md:block">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Model</TableHead>
|
||||
<TableHead>Provider</TableHead>
|
||||
{!isEmbeddingModel && <TableHead className="text-center">Enable</TableHead>}
|
||||
<TableHead className="text-center">CORS</TableHead>
|
||||
<TableHead className="w-[100px] text-center">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{models.map((model) => (
|
||||
<TableRow
|
||||
key={getModelKeyFromModel(model)}
|
||||
className="hover:bg-interactive-accent/10"
|
||||
>
|
||||
<TableCell>{model.name}</TableCell>
|
||||
<TableCell>{getProviderLabel(model.provider, model)}</TableCell>
|
||||
{!isEmbeddingModel && (
|
||||
<TableCell className="text-center">
|
||||
<Checkbox
|
||||
id={`${getModelKeyFromModel(model)}-enabled`}
|
||||
checked={model.enabled}
|
||||
onCheckedChange={(checked: boolean) =>
|
||||
onUpdateModel({ ...model, enabled: checked })
|
||||
}
|
||||
className="mx-auto"
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleDragEnd}
|
||||
modifiers={[createDragModifier(false)]}
|
||||
>
|
||||
<div className="relative overflow-hidden">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-6 px-2"></TableHead>
|
||||
<TableHead className="pl-0">Model</TableHead>
|
||||
<TableHead>Provider</TableHead>
|
||||
<TableHead className="text-center">Capabilities</TableHead>
|
||||
{!isEmbeddingModel && <TableHead className="text-center">Enable</TableHead>}
|
||||
<TableHead className="text-center">CORS</TableHead>
|
||||
<TableHead className="w-[100px] text-center">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody className="relative">
|
||||
<SortableContext
|
||||
items={models.map((model) => getModelKeyFromModel(model))}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
{models.map((model) => (
|
||||
<SortableTableRow
|
||||
key={getModelKeyFromModel(model)}
|
||||
model={model}
|
||||
onEdit={onEdit ? () => onEdit(model) : undefined}
|
||||
onDelete={onDelete}
|
||||
onUpdateModel={onUpdateModel}
|
||||
isEmbeddingModel={isEmbeddingModel}
|
||||
/>
|
||||
</TableCell>
|
||||
)}
|
||||
<TableCell className="text-center">
|
||||
<Checkbox
|
||||
id={`${getModelKeyFromModel(model)}-enableCors`}
|
||||
checked={model.enableCors}
|
||||
onCheckedChange={(checked: boolean) =>
|
||||
onUpdateModel({ ...model, enableCors: checked })
|
||||
}
|
||||
className="mx-auto"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<div className="flex justify-center gap-2">
|
||||
{onEdit && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => onEdit(model)}
|
||||
className="shadow-sm hover:shadow-md transition-shadow"
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
{!model.core && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => onDelete(getModelKeyFromModel(model))}
|
||||
className="shadow-sm hover:shadow-md transition-shadow"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
))}
|
||||
</SortableContext>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</DndContext>
|
||||
</div>
|
||||
|
||||
{/* Mobile View */}
|
||||
<div className="md:hidden">
|
||||
<div className="space-y-2">
|
||||
{models.map((model) => (
|
||||
<ModelCard
|
||||
key={getModelKeyFromModel(model)}
|
||||
model={model}
|
||||
onEdit={onEdit ? () => onEdit(model) : undefined}
|
||||
onDelete={() => onDelete(getModelKeyFromModel(model))}
|
||||
onUpdateModel={onUpdateModel}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{/* Mobile view */}
|
||||
{renderMobileView()}
|
||||
|
||||
<div className="mt-4 flex justify-end">
|
||||
<Button onClick={onAdd} variant="outline" className="flex items-center gap-2">
|
||||
|
|
|
|||
33
src/utils.ts
33
src/utils.ts
|
|
@ -5,6 +5,8 @@ import {
|
|||
ProviderInfo,
|
||||
ProviderMetadata,
|
||||
USER_SENDER,
|
||||
DisplayKeyProviders,
|
||||
ProviderSettingsKeyMap,
|
||||
} from "@/constants";
|
||||
import { ChatMessage } from "@/sharedState";
|
||||
import { BaseChatModel } from "@langchain/core/language_models/chat_models";
|
||||
|
|
@ -14,6 +16,7 @@ import { BaseChain, RetrievalQAChain } from "langchain/chains";
|
|||
import moment from "moment";
|
||||
import { MarkdownView, Notice, TFile, Vault, requestUrl } from "obsidian";
|
||||
import { CustomModel } from "./aiParams";
|
||||
import { CopilotSettings } from "@/settings/model";
|
||||
|
||||
// Add custom error type at the top of the file
|
||||
interface APIError extends Error {
|
||||
|
|
@ -780,10 +783,7 @@ export function getProviderInfo(provider: string): ProviderMetadata {
|
|||
|
||||
export function getProviderLabel(provider: string, model?: CustomModel): string {
|
||||
const baseLabel = ProviderInfo[provider as Provider]?.label || provider;
|
||||
if (model?.believerExclusive && baseLabel === "Copilot Plus") {
|
||||
return "Believer";
|
||||
}
|
||||
return baseLabel;
|
||||
return baseLabel + (model?.believerExclusive && baseLabel === "Copilot Plus" ? "(Believer)" : "");
|
||||
}
|
||||
|
||||
export function getProviderHost(provider: string): string {
|
||||
|
|
@ -885,3 +885,28 @@ export function getMessageRole(
|
|||
): "system" | "human" {
|
||||
return isOSeriesModel(model) ? "human" : defaultRole;
|
||||
}
|
||||
|
||||
export function checkModelApiKey(
|
||||
model: CustomModel,
|
||||
settings: Readonly<CopilotSettings>
|
||||
): {
|
||||
hasApiKey: boolean;
|
||||
errorNotice?: string;
|
||||
} {
|
||||
const providerKeyName = ProviderSettingsKeyMap[model.provider as DisplayKeyProviders];
|
||||
const hasNoApiKey = !model.apiKey && !settings[providerKeyName];
|
||||
|
||||
if (hasNoApiKey) {
|
||||
const notice =
|
||||
`Please configure API Key for ${model.name} in settings first.` +
|
||||
"\nPath: Settings > copilot plugin > Basic Tab > Set Keys";
|
||||
return {
|
||||
hasApiKey: false,
|
||||
errorNotice: notice,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
hasApiKey: true,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,10 @@ module.exports = {
|
|||
callout: {
|
||||
warning: "rgba(var(--callout-warning),<alpha-value>)",
|
||||
},
|
||||
"model-capabilities": {
|
||||
green: "var(--color-green)",
|
||||
blue: "var(--color-blue)",
|
||||
},
|
||||
},
|
||||
backgroundColor: {
|
||||
inherit: colors.inherit,
|
||||
|
|
|
|||
Loading…
Reference in a new issue