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:
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 { Skeleton, Table, Tag, Tooltip, notification, Modal, Input } from "antd"
|
||||
import { bytePerSecondFormatter } from "~/libs/byte-formater"
|
||||
import { deleteModel, getAllModels } from "~/services/ollama"
|
||||
import {
|
||||
Skeleton,
|
||||
Table,
|
||||
Tag,
|
||||
Tooltip,
|
||||
notification,
|
||||
Modal,
|
||||
Input,
|
||||
Segmented
|
||||
} from "antd"
|
||||
import dayjs from "dayjs"
|
||||
import relativeTime from "dayjs/plugin/relativeTime"
|
||||
import { useState } from "react"
|
||||
import { useForm } from "@mantine/form"
|
||||
import { Download, RotateCcw, Trash2 } from "lucide-react"
|
||||
import { Download } from "lucide-react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useStorage } from "@plasmohq/storage/hook"
|
||||
import { OllamaModelsTable } from "./OllamaModelsTable"
|
||||
import { CustomModelsTable } from "./CustomModelsTable"
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
export const ModelsBody = () => {
|
||||
const queryClient = useQueryClient()
|
||||
const [open, setOpen] = useState(false)
|
||||
const { t } = useTranslation(["settings", "common"])
|
||||
const [selectedModel, setSelectedModel] = useStorage("selectedModel")
|
||||
const [segmented, setSegmented] = useState<string>("ollama")
|
||||
|
||||
const { t } = useTranslation(["settings", "common", "openai"])
|
||||
|
||||
const form = useForm({
|
||||
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) => {
|
||||
notification.info({
|
||||
message: t("manageModels.notification.pullModel"),
|
||||
@@ -86,130 +70,26 @@ export const ModelsBody = () => {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{status === "pending" && <Skeleton paragraph={{ rows: 8 }} />}
|
||||
|
||||
{status === "success" && (
|
||||
<div className="overflow-x-auto">
|
||||
<Table
|
||||
columns={[
|
||||
<div className="flex items-center justify-end mt-3">
|
||||
<Segmented
|
||||
options={[
|
||||
{
|
||||
title: t("manageModels.columns.name"),
|
||||
dataIndex: "name",
|
||||
key: "name"
|
||||
label: t("common:segmented.ollama"),
|
||||
value: "ollama"
|
||||
},
|
||||
{
|
||||
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>
|
||||
)
|
||||
label: t("common:segmented.custom"),
|
||||
value: "custom"
|
||||
}
|
||||
]}
|
||||
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
|
||||
onChange={(value) => {
|
||||
setSegmented(value)
|
||||
}}
|
||||
bordered
|
||||
dataSource={data}
|
||||
rowKey={(record) => `${record.model}-${record.digest}`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{segmented === "ollama" ? <OllamaModelsTable /> : <CustomModelsTable />}
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
|
||||
Reference in New Issue
Block a user