diff --git a/src/LLMProviders/chatModelManager.ts b/src/LLMProviders/chatModelManager.ts index 6c2627a6..b789c3b2 100644 --- a/src/LLMProviders/chatModelManager.ts +++ b/src/LLMProviders/chatModelManager.ts @@ -122,7 +122,11 @@ export default class ChatModelManager { fetch: customModel.enableCors ? safeFetch : undefined, organization: await getDecryptedKey(customModel.openAIOrgId || settings.openAIOrgId), }, - ...this.handleOpenAIExtraArgs(isOSeries, settings.maxTokens, settings.temperature), + ...this.handleOpenAIExtraArgs( + isOSeries, + customModel.maxTokens ?? settings.maxTokens, + customModel.temperature ?? settings.temperature + ), }, [ChatModelProviders.ANTHROPIC]: { anthropicApiKey: await getDecryptedKey(customModel.apiKey || settings.anthropicApiKey), @@ -156,7 +160,11 @@ export default class ChatModelManager { }, fetch: customModel.enableCors ? safeFetch : undefined, }, - ...this.handleOpenAIExtraArgs(isOSeries, settings.maxTokens, settings.temperature), + ...this.handleOpenAIExtraArgs( + isOSeries, + customModel.maxTokens ?? settings.maxTokens, + customModel.temperature ?? settings.temperature + ), }, [ChatModelProviders.COHEREAI]: { apiKey: await getDecryptedKey(customModel.apiKey || settings.cohereApiKey), @@ -231,7 +239,11 @@ export default class ChatModelManager { fetch: customModel.enableCors ? safeFetch : undefined, defaultHeaders: { "dangerously-allow-browser": "true" }, }, - ...this.handleOpenAIExtraArgs(isOSeries, settings.maxTokens, settings.temperature), + ...this.handleOpenAIExtraArgs( + isOSeries, + customModel.maxTokens ?? settings.maxTokens, + customModel.temperature ?? settings.temperature + ), }, [ChatModelProviders.COPILOT_PLUS]: { modelName: modelName, @@ -259,7 +271,12 @@ export default class ChatModelManager { const selectedProviderConfig = providerConfig[customModel.provider as keyof typeof providerConfig] || {}; - // Add token configuration separately to ensure they don't conflict + // Get provider-specific parameters (like topP, frequencyPenalty) that the provider supports + const providerSpecificParams = this.getProviderSpecificParams( + customModel.provider as ChatModelProviders, + customModel + ); + const tokenConfig = isThinkingEnabled ? { maxTokens: customModel.maxTokens ?? settings.maxTokens, @@ -273,6 +290,7 @@ export default class ChatModelManager { const finalConfig = { ...baseConfig, ...selectedProviderConfig, + ...providerSpecificParams, ...tokenConfig, }; @@ -301,6 +319,56 @@ export default class ChatModelManager { return config; } + /** + * Returns provider-specific parameters (like topP, frequencyPenalty) based on what the provider supports + * This prevents passing undefined values to providers that don't support them + */ + private getProviderSpecificParams(provider: ChatModelProviders, customModel: CustomModel) { + const params: Record = {}; + + // Add topP only if defined + if (customModel.topP !== undefined) { + // These providers support topP + if ( + [ + ChatModelProviders.OPENAI, + ChatModelProviders.AZURE_OPENAI, + ChatModelProviders.ANTHROPIC, + ChatModelProviders.GOOGLE, + ChatModelProviders.OPENROUTERAI, + ChatModelProviders.OLLAMA, + ChatModelProviders.LM_STUDIO, + ChatModelProviders.OPENAI_FORMAT, + ChatModelProviders.MISTRAL, + ChatModelProviders.DEEPSEEK, + ].includes(provider) + ) { + params.topP = customModel.topP; + } + } + + // Add frequencyPenalty only if defined + if (customModel.frequencyPenalty !== undefined) { + // These providers support frequencyPenalty + if ( + [ + ChatModelProviders.OPENAI, + ChatModelProviders.AZURE_OPENAI, + ChatModelProviders.OPENROUTERAI, + ChatModelProviders.OLLAMA, + ChatModelProviders.LM_STUDIO, + ChatModelProviders.OPENAI_FORMAT, + ChatModelProviders.MISTRAL, + ChatModelProviders.DEEPSEEK, + ].includes(provider) + ) { + params.frequencyPenalty = customModel.frequencyPenalty; + } + } + + return params; + } + // Build a map of modelKey to model config public buildModelMap() { const activeModels = getSettings().activeModels; diff --git a/src/aiParams.ts b/src/aiParams.ts index 2e243ad4..43253cd1 100644 --- a/src/aiParams.ts +++ b/src/aiParams.ts @@ -105,8 +105,9 @@ export interface CustomModel { stream?: boolean; temperature?: number; maxTokens?: number; + topP?: number; + frequencyPenalty?: number; - context?: number; projectEnabled?: boolean; plusExclusive?: boolean; believerExclusive?: boolean; diff --git a/src/components/modals/project/AddProjectModal.tsx b/src/components/modals/project/AddProjectModal.tsx index 8cecc032..128a2257 100644 --- a/src/components/modals/project/AddProjectModal.tsx +++ b/src/components/modals/project/AddProjectModal.tsx @@ -16,6 +16,7 @@ import { HelpCircle } from "lucide-react"; import { App, Modal, Notice } from "obsidian"; import React, { useState } from "react"; import { createRoot, Root } from "react-dom/client"; +import { DEFAULT_MODEL_SETTING } from "@/constants"; interface AddProjectModalContentProps { initialProject?: ProjectConfig; @@ -41,8 +42,8 @@ function AddProjectModalContent({ initialProject, onSave, onCancel }: AddProject systemPrompt: "", projectModelKey: "", modelConfigs: { - temperature: 1.0, - maxTokens: 1000, + temperature: DEFAULT_MODEL_SETTING.TEMPERATURE, + maxTokens: DEFAULT_MODEL_SETTING.MAX_TOKENS, }, contextSource: { inclusions: "", @@ -231,7 +232,7 @@ function AddProjectModalContent({ initialProject, onSave, onCancel }: AddProject
handleInputChange("modelConfigs.temperature", value)} min={0} max={2} @@ -241,7 +242,7 @@ function AddProjectModalContent({ initialProject, onSave, onCancel }: AddProject handleInputChange("modelConfigs.maxTokens", value)} min={1} max={65000} diff --git a/src/components/ui/scroll-area.tsx b/src/components/ui/scroll-area.tsx index 253a9f97..430f8774 100644 --- a/src/components/ui/scroll-area.tsx +++ b/src/components/ui/scroll-area.tsx @@ -32,9 +32,9 @@ const ScrollBar = React.forwardRef< className={cn( "tw-flex tw-touch-none tw-select-none tw-transition-colors", orientation === "vertical" && - "tw-h-full tw-w-2.5 tw-border-l tw-border-l-transparent tw-p-[1px]", + "tw-h-full tw-w-2.5 tw-border-l tw-border-l-transparent tw-p-px", orientation === "horizontal" && - "tw-h-2.5 tw-flex-col tw-border-t tw-border-t-transparent tw-p-[1px]", + "tw-h-2.5 tw-flex-col tw-border-t tw-border-t-transparent tw-p-px", className )} {...props} diff --git a/src/components/ui/separator.tsx b/src/components/ui/separator.tsx index 14a4a35b..1ba03158 100644 --- a/src/components/ui/separator.tsx +++ b/src/components/ui/separator.tsx @@ -16,7 +16,7 @@ const Separator = React.forwardRef< orientation={orientation} className={cn( "tw-shrink-0 tw-bg-[var(--background-modifier-border)]", - orientation === "horizontal" ? "tw-h-[1px] tw-w-full" : "tw-h-full tw-w-[1px]", + orientation === "horizontal" ? "tw-h-px tw-w-full" : "tw-h-full tw-w-px", className )} {...props} diff --git a/src/constants.ts b/src/constants.ts index cd4b835f..16b78cc5 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -108,6 +108,11 @@ export const PLUS_UTM_MEDIUMS = { }; export type PlusUtmMedium = (typeof PLUS_UTM_MEDIUMS)[keyof typeof PLUS_UTM_MEDIUMS]; +export const DEFAULT_MODEL_SETTING = { + MAX_TOKENS: 6000, + TEMPERATURE: 0.1, +}; + export enum ChatModels { COPILOT_PLUS_FLASH = "copilot-plus-flash", GPT_41 = "gpt-4.1", @@ -653,8 +658,8 @@ export const DEFAULT_SETTINGS: CopilotSettings = { defaultChainType: ChainType.LLM_CHAIN, defaultModelKey: ChatModels.GPT_41 + "|" + ChatModelProviders.OPENAI, embeddingModelKey: EmbeddingModels.OPENAI_EMBEDDING_SMALL + "|" + EmbeddingModelProviders.OPENAI, - temperature: 0.1, - maxTokens: 6000, + temperature: DEFAULT_MODEL_SETTING.TEMPERATURE, + maxTokens: DEFAULT_MODEL_SETTING.MAX_TOKENS, contextTurns: 15, userSystemPrompt: "", openAIProxyBaseUrl: "", diff --git a/src/settings/v2/components/ModelEditDialog.tsx b/src/settings/v2/components/ModelEditDialog.tsx index c9307079..8f3177cd 100644 --- a/src/settings/v2/components/ModelEditDialog.tsx +++ b/src/settings/v2/components/ModelEditDialog.tsx @@ -1,18 +1,12 @@ import { CustomModel } from "@/aiParams"; import { Checkbox } from "@/components/ui/checkbox"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; import { FormField } from "@/components/ui/form-field"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { PasswordInput } from "@/components/ui/password-input"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { + DEFAULT_MODEL_SETTING, MODEL_CAPABILITIES, ModelCapability, Provider, @@ -20,28 +14,33 @@ import { ProviderSettingsKeyMap, SettingKeyProviders, } from "@/constants"; -import { useTab } from "@/contexts/TabContext"; import { getSettings } from "@/settings/model"; import { debounce, getProviderInfo, getProviderLabel } from "@/utils"; import { HelpCircle } from "lucide-react"; import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { SettingSlider } from "@/components/ui/setting-slider"; +import { App, Modal } from "obsidian"; +import { createRoot, Root } from "react-dom/client"; -interface ModelEditDialogProps { - open: boolean; - onOpenChange: (open: boolean) => void; - model: CustomModel | null; - onUpdate: (originalModel: CustomModel, updatedModel: CustomModel) => void; +interface ModelEditModalContentProps { + model: CustomModel; + isEmbeddingModel: boolean; + onUpdate: ( + isEmbeddingModel: boolean, + originalModel: CustomModel, + updatedModel: CustomModel + ) => void; + onCancel: () => void; } -export const ModelEditDialog: React.FC = ({ - open, - onOpenChange, +export const ModelEditModalContent: React.FC = ({ model, onUpdate, + onCancel, + isEmbeddingModel, }) => { - const { modalContainer } = useTab(); - const [localModel, setLocalModel] = useState(model); - const [originalModel, setOriginalModel] = useState(model); + const [localModel, setLocalModel] = useState(model); + const [originalModel, setOriginalModel] = useState(model); const [providerInfo, setProviderInfo] = useState({} as ProviderMetadata); const settings = getSettings(); @@ -52,7 +51,7 @@ export const ModelEditDialog: React.FC = ({ useEffect(() => { setLocalModel(model); setOriginalModel(model); - if (model?.provider) { + if (model.provider) { setProviderInfo(getProviderInfo(model.provider)); } }, [model]); @@ -60,19 +59,16 @@ export const ModelEditDialog: React.FC = ({ // Debounce the onUpdate callback const debouncedOnUpdate = useMemo( () => - debounce((currentOriginalModel: CustomModel | null, updatedModel: CustomModel) => { - if (currentOriginalModel) { - onUpdate(currentOriginalModel, updatedModel); - } + debounce((currentOriginalModel: CustomModel, updatedModel: CustomModel) => { + onUpdate(isEmbeddingModel, currentOriginalModel, updatedModel); }, 500), - [onUpdate] + [isEmbeddingModel, onUpdate] ); // Function to update local state immediately const handleLocalUpdate = useCallback( (field: keyof CustomModel, value: any) => { setLocalModel((prevModel) => { - if (!prevModel) return null; const updatedModel = { ...prevModel, [field]: value, @@ -109,147 +105,162 @@ export const ModelEditDialog: React.FC = ({ })) as Array<{ id: ModelCapability; label: string; description: string }>; const displayApiKey = localModel.apiKey || getDefaultApiKey(localModel.provider as Provider); + const showOtherParameters = + !isEmbeddingModel && + localModel.provider !== "copilot-plus" && + localModel.provider !== "copilot-plus-jina"; return ( - - - - Model Settings - {localModel.name} - Customize model parameters. - +
+
+

Model Settings - {localModel.name}

+

Customize model parameters

+
-
- - handleLocalUpdate("name", e.target.value)} - placeholder="Enter model name" - /> - +
+ + handleLocalUpdate("name", e.target.value)} + placeholder="Enter model name" + /> + - - Display Name - - - - - - -
-
Suggested format:
-
[Source]-[Payment]:[Pretty Model Name]
-
- Example: -
  • Direct-Paid:Ds-r1
  • -
  • OpenRouter-Paid:Ds-r1
  • -
  • Perplexity-Paid:lg
  • -
    + + Display Name + + + + + + +
    +
    Suggested format:
    +
    [Source]-[Payment]:[Pretty Model Name]
    +
    + Example: +
  • Direct-Paid:Ds-r1
  • +
  • OpenRouter-Paid:Ds-r1
  • +
  • Perplexity-Paid:lg
  • - - - -
    - } - > - handleLocalUpdate("displayName", e.target.value)} - /> -
    - - - - - - - handleLocalUpdate("baseUrl", e.target.value)} - /> - - - - handleLocalUpdate("apiKey", value)} - /> - {providerInfo.keyManagementURL && ( -

    - - Get {providerInfo.label} API Key - -

    - )} -
    - - - Model Capabilities - - - - - - -
    - Only used to display model capabilities, does not affect model functionality -
    -
    -
    -
    -
    - } - > -
    - {capabilityOptions.map(({ id, label, description }) => ( -
    - { - const newCapabilities = localModel.capabilities || []; - const value = checked - ? [...newCapabilities, id] - : newCapabilities.filter((cap) => cap !== id); - handleLocalUpdate("capabilities", value); - }} - /> - -
    - ))} +
    +
    +
    +
    - + } + > + handleLocalUpdate("displayName", e.target.value)} + /> + - {/* + + + + + handleLocalUpdate("baseUrl", e.target.value)} + /> + + + + handleLocalUpdate("apiKey", value)} + /> + {providerInfo.keyManagementURL && ( +

    + + Get {providerInfo.label} API Key + +

    + )} +
    + + {showOtherParameters && ( + <> + - Temperature +
    + Model Capabilities - + + + +
    + Only used to display model capabilities, does not affect model + functionality +
    +
    +
    +
    +
    + } + > +
    + {capabilityOptions.map(({ id, label, description }) => ( +
    + { + const newCapabilities = localModel.capabilities || []; + const value = checked + ? [...newCapabilities, id] + : newCapabilities.filter((cap) => cap !== id); + handleLocalUpdate("capabilities", value); + }} + /> + +
    + ))} +
    +
    + + + Token limit + + + + - Controls randomness: 0 is focused and deterministic, 2 is more creative +
    +

    + The maximum number of output tokens to generate. Default is{" "} + {DEFAULT_MODEL_SETTING.MAX_TOKENS}. +

    + + This number plus the length of your prompt (input tokens) must be + smaller than the context window of the model. + +
    @@ -257,25 +268,30 @@ export const ModelEditDialog: React.FC = ({ } > handleLocalUpdate("temperature", value)} - max={2} + value={ + localModel.maxTokens ?? settings.maxTokens ?? DEFAULT_MODEL_SETTING.MAX_TOKENS + } + onChange={(value) => handleLocalUpdate("maxTokens", value)} min={0} - step={0.1} + max={65000} + step={100} />
    - Context + Temperature - + - Maximum number of tokens to use for context +
    + Default is {DEFAULT_MODEL_SETTING.TEMPERATURE}. Higher values will result + in more creativeness, but also more mistakes. Set to 0 for no randomness. +
    @@ -283,35 +299,132 @@ export const ModelEditDialog: React.FC = ({ } > handleLocalUpdate("context", value)} - max={65000} + value={ + localModel.temperature ?? + settings.temperature ?? + DEFAULT_MODEL_SETTING.TEMPERATURE + } + onChange={(value) => handleLocalUpdate("temperature", value)} + max={2} min={0} - step={100} + step={0.05} />
    -
    -
    - Stream output - - - - - - - Enable streaming responses from the model - - - -
    - handleLocalUpdate("stream", checked)} - /> -
    */} -
    - -
    + + Top-P + + + + + + +
    + Default value is 0.9, the smaller the value, the less variety in the + answers, the easier to understand, the larger the value, the larger the + range of the Al's vocabulary, the more diverse +
    +
    +
    +
    +
    + } + > + handleLocalUpdate("topP", value)} + max={1} + min={0} + step={0.05} + /> + + + + Frequency Penalty + + + + + + +
    +

    + The frequency penalty parameter tells the model not to repeat a word + that has already been used multiple times in the conversation. +

    + + The higher the value, the more the model is penalized for repeating + words. + +
    +
    +
    +
    + + } + > + handleLocalUpdate("frequencyPenalty", value)} + max={2} + min={0} + step={0.05} + /> +
    + + )} + + ); }; + +export class ModelEditModal extends Modal { + private root: Root; + + constructor( + app: App, + private model: CustomModel, + private isEmbeddingModel: boolean, + private onUpdate: ( + isEmbeddingModel: boolean, + originalModel: CustomModel, + updatedModel: CustomModel + ) => void + ) { + super(app); + } + + onOpen() { + const { contentEl } = this; + this.root = createRoot(contentEl); + + const handleUpdate = ( + isEmbeddingModel: boolean, + originalModel: CustomModel, + updatedModel: CustomModel + ) => { + this.onUpdate(isEmbeddingModel, originalModel, updatedModel); + }; + + const handleCancel = () => { + this.close(); + }; + + this.root.render( + + ); + } + + onClose() { + this.root.unmount(); + } +} diff --git a/src/settings/v2/components/ModelSettings.tsx b/src/settings/v2/components/ModelSettings.tsx index 4dc44d1b..29be2e46 100644 --- a/src/settings/v2/components/ModelSettings.tsx +++ b/src/settings/v2/components/ModelSettings.tsx @@ -4,19 +4,48 @@ import { BUILTIN_CHAT_MODELS, BUILTIN_EMBEDDING_MODELS } from "@/constants"; import EmbeddingManager from "@/LLMProviders/embeddingManager"; import ProjectManager from "@/LLMProviders/projectManager"; import { logError } from "@/logger"; -import { setSettings, updateSetting, useSettingsValue } from "@/settings/model"; +import { CopilotSettings, setSettings, updateSetting, useSettingsValue } from "@/settings/model"; import { ModelAddDialog } from "@/settings/v2/components/ModelAddDialog"; -import { ModelEditDialog } from "@/settings/v2/components/ModelEditDialog"; +import { ModelEditModal } from "@/settings/v2/components/ModelEditDialog"; import { ModelTable } from "@/settings/v2/components/ModelTable"; +import { omit } from "@/utils"; import { Notice } from "obsidian"; import React, { useState } from "react"; export const ModelSettings: React.FC = () => { const settings = useSettingsValue(); - const [editingModel, setEditingModel] = useState(null); const [showAddDialog, setShowAddDialog] = useState(false); const [showAddEmbeddingDialog, setShowAddEmbeddingDialog] = useState(false); + const onCopyModel = (model: CustomModel, isEmbeddingModel: boolean = false) => { + const newModel: CustomModel = { + ...omit(model, [ + "isBuiltIn", + "core", + "projectEnabled", + "plusExclusive", + "believerExclusive", + "capabilities", + "displayName", + "dimensions", + ]), + name: `${model.name} (copy)`, + }; + + const settingField: keyof CopilotSettings = isEmbeddingModel + ? "activeEmbeddingModels" + : "activeModels"; + + updateSetting(settingField, [...settings[settingField], newModel]); + }; + + const handleModelReorder = (newModels: CustomModel[], isEmbeddingModel: boolean = false) => { + const settingField: keyof CopilotSettings = isEmbeddingModel + ? "activeEmbeddingModels" + : "activeModels"; + updateSetting(settingField, newModels); + }; + const onDeleteModel = (modelKey: string) => { const [modelName, provider] = modelKey.split("|"); const updatedActiveModels = settings.activeModels.filter( @@ -37,14 +66,22 @@ export const ModelSettings: React.FC = () => { }); }; - const handleModelUpdate = (originalModel: CustomModel, updatedModel: CustomModel) => { - const modelIndex = settings.activeModels.findIndex( + const handleModelUpdate = ( + isEmbeddingModel: boolean, + originalModel: CustomModel, + updatedModel: CustomModel + ) => { + const settingField: keyof CopilotSettings = isEmbeddingModel + ? "activeEmbeddingModels" + : "activeModels"; + + const modelIndex = settings[settingField].findIndex( (m) => m.name === originalModel.name && m.provider === originalModel.provider ); if (modelIndex !== -1) { - const updatedModels = [...settings.activeModels]; + const updatedModels = [...settings[settingField]]; updatedModels[modelIndex] = updatedModel; - updateSetting("activeModels", updatedModels); + updateSetting(settingField, updatedModels); } else { new Notice("Could not find model to update"); logError("Could not find model to update:", originalModel); @@ -59,10 +96,6 @@ export 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( @@ -78,10 +111,6 @@ export const ModelSettings: React.FC = () => { updateSetting("activeEmbeddingModels", updatedModels); }; - const handleEmbeddingModelReorder = (newModels: CustomModel[]) => { - updateSetting("activeEmbeddingModels", newModels); - }; - const handleRefreshChatModels = () => { // Get all custom models (non-built-in models) const customModels = settings.activeModels.filter((model) => !model.isBuiltIn); @@ -106,29 +135,27 @@ export const ModelSettings: React.FC = () => { new Notice("Embedding models refreshed successfully"); }; + const handleEditModel = (model: CustomModel, isEmbeddingModel: boolean = false) => { + const modal = new ModelEditModal(app, model, isEmbeddingModel, handleModelUpdate); + modal.open(); + }; + return (
    Chat Models
    handleEditModel(model)} + onCopy={(model) => onCopyModel(model)} onDelete={onDeleteModel} onAdd={() => setShowAddDialog(true)} onUpdateModel={handleTableUpdate} - onReorderModels={handleModelReorder} + onReorderModels={(newModels) => handleModelReorder(newModels)} onRefresh={handleRefreshChatModels} title="Chat Model" /> - {/* model edit dialog*/} - !open && setEditingModel(null)} - model={editingModel} - onUpdate={handleModelUpdate} - /> - {/* model add dialog */} { />
    - updateSetting("temperature", value)} - min={0} - max={2} - step={0.05} - /> - - -

    - The maximum number of output tokens to generate. Default is 6000. -

    - - This number plus the length of your prompt (input tokens) must be smaller than the - context window of the model. - - - } - value={settings.maxTokens} - onChange={(value) => updateSetting("maxTokens", value)} - min={0} - max={65000} - step={100} - /> - {
    Embedding Models
    handleEditModel(model, true)} onDelete={onDeleteEmbeddingModel} + onCopy={(model) => onCopyModel(model, true)} onAdd={() => setShowAddEmbeddingDialog(true)} onUpdateModel={handleEmbeddingModelUpdate} - onReorderModels={handleEmbeddingModelReorder} + onReorderModels={(newModels) => handleModelReorder(newModels, true)} onRefresh={handleRefreshEmbeddingModels} title="Embedding Model" /> diff --git a/src/settings/v2/components/ModelTable.tsx b/src/settings/v2/components/ModelTable.tsx index 0bba8190..d270137b 100644 --- a/src/settings/v2/components/ModelTable.tsx +++ b/src/settings/v2/components/ModelTable.tsx @@ -36,17 +36,27 @@ import { CSS } from "@dnd-kit/utilities"; import { ChevronDown, ChevronRight, + Copy, Eye, Globe, GripVertical, Lightbulb, LucideProps, + MoreVertical, Pencil, + PencilLine, Plus, RefreshCw, Trash2, } from "lucide-react"; import React, { ForwardRefExoticComponent, RefAttributes } from "react"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { useContainerContext } from "@/settings/v2/components/ContainerContext"; const CAPABILITY_ICONS: Record< ModelCapability, @@ -110,27 +120,25 @@ const renderCapabilities = (model: CustomModel) => { ); }; -interface ModelTableProps { - models: CustomModel[]; - onEdit?: (model: CustomModel) => void; - onDelete: (modelKey: string) => void; - onAdd: () => void; - onUpdateModel: (model: CustomModel) => void; - onReorderModels?: (newModels: CustomModel[]) => void; - onRefresh?: () => void; - title: string; -} - interface ModelCardProps { model: CustomModel; - onEdit?: () => void; - onDelete?: () => void; + onEdit?: (model: CustomModel) => void; + onCopy?: (model: CustomModel) => void; + onDelete: (modelKey: string) => void; onUpdateModel: (model: CustomModel) => void; id: string; } -const ModelCard: React.FC = ({ model, onEdit, onDelete, onUpdateModel, id }) => { +const ModelCard: React.FC = ({ + model, + onEdit, + onCopy, + onDelete, + onUpdateModel, + id, +}) => { const [isExpanded, setIsExpanded] = React.useState(false); + const container = useContainerContext(); const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id, @@ -205,24 +213,57 @@ const ModelCard: React.FC = ({ model, onEdit, onDelete, onUpdate size="icon" onClick={(e) => { e.stopPropagation(); - onEdit(); + onEdit(model); }} > )} - {onDelete && !model.core && ( - - )} + + + + + + {onEdit && ( + { + e.stopPropagation(); + onEdit(model); + }} + > + + Edit + + )} + + {onCopy && !model.core && ( + { + e.stopPropagation(); + onCopy(model); + }} + > + + Copy + + )} + + {!model.core && ( + { + e.stopPropagation(); + onDelete(getModelKeyFromModel(model)); + }} + className="tw-text-error" + > + + Delete + + )} + +
    @@ -261,23 +302,27 @@ const ModelCard: React.FC = ({ model, onEdit, onDelete, onUpdate ); }; -const SortableTableRow: React.FC<{ +const DesktopSortableTableRow: React.FC<{ model: CustomModel; onEdit?: (model: CustomModel) => void; + onCopy?: (model: CustomModel) => void; onDelete: (modelKey: string) => void; onUpdateModel: (model: CustomModel) => void; isEmbeddingModel: boolean; -}> = ({ model, onEdit, onDelete, onUpdateModel, isEmbeddingModel }) => { +}> = ({ model, onEdit, onCopy, onDelete, onUpdateModel, isEmbeddingModel }) => { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: getModelKeyFromModel(model), disabled: model.core, }); + const container = useContainerContext(); const style = { transform: CSS.Transform.toString(transform), transition, }; + const showDropdownMenu = onEdit || !model.core || (onCopy && !model.core); + return ( )} - {!model.core && ( - + + {showDropdownMenu && ( + + + + + + {onEdit && ( + onEdit(model)}> + + Edit + + )} + + {onCopy && !model.core && ( + onCopy(model)}> + + Copy + + )} + + {!model.core && ( + onDelete(getModelKeyFromModel(model))} + className="tw-text-error" + > + + Delete + + )} + + )} @@ -350,9 +420,22 @@ const SortableTableRow: React.FC<{ ); }; +interface ModelTableProps { + models: CustomModel[]; + onEdit?: (model: CustomModel) => void; + onCopy?: (model: CustomModel) => void; + onDelete: (modelKey: string) => void; + onAdd: () => void; + onUpdateModel: (model: CustomModel) => void; + onReorderModels?: (newModels: CustomModel[]) => void; + onRefresh?: () => void; + title: string; +} + export const ModelTable: React.FC = ({ models, onEdit, + onCopy, onDelete, onAdd, onUpdateModel, @@ -457,8 +540,9 @@ export const ModelTable: React.FC = ({ key={getModelKeyFromModel(model)} id={getModelKeyFromModel(model)} model={model} - onEdit={onEdit ? () => onEdit(model) : undefined} - onDelete={() => onDelete(getModelKeyFromModel(model))} + onEdit={onEdit} + onCopy={onCopy} + onDelete={onDelete} onUpdateModel={onUpdateModel} /> ))} @@ -497,10 +581,11 @@ export const ModelTable: React.FC = ({ strategy={verticalListSortingStrategy} > {models.map((model) => ( - onEdit(model) : undefined} + onEdit={onEdit} + onCopy={onCopy} onDelete={onDelete} onUpdateModel={onUpdateModel} isEmbeddingModel={isEmbeddingModel} diff --git a/src/tests/projectUtils.test.ts b/src/tests/projectUtils.test.ts index e1f8aa24..0796cbf5 100644 --- a/src/tests/projectUtils.test.ts +++ b/src/tests/projectUtils.test.ts @@ -12,7 +12,7 @@ describe("projectUtils", () => { projectModelKey: "gpt-3.5-turbo", modelConfigs: { temperature: 0.7, - maxTokens: 1000, + maxTokens: 6000, }, contextSource: { inclusions: "src/**/*.tsx", diff --git a/src/utils.ts b/src/utils.ts index 84f2c03f..fced5072 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -487,12 +487,6 @@ export async function safeFetch(url: string, options: RequestInit = {}): Promise // Remove content-length if it exists delete (headers as Record)["content-length"]; - if (typeof options.body === "string") { - const newBody = JSON.parse(options.body ?? {}); - delete newBody["frequency_penalty"]; - options.body = JSON.stringify(newBody); - } - logInfo("==== safeFetch method request ===="); const method = options.method?.toUpperCase() || "POST";