feat: add model management UI
This commit introduces a new UI for managing models within the OpenAI integration. This UI allows users to view, add, and delete OpenAI models associated with their OpenAI providers. It includes functionality to fetch and refresh model lists, as well as to search for specific models. These changes enhance the user experience by offering greater control over their OpenAI model interactions. This commit also includes improvements to the existing OpenAI configuration UI, enabling users to seamlessly manage multiple OpenAI providers and associated models.
This commit is contained in:
parent
e2e3655c47
commit
2a2610afb8
@ -96,5 +96,9 @@
|
|||||||
"translate": "Translate",
|
"translate": "Translate",
|
||||||
"custom": "Custom"
|
"custom": "Custom"
|
||||||
},
|
},
|
||||||
"citations": "Citations"
|
"citations": "Citations",
|
||||||
|
"segmented": {
|
||||||
|
"ollama": "Ollama Models",
|
||||||
|
"custom": "Custom Models"
|
||||||
|
}
|
||||||
}
|
}
|
@ -26,13 +26,37 @@
|
|||||||
"required": "API Key is required.",
|
"required": "API Key is required.",
|
||||||
"placeholder": "Enter API Key"
|
"placeholder": "Enter API Key"
|
||||||
},
|
},
|
||||||
"submit": "Submit",
|
"submit": "Save",
|
||||||
"update": "Update",
|
"update": "Update",
|
||||||
"deleteConfirm": "Are you sure you want to delete this provider?"
|
"deleteConfirm": "Are you sure you want to delete this provider?",
|
||||||
|
"model": {
|
||||||
|
"title": "Model List",
|
||||||
|
"subheading": "Please select the models you want to use with this provider.",
|
||||||
|
"success": "Successfully added new models."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"addSuccess": "Provider added successfully.",
|
"addSuccess": "Provider added successfully.",
|
||||||
"deleteSuccess": "Provider deleted successfully.",
|
"deleteSuccess": "Provider deleted successfully.",
|
||||||
"updateSuccess": "Provider updated successfully.",
|
"updateSuccess": "Provider updated successfully.",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"edit": "Edit"
|
"edit": "Edit",
|
||||||
|
"refetch": "Refech Model List",
|
||||||
|
"searchModel": "Search Model",
|
||||||
|
"selectAll": "Select All",
|
||||||
|
"save": "Save",
|
||||||
|
"saving": "Saving...",
|
||||||
|
"manageModels": {
|
||||||
|
"columns": {
|
||||||
|
"name": "Model Name",
|
||||||
|
"model_id": "Model ID",
|
||||||
|
"provider": "Provider Name",
|
||||||
|
"actions": "Action"
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"delete": "Delete"
|
||||||
|
},
|
||||||
|
"confirm": {
|
||||||
|
"delete": "Are you sure you want to delete this model?"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
85
src/components/Option/Models/CustomModelsTable.tsx
Normal file
85
src/components/Option/Models/CustomModelsTable.tsx
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { getAllCustomModels, deleteModel } from "@/db/models"
|
||||||
|
import { useStorage } from "@plasmohq/storage/hook"
|
||||||
|
import { useQuery, useQueryClient, useMutation } from "@tanstack/react-query"
|
||||||
|
import { Skeleton, Table, Tooltip } from "antd"
|
||||||
|
import { Trash2 } from "lucide-react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
export const CustomModelsTable = () => {
|
||||||
|
const [selectedModel, setSelectedModel] = useStorage("selectedModel")
|
||||||
|
|
||||||
|
const { t } = useTranslation(["openai", "common"])
|
||||||
|
|
||||||
|
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
const { data, status } = useQuery({
|
||||||
|
queryKey: ["fetchCustomModels"],
|
||||||
|
queryFn: () => getAllCustomModels()
|
||||||
|
})
|
||||||
|
|
||||||
|
const { mutate: deleteCustomModel } = useMutation({
|
||||||
|
mutationFn: deleteModel,
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["fetchCustomModels"]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
{status === "pending" && <Skeleton paragraph={{ rows: 8 }} />}
|
||||||
|
|
||||||
|
{status === "success" && (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<Table
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
title: t("manageModels.columns.name"),
|
||||||
|
dataIndex: "name",
|
||||||
|
key: "name"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("manageModels.columns.model_id"),
|
||||||
|
dataIndex: "model_id",
|
||||||
|
key: "model_id"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("manageModels.columns.provider"),
|
||||||
|
dataIndex: "provider",
|
||||||
|
render: (_, record) => record.provider.name
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("manageModels.columns.actions"),
|
||||||
|
render: (_, record) => (
|
||||||
|
<Tooltip title={t("manageModels.tooltip.delete")}>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
if (
|
||||||
|
window.confirm(t("manageModels.confirm.delete"))
|
||||||
|
) {
|
||||||
|
deleteCustomModel(record.id)
|
||||||
|
if (selectedModel && selectedModel === record.id) {
|
||||||
|
setSelectedModel(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="text-red-500 dark:text-red-400">
|
||||||
|
<Trash2 className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
bordered
|
||||||
|
dataSource={data}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
199
src/components/Option/Models/OllamaModelsTable.tsx
Normal file
199
src/components/Option/Models/OllamaModelsTable.tsx
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||||
|
import { Skeleton, Table, Tag, Tooltip, notification, Modal, Input } from "antd"
|
||||||
|
import { bytePerSecondFormatter } from "~/libs/byte-formater"
|
||||||
|
import { deleteModel, getAllModels } from "~/services/ollama"
|
||||||
|
import dayjs from "dayjs"
|
||||||
|
import relativeTime from "dayjs/plugin/relativeTime"
|
||||||
|
import { useForm } from "@mantine/form"
|
||||||
|
import { RotateCcw, Trash2 } from "lucide-react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { useStorage } from "@plasmohq/storage/hook"
|
||||||
|
|
||||||
|
dayjs.extend(relativeTime)
|
||||||
|
|
||||||
|
export const OllamaModelsTable = () => {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const { t } = useTranslation(["settings", "common"])
|
||||||
|
const [selectedModel, setSelectedModel] = useStorage("selectedModel")
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
initialValues: {
|
||||||
|
model: ""
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const { data, status } = useQuery({
|
||||||
|
queryKey: ["fetchAllModels"],
|
||||||
|
queryFn: () => getAllModels({ returnEmpty: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
const { mutate: deleteOllamaModel } = useMutation({
|
||||||
|
mutationFn: deleteModel,
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["fetchAllModels"]
|
||||||
|
})
|
||||||
|
notification.success({
|
||||||
|
message: t("manageModels.notification.success"),
|
||||||
|
description: t("manageModels.notification.successDeleteDescription")
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
notification.error({
|
||||||
|
message: "Error",
|
||||||
|
description: error?.message || t("manageModels.notification.someError")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const pullModel = async (modelName: string) => {
|
||||||
|
notification.info({
|
||||||
|
message: t("manageModels.notification.pullModel"),
|
||||||
|
description: t("manageModels.notification.pullModelDescription", {
|
||||||
|
modelName
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
form.reset()
|
||||||
|
|
||||||
|
browser.runtime.sendMessage({
|
||||||
|
type: "pull_model",
|
||||||
|
modelName
|
||||||
|
})
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const { mutate: pullOllamaModel } = useMutation({
|
||||||
|
mutationFn: pullModel
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
{status === "pending" && <Skeleton paragraph={{ rows: 8 }} />}
|
||||||
|
|
||||||
|
{status === "success" && (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<Table
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
title: t("manageModels.columns.name"),
|
||||||
|
dataIndex: "name",
|
||||||
|
key: "name"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("manageModels.columns.digest"),
|
||||||
|
dataIndex: "digest",
|
||||||
|
key: "digest",
|
||||||
|
render: (text: string) => (
|
||||||
|
<Tooltip title={text}>
|
||||||
|
<Tag
|
||||||
|
className="cursor-pointer"
|
||||||
|
color="blue">{`${text?.slice(0, 5)}...${text?.slice(-4)}`}</Tag>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("manageModels.columns.modifiedAt"),
|
||||||
|
dataIndex: "modified_at",
|
||||||
|
key: "modified_at",
|
||||||
|
render: (text: string) => dayjs(text).fromNow(true)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("manageModels.columns.size"),
|
||||||
|
dataIndex: "size",
|
||||||
|
key: "size",
|
||||||
|
render: (text: number) => bytePerSecondFormatter(text)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("manageModels.columns.actions"),
|
||||||
|
render: (_, record) => (
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<Tooltip title={t("manageModels.tooltip.delete")}>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
if (
|
||||||
|
window.confirm(t("manageModels.confirm.delete"))
|
||||||
|
) {
|
||||||
|
deleteOllamaModel(record.model)
|
||||||
|
if (
|
||||||
|
selectedModel &&
|
||||||
|
selectedModel === record.model
|
||||||
|
) {
|
||||||
|
setSelectedModel(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="text-red-500 dark:text-red-400">
|
||||||
|
<Trash2 className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title={t("manageModels.tooltip.repull")}>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
if (
|
||||||
|
window.confirm(t("manageModels.confirm.repull"))
|
||||||
|
) {
|
||||||
|
pullOllamaModel(record.model)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="text-gray-700 dark:text-gray-400">
|
||||||
|
<RotateCcw className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
expandable={{
|
||||||
|
expandedRowRender: (record) => (
|
||||||
|
<Table
|
||||||
|
pagination={false}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
title: t("manageModels.expandedColumns.parentModel"),
|
||||||
|
key: "parent_model",
|
||||||
|
dataIndex: "parent_model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("manageModels.expandedColumns.format"),
|
||||||
|
key: "format",
|
||||||
|
dataIndex: "format"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("manageModels.expandedColumns.family"),
|
||||||
|
key: "family",
|
||||||
|
dataIndex: "family"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("manageModels.expandedColumns.parameterSize"),
|
||||||
|
key: "parameter_size",
|
||||||
|
dataIndex: "parameter_size"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t(
|
||||||
|
"manageModels.expandedColumns.quantizationLevel"
|
||||||
|
),
|
||||||
|
key: "quantization_level",
|
||||||
|
dataIndex: "quantization_level"
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
dataSource={[record.details]}
|
||||||
|
locale={{
|
||||||
|
emptyText: t("common:noData")
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
defaultExpandAllRows: false
|
||||||
|
}}
|
||||||
|
bordered
|
||||||
|
dataSource={data}
|
||||||
|
rowKey={(record) => `${record.model}-${record.digest}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -1,22 +1,30 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||||
import { Skeleton, Table, Tag, Tooltip, notification, Modal, Input } from "antd"
|
import {
|
||||||
import { bytePerSecondFormatter } from "~/libs/byte-formater"
|
Skeleton,
|
||||||
import { deleteModel, getAllModels } from "~/services/ollama"
|
Table,
|
||||||
|
Tag,
|
||||||
|
Tooltip,
|
||||||
|
notification,
|
||||||
|
Modal,
|
||||||
|
Input,
|
||||||
|
Segmented
|
||||||
|
} from "antd"
|
||||||
import dayjs from "dayjs"
|
import dayjs from "dayjs"
|
||||||
import relativeTime from "dayjs/plugin/relativeTime"
|
import relativeTime from "dayjs/plugin/relativeTime"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { useForm } from "@mantine/form"
|
import { useForm } from "@mantine/form"
|
||||||
import { Download, RotateCcw, Trash2 } from "lucide-react"
|
import { Download } from "lucide-react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { useStorage } from "@plasmohq/storage/hook"
|
import { OllamaModelsTable } from "./OllamaModelsTable"
|
||||||
|
import { CustomModelsTable } from "./CustomModelsTable"
|
||||||
|
|
||||||
dayjs.extend(relativeTime)
|
dayjs.extend(relativeTime)
|
||||||
|
|
||||||
export const ModelsBody = () => {
|
export const ModelsBody = () => {
|
||||||
const queryClient = useQueryClient()
|
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const { t } = useTranslation(["settings", "common"])
|
const [segmented, setSegmented] = useState<string>("ollama")
|
||||||
const [selectedModel, setSelectedModel] = useStorage("selectedModel")
|
|
||||||
|
const { t } = useTranslation(["settings", "common", "openai"])
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
@ -24,30 +32,6 @@ export const ModelsBody = () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const { data, status } = useQuery({
|
|
||||||
queryKey: ["fetchAllModels"],
|
|
||||||
queryFn: () => getAllModels({ returnEmpty: true })
|
|
||||||
})
|
|
||||||
|
|
||||||
const { mutate: deleteOllamaModel } = useMutation({
|
|
||||||
mutationFn: deleteModel,
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: ["fetchAllModels"]
|
|
||||||
})
|
|
||||||
notification.success({
|
|
||||||
message: t("manageModels.notification.success"),
|
|
||||||
description: t("manageModels.notification.successDeleteDescription")
|
|
||||||
})
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
notification.error({
|
|
||||||
message: "Error",
|
|
||||||
description: error?.message || t("manageModels.notification.someError")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const pullModel = async (modelName: string) => {
|
const pullModel = async (modelName: string) => {
|
||||||
notification.info({
|
notification.info({
|
||||||
message: t("manageModels.notification.pullModel"),
|
message: t("manageModels.notification.pullModel"),
|
||||||
@ -86,130 +70,26 @@ export const ModelsBody = () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="flex items-center justify-end mt-3">
|
||||||
|
<Segmented
|
||||||
{status === "pending" && <Skeleton paragraph={{ rows: 8 }} />}
|
options={[
|
||||||
|
|
||||||
{status === "success" && (
|
|
||||||
<div className="overflow-x-auto">
|
|
||||||
<Table
|
|
||||||
columns={[
|
|
||||||
{
|
{
|
||||||
title: t("manageModels.columns.name"),
|
label: t("common:segmented.ollama"),
|
||||||
dataIndex: "name",
|
value: "ollama"
|
||||||
key: "name"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("manageModels.columns.digest"),
|
label: t("common:segmented.custom"),
|
||||||
dataIndex: "digest",
|
value: "custom"
|
||||||
key: "digest",
|
|
||||||
render: (text: string) => (
|
|
||||||
<Tooltip title={text}>
|
|
||||||
<Tag
|
|
||||||
className="cursor-pointer"
|
|
||||||
color="blue">{`${text?.slice(0, 5)}...${text?.slice(-4)}`}</Tag>
|
|
||||||
</Tooltip>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("manageModels.columns.modifiedAt"),
|
|
||||||
dataIndex: "modified_at",
|
|
||||||
key: "modified_at",
|
|
||||||
render: (text: string) => dayjs(text).fromNow(true)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("manageModels.columns.size"),
|
|
||||||
dataIndex: "size",
|
|
||||||
key: "size",
|
|
||||||
render: (text: number) => bytePerSecondFormatter(text)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("manageModels.columns.actions"),
|
|
||||||
render: (_, record) => (
|
|
||||||
<div className="flex gap-4">
|
|
||||||
<Tooltip title={t("manageModels.tooltip.delete")}>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
if (
|
|
||||||
window.confirm(t("manageModels.confirm.delete"))
|
|
||||||
) {
|
|
||||||
deleteOllamaModel(record.model)
|
|
||||||
if (
|
|
||||||
selectedModel &&
|
|
||||||
selectedModel === record.model
|
|
||||||
) {
|
|
||||||
setSelectedModel(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="text-red-500 dark:text-red-400">
|
|
||||||
<Trash2 className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title={t("manageModels.tooltip.repull")}>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
if (
|
|
||||||
window.confirm(t("manageModels.confirm.repull"))
|
|
||||||
) {
|
|
||||||
pullOllamaModel(record.model)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="text-gray-700 dark:text-gray-400">
|
|
||||||
<RotateCcw className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
expandable={{
|
onChange={(value) => {
|
||||||
expandedRowRender: (record) => (
|
setSegmented(value)
|
||||||
<Table
|
|
||||||
pagination={false}
|
|
||||||
columns={[
|
|
||||||
{
|
|
||||||
title: t("manageModels.expandedColumns.parentModel"),
|
|
||||||
key: "parent_model",
|
|
||||||
dataIndex: "parent_model"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("manageModels.expandedColumns.format"),
|
|
||||||
key: "format",
|
|
||||||
dataIndex: "format"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("manageModels.expandedColumns.family"),
|
|
||||||
key: "family",
|
|
||||||
dataIndex: "family"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("manageModels.expandedColumns.parameterSize"),
|
|
||||||
key: "parameter_size",
|
|
||||||
dataIndex: "parameter_size"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t(
|
|
||||||
"manageModels.expandedColumns.quantizationLevel"
|
|
||||||
),
|
|
||||||
key: "quantization_level",
|
|
||||||
dataIndex: "quantization_level"
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
dataSource={[record.details]}
|
|
||||||
locale={{
|
|
||||||
emptyText: t("common:noData")
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
defaultExpandAllRows: false
|
|
||||||
}}
|
}}
|
||||||
bordered
|
|
||||||
dataSource={data}
|
|
||||||
rowKey={(record) => `${record.model}-${record.digest}`}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
|
|
||||||
|
{segmented === "ollama" ? <OllamaModelsTable /> : <CustomModelsTable />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
|
132
src/components/Option/Settings/openai-fetch-model.tsx
Normal file
132
src/components/Option/Settings/openai-fetch-model.tsx
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
import { getOpenAIConfigById } from "@/db/openai"
|
||||||
|
import { getAllOpenAIModels } from "@/libs/openai"
|
||||||
|
import { useMutation, useQuery } from "@tanstack/react-query"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { Checkbox, Input, Spin, message } from "antd"
|
||||||
|
import { useState, useMemo } from "react"
|
||||||
|
import { createManyModels } from "@/db/models"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
openaiId: string
|
||||||
|
setOpenModelModal: (openModelModal: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OpenAIFetchModel = ({ openaiId, setOpenModelModal }: Props) => {
|
||||||
|
const { t } = useTranslation(["openai"])
|
||||||
|
const [selectedModels, setSelectedModels] = useState<string[]>([])
|
||||||
|
const [searchTerm, setSearchTerm] = useState("")
|
||||||
|
|
||||||
|
const { data, status } = useQuery({
|
||||||
|
queryKey: ["openAIConfigs", openaiId],
|
||||||
|
queryFn: async () => {
|
||||||
|
const config = await getOpenAIConfigById(openaiId)
|
||||||
|
const models = await getAllOpenAIModels(config.baseUrl, config.apiKey)
|
||||||
|
return models
|
||||||
|
},
|
||||||
|
enabled: !!openaiId
|
||||||
|
})
|
||||||
|
|
||||||
|
const filteredModels = useMemo(() => {
|
||||||
|
return (
|
||||||
|
data?.filter((model) =>
|
||||||
|
(model.name ?? model.id)
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(searchTerm.toLowerCase())
|
||||||
|
) || []
|
||||||
|
)
|
||||||
|
}, [data, searchTerm])
|
||||||
|
|
||||||
|
const handleSelectAll = (checked: boolean) => {
|
||||||
|
if (checked) {
|
||||||
|
setSelectedModels(filteredModels.map((model) => model.id))
|
||||||
|
} else {
|
||||||
|
setSelectedModels([])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleModelSelect = (modelId: string, checked: boolean) => {
|
||||||
|
if (checked) {
|
||||||
|
setSelectedModels((prev) => [...prev, modelId])
|
||||||
|
} else {
|
||||||
|
setSelectedModels((prev) => prev.filter((id) => id !== modelId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSave = async (models: string[]) => {
|
||||||
|
const payload = models.map((id) => ({
|
||||||
|
model_id: id,
|
||||||
|
name: filteredModels.find((model) => model.id === id)?.name ?? id,
|
||||||
|
provider_id: openaiId
|
||||||
|
}))
|
||||||
|
|
||||||
|
await createManyModels(payload)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const { mutate: saveModels, isPending: isSaving } = useMutation({
|
||||||
|
mutationFn: onSave,
|
||||||
|
onSuccess: () => {
|
||||||
|
setOpenModelModal(false)
|
||||||
|
message.success(t("modal.model.success"))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
saveModels(selectedModels)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === "pending") {
|
||||||
|
return <Spin />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === "error" || !data || data.length === 0) {
|
||||||
|
return <div>{t("noModelFound")}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{t("modal.model.subheading")}
|
||||||
|
</p>
|
||||||
|
<Input
|
||||||
|
placeholder={t("searchModel")}
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<Checkbox
|
||||||
|
checked={selectedModels.length === filteredModels.length}
|
||||||
|
indeterminate={
|
||||||
|
selectedModels.length > 0 &&
|
||||||
|
selectedModels.length < filteredModels.length
|
||||||
|
}
|
||||||
|
onChange={(e) => handleSelectAll(e.target.checked)}>
|
||||||
|
{t("selectAll")}
|
||||||
|
</Checkbox>
|
||||||
|
<div className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200">
|
||||||
|
{`${selectedModels?.length} / ${data?.length}`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2 custom-scrollbar max-h-[300px] border overflow-y-auto dark:border-gray-600 rounded-md p-3">
|
||||||
|
<div className="grid grid-cols-2 gap-4 items-center">
|
||||||
|
{filteredModels.map((model) => (
|
||||||
|
<Checkbox
|
||||||
|
key={model.id}
|
||||||
|
checked={selectedModels.includes(model.id)}
|
||||||
|
onChange={(e) => handleModelSelect(model.id, e.target.checked)}>
|
||||||
|
{model?.name || model.id}
|
||||||
|
</Checkbox>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={isSaving}
|
||||||
|
className="inline-flex justify-center w-full text-center mt-4 items-center rounded-md border border-transparent bg-black px-2 py-2 text-sm font-medium leading-4 text-white shadow-sm hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:bg-white dark:text-gray-800 dark:hover:bg-gray-100 dark:focus:ring-gray-500 dark:focus:ring-offset-gray-100 disabled:opacity-50">
|
||||||
|
{isSaving ? t("saving") : t("save")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -8,7 +8,8 @@ import {
|
|||||||
updateOpenAIConfig
|
updateOpenAIConfig
|
||||||
} from "@/db/openai"
|
} from "@/db/openai"
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||||
import { Pencil, Trash2, Plus } from "lucide-react"
|
import { Pencil, Trash2, RotateCwIcon } from "lucide-react"
|
||||||
|
import { OpenAIFetchModel } from "./openai-fetch-model"
|
||||||
|
|
||||||
export const OpenAIApp = () => {
|
export const OpenAIApp = () => {
|
||||||
const { t } = useTranslation("openai")
|
const { t } = useTranslation("openai")
|
||||||
@ -16,6 +17,8 @@ export const OpenAIApp = () => {
|
|||||||
const [editingConfig, setEditingConfig] = useState(null)
|
const [editingConfig, setEditingConfig] = useState(null)
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const [form] = Form.useForm()
|
const [form] = Form.useForm()
|
||||||
|
const [openaiId, setOpenaiId] = useState<string | null>(null)
|
||||||
|
const [openModelModal, setOpenModelModal] = useState(false)
|
||||||
|
|
||||||
const { data: configs, isLoading } = useQuery({
|
const { data: configs, isLoading } = useQuery({
|
||||||
queryKey: ["openAIConfigs"],
|
queryKey: ["openAIConfigs"],
|
||||||
@ -24,12 +27,14 @@ export const OpenAIApp = () => {
|
|||||||
|
|
||||||
const addMutation = useMutation({
|
const addMutation = useMutation({
|
||||||
mutationFn: addOpenAICofig,
|
mutationFn: addOpenAICofig,
|
||||||
onSuccess: () => {
|
onSuccess: (data) => {
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ["openAIConfigs"]
|
queryKey: ["openAIConfigs"]
|
||||||
})
|
})
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
message.success(t("addSuccess"))
|
message.success(t("addSuccess"))
|
||||||
|
setOpenaiId(data)
|
||||||
|
setOpenModelModal(true)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -129,6 +134,18 @@ export const OpenAIApp = () => {
|
|||||||
<Pencil className="size-4" />
|
<Pencil className="size-4" />
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip title={t("refetch")}>
|
||||||
|
<button
|
||||||
|
className="text-gray-700 dark:text-gray-400"
|
||||||
|
onClick={() => {
|
||||||
|
setOpenModelModal(true)
|
||||||
|
setOpenaiId(record.id)
|
||||||
|
}}
|
||||||
|
disabled={!record.id}>
|
||||||
|
<RotateCwIcon className="size-4" />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
<Tooltip title={t("delete")}>
|
<Tooltip title={t("delete")}>
|
||||||
<button
|
<button
|
||||||
className="text-red-500 dark:text-red-400"
|
className="text-red-500 dark:text-red-400"
|
||||||
@ -212,6 +229,19 @@ export const OpenAIApp = () => {
|
|||||||
</button>
|
</button>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
open={openModelModal}
|
||||||
|
title={t("modal.model.title")}
|
||||||
|
footer={null}
|
||||||
|
onCancel={() => setOpenModelModal(false)}>
|
||||||
|
{openaiId ? (
|
||||||
|
<OpenAIFetchModel
|
||||||
|
openaiId={openaiId}
|
||||||
|
setOpenModelModal={setOpenModelModal}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
176
src/db/models.ts
Normal file
176
src/db/models.ts
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
import { getOpenAIConfigById as providerInfo } from "./openai"
|
||||||
|
|
||||||
|
type Model = {
|
||||||
|
id: string
|
||||||
|
model_id: string
|
||||||
|
name: string
|
||||||
|
provider_id: string
|
||||||
|
lookup: string
|
||||||
|
db_type: string
|
||||||
|
}
|
||||||
|
export const generateID = () => {
|
||||||
|
return "model-xxxx-xxxx-xxx-xxxx".replace(/[x]/g, () => {
|
||||||
|
const r = Math.floor(Math.random() * 16)
|
||||||
|
return r.toString(16)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const removeModelPrefix = (id: string) => {
|
||||||
|
return id.replace(/^model-/, "")
|
||||||
|
}
|
||||||
|
export class ModelDb {
|
||||||
|
db: chrome.storage.StorageArea
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.db = chrome.storage.local
|
||||||
|
}
|
||||||
|
|
||||||
|
getAll = async (): Promise<Model[]> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.db.get(null, (result) => {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
reject(chrome.runtime.lastError)
|
||||||
|
} else {
|
||||||
|
const data = Object.keys(result).map((key) => result[key])
|
||||||
|
resolve(data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
create = async (model: Model): Promise<void> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.db.set({ [model.id]: model }, () => {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
reject(chrome.runtime.lastError)
|
||||||
|
} else {
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
getById = async (id: string): Promise<Model> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.db.get(id, (result) => {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
reject(chrome.runtime.lastError)
|
||||||
|
} else {
|
||||||
|
resolve(result[id])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
update = async (model: Model): Promise<void> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.db.set({ [model.id]: model }, () => {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
reject(chrome.runtime.lastError)
|
||||||
|
} else {
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
delete = async (id: string): Promise<void> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.db.remove(id, () => {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
reject(chrome.runtime.lastError)
|
||||||
|
} else {
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteAll = async (): Promise<void> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.db.clear(() => {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
reject(chrome.runtime.lastError)
|
||||||
|
} else {
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createManyModels = async (
|
||||||
|
data: { model_id: string; name: string; provider_id: string }[]
|
||||||
|
) => {
|
||||||
|
const db = new ModelDb()
|
||||||
|
|
||||||
|
const models = data.map((item) => {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
lookup: `${item.model_id}_${item.provider_id}`,
|
||||||
|
id: `${item.model_id}_${generateID()}`,
|
||||||
|
db_type: "openai_model"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const model of models) {
|
||||||
|
const isExist = await isLookupExist(model.lookup)
|
||||||
|
|
||||||
|
if (isExist) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.create(model)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createModel = async (
|
||||||
|
model_id: string,
|
||||||
|
name: string,
|
||||||
|
provider_id: string
|
||||||
|
) => {
|
||||||
|
const db = new ModelDb()
|
||||||
|
const id = generateID()
|
||||||
|
const model: Model = {
|
||||||
|
id: `${model_id}_${id}`,
|
||||||
|
model_id,
|
||||||
|
name,
|
||||||
|
provider_id,
|
||||||
|
lookup: `${model_id}_${provider_id}`,
|
||||||
|
db_type: "openai_model"
|
||||||
|
}
|
||||||
|
await db.create(model)
|
||||||
|
return model
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getModelInfo = async (id: string) => {
|
||||||
|
const db = new ModelDb()
|
||||||
|
const model = await db.getById(id)
|
||||||
|
return model
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getAllCustomModels = async () => {
|
||||||
|
const db = new ModelDb()
|
||||||
|
const models = (await db.getAll()).filter(
|
||||||
|
(model) => model.db_type === "openai_model"
|
||||||
|
)
|
||||||
|
const modelsWithProvider = await Promise.all(
|
||||||
|
models.map(async (model) => {
|
||||||
|
const provider = await providerInfo(model.provider_id)
|
||||||
|
return { ...model, provider }
|
||||||
|
})
|
||||||
|
)
|
||||||
|
return modelsWithProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteModel = async (id: string) => {
|
||||||
|
const db = new ModelDb()
|
||||||
|
await db.delete(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isLookupExist = async (lookup: string) => {
|
||||||
|
const db = new ModelDb()
|
||||||
|
const models = await db.getAll()
|
||||||
|
const model = models.find((model) => model.lookup === lookup)
|
||||||
|
return model ? true : false
|
||||||
|
}
|
@ -1,9 +1,12 @@
|
|||||||
|
import { cleanUrl } from "@/libs/clean-url"
|
||||||
|
|
||||||
type OpenAIModelConfig = {
|
type OpenAIModelConfig = {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
baseUrl: string
|
baseUrl: string
|
||||||
apiKey?: string
|
apiKey?: string
|
||||||
createdAt: number
|
createdAt: number
|
||||||
|
db_type: string
|
||||||
}
|
}
|
||||||
export const generateID = () => {
|
export const generateID = () => {
|
||||||
return "openai-xxxx-xxx-xxxx".replace(/[x]/g, () => {
|
return "openai-xxxx-xxx-xxxx".replace(/[x]/g, () => {
|
||||||
@ -95,9 +98,10 @@ export const addOpenAICofig = async ({ name, baseUrl, apiKey }: { name: string,
|
|||||||
const config: OpenAIModelConfig = {
|
const config: OpenAIModelConfig = {
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
baseUrl,
|
baseUrl: cleanUrl(baseUrl),
|
||||||
apiKey,
|
apiKey,
|
||||||
createdAt: Date.now()
|
createdAt: Date.now(),
|
||||||
|
db_type: "openai"
|
||||||
}
|
}
|
||||||
await openaiDb.create(config)
|
await openaiDb.create(config)
|
||||||
return id
|
return id
|
||||||
@ -107,7 +111,7 @@ export const addOpenAICofig = async ({ name, baseUrl, apiKey }: { name: string,
|
|||||||
export const getAllOpenAIConfig = async () => {
|
export const getAllOpenAIConfig = async () => {
|
||||||
const openaiDb = new OpenAIModelDb()
|
const openaiDb = new OpenAIModelDb()
|
||||||
const configs = await openaiDb.getAll()
|
const configs = await openaiDb.getAll()
|
||||||
return configs
|
return configs.filter(config => config.db_type === "openai")
|
||||||
}
|
}
|
||||||
|
|
||||||
export const updateOpenAIConfig = async ({ id, name, baseUrl, apiKey }: { id: string, name: string, baseUrl: string, apiKey: string }) => {
|
export const updateOpenAIConfig = async ({ id, name, baseUrl, apiKey }: { id: string, name: string, baseUrl: string, apiKey: string }) => {
|
||||||
@ -115,9 +119,10 @@ export const updateOpenAIConfig = async ({ id, name, baseUrl, apiKey }: { id: st
|
|||||||
const config: OpenAIModelConfig = {
|
const config: OpenAIModelConfig = {
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
baseUrl,
|
baseUrl: cleanUrl(baseUrl),
|
||||||
apiKey,
|
apiKey,
|
||||||
createdAt: Date.now()
|
createdAt: Date.now(),
|
||||||
|
db_type: "openai"
|
||||||
}
|
}
|
||||||
|
|
||||||
await openaiDb.update(config)
|
await openaiDb.update(config)
|
||||||
@ -137,10 +142,18 @@ export const updateOpenAIConfigApiKey = async (id: string, { name, baseUrl, apiK
|
|||||||
const config: OpenAIModelConfig = {
|
const config: OpenAIModelConfig = {
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
baseUrl,
|
baseUrl: cleanUrl(baseUrl),
|
||||||
apiKey,
|
apiKey,
|
||||||
createdAt: Date.now()
|
createdAt: Date.now(),
|
||||||
|
db_type: "openai"
|
||||||
}
|
}
|
||||||
|
|
||||||
await openaiDb.update(config)
|
await openaiDb.update(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const getOpenAIConfigById = async (id: string) => {
|
||||||
|
const openaiDb = new OpenAIModelDb()
|
||||||
|
const config = await openaiDb.getById(id)
|
||||||
|
return config
|
||||||
|
}
|
25
src/libs/openai.ts
Normal file
25
src/libs/openai.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
type Model = {
|
||||||
|
id: string
|
||||||
|
name?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getAllOpenAIModels = async (baseUrl: string, apiKey?: string) => {
|
||||||
|
const url = `${baseUrl}/models`
|
||||||
|
const headers = apiKey
|
||||||
|
? {
|
||||||
|
Authorization: `Bearer ${apiKey}`
|
||||||
|
}
|
||||||
|
: {}
|
||||||
|
|
||||||
|
const res = await fetch(url, {
|
||||||
|
headers
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = (await res.json()) as { data: Model[] }
|
||||||
|
|
||||||
|
return data.data
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user