feat: OpenAI settings page
Add a new settings page for OpenAI configuration, including a dedicated tab in the settings layout, translations, and routing.
This commit is contained in:
@@ -6,12 +6,12 @@ import {
|
||||
BlocksIcon,
|
||||
InfoIcon,
|
||||
CombineIcon,
|
||||
ChromeIcon
|
||||
ChromeIcon,
|
||||
CloudCogIcon
|
||||
} from "lucide-react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Link, useLocation } from "react-router-dom"
|
||||
import { OllamaIcon } from "../Icons/Ollama"
|
||||
import { Tag } from "antd"
|
||||
import { BetaTag } from "../Common/Beta"
|
||||
|
||||
function classNames(...classes: string[]) {
|
||||
@@ -22,12 +22,11 @@ const LinkComponent = (item: {
|
||||
href: string
|
||||
name: string | JSX.Element
|
||||
icon: any
|
||||
current: string,
|
||||
current: string
|
||||
beta?: boolean
|
||||
}) => {
|
||||
return (
|
||||
<li className="inline-flex items-center">
|
||||
|
||||
<Link
|
||||
to={item.href}
|
||||
className={classNames(
|
||||
@@ -47,16 +46,14 @@ const LinkComponent = (item: {
|
||||
/>
|
||||
{item.name}
|
||||
</Link>
|
||||
{
|
||||
item.beta && <BetaTag />
|
||||
}
|
||||
{item.beta && <BetaTag />}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
export const SettingsLayout = ({ children }: { children: React.ReactNode }) => {
|
||||
const location = useLocation()
|
||||
const { t } = useTranslation(["settings", "common"])
|
||||
const { t } = useTranslation(["settings", "common", "openai"])
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -93,6 +90,13 @@ export const SettingsLayout = ({ children }: { children: React.ReactNode }) => {
|
||||
beta
|
||||
/>
|
||||
)}
|
||||
<LinkComponent
|
||||
href="/settings/openai"
|
||||
name={t("openai:settings")}
|
||||
icon={CloudCogIcon}
|
||||
current={location.pathname}
|
||||
beta
|
||||
/>
|
||||
<LinkComponent
|
||||
href="/settings/model"
|
||||
name={t("manageModels.title")}
|
||||
|
||||
218
src/components/Option/Settings/openai.tsx
Normal file
218
src/components/Option/Settings/openai.tsx
Normal file
@@ -0,0 +1,218 @@
|
||||
import { Form, Input, Modal, Table, message, Tooltip } from "antd"
|
||||
import { useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import {
|
||||
addOpenAICofig,
|
||||
getAllOpenAIConfig,
|
||||
deleteOpenAIConfig,
|
||||
updateOpenAIConfig
|
||||
} from "@/db/openai"
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||
import { Pencil, Trash2, Plus } from "lucide-react"
|
||||
|
||||
export const OpenAIApp = () => {
|
||||
const { t } = useTranslation("openai")
|
||||
const [open, setOpen] = useState(false)
|
||||
const [editingConfig, setEditingConfig] = useState(null)
|
||||
const queryClient = useQueryClient()
|
||||
const [form] = Form.useForm()
|
||||
|
||||
const { data: configs, isLoading } = useQuery({
|
||||
queryKey: ["openAIConfigs"],
|
||||
queryFn: getAllOpenAIConfig
|
||||
})
|
||||
|
||||
const addMutation = useMutation({
|
||||
mutationFn: addOpenAICofig,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["openAIConfigs"]
|
||||
})
|
||||
setOpen(false)
|
||||
message.success(t("addSuccess"))
|
||||
}
|
||||
})
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: updateOpenAIConfig,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["openAIConfigs"]
|
||||
})
|
||||
setOpen(false)
|
||||
message.success(t("updateSuccess"))
|
||||
}
|
||||
})
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: deleteOpenAIConfig,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["openAIConfigs"]
|
||||
})
|
||||
message.success(t("deleteSuccess"))
|
||||
}
|
||||
})
|
||||
|
||||
const handleSubmit = (values: {
|
||||
id?: string
|
||||
name: string
|
||||
baseUrl: string
|
||||
apiKey: string
|
||||
}) => {
|
||||
if (editingConfig) {
|
||||
updateMutation.mutate({ id: editingConfig.id, ...values })
|
||||
} else {
|
||||
addMutation.mutate(values)
|
||||
}
|
||||
}
|
||||
|
||||
const handleEdit = (record: any) => {
|
||||
setEditingConfig(record)
|
||||
setOpen(true)
|
||||
form.setFieldsValue(record)
|
||||
}
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
deleteMutation.mutate(id)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<div>
|
||||
<h2 className="text-base font-semibold leading-7 text-gray-900 dark:text-white">
|
||||
{t("heading")}
|
||||
</h2>
|
||||
<p className="mt-1 text-sm leading-6 text-gray-600 dark:text-gray-400">
|
||||
{t("subheading")}
|
||||
</p>
|
||||
<div className="border border-b border-gray-200 dark:border-gray-600 mt-3 mb-6"></div>
|
||||
</div>
|
||||
<div className="mb-6">
|
||||
<div className="-ml-4 -mt-2 flex flex-wrap items-center justify-end sm:flex-nowrap">
|
||||
<div className="ml-4 mt-2 flex-shrink-0">
|
||||
<button
|
||||
onClick={() => {
|
||||
setEditingConfig(null)
|
||||
setOpen(true)
|
||||
form.resetFields()
|
||||
}}
|
||||
className="inline-flex items-center rounded-md border border-transparent bg-black px-2 py-2 text-md font-medium leading-4 text-white shadow-sm hover:bg-gray-800 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">
|
||||
{t("addBtn")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
columns={[
|
||||
{
|
||||
title: t("table.name"),
|
||||
dataIndex: "name",
|
||||
key: "name"
|
||||
},
|
||||
{
|
||||
title: t("table.baseUrl"),
|
||||
dataIndex: "baseUrl",
|
||||
key: "baseUrl"
|
||||
},
|
||||
{
|
||||
title: t("table.actions"),
|
||||
key: "actions",
|
||||
render: (_, record) => (
|
||||
<div className="flex gap-4">
|
||||
<Tooltip title={t("edit")}>
|
||||
<button
|
||||
className="text-gray-700 dark:text-gray-400"
|
||||
onClick={() => handleEdit(record)}>
|
||||
<Pencil className="size-4" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip title={t("delete")}>
|
||||
<button
|
||||
className="text-red-500 dark:text-red-400"
|
||||
onClick={() => {
|
||||
// add confirmation here
|
||||
if (
|
||||
confirm(
|
||||
t("modal.deleteConfirm", {
|
||||
name: record.name
|
||||
})
|
||||
)
|
||||
) {
|
||||
handleDelete(record.id)
|
||||
}
|
||||
}}>
|
||||
<Trash2 className="size-4" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
]}
|
||||
dataSource={configs}
|
||||
loading={isLoading}
|
||||
rowKey="id"
|
||||
/>
|
||||
|
||||
<Modal
|
||||
open={open}
|
||||
title={editingConfig ? t("modal.titleEdit") : t("modal.titleAdd")}
|
||||
onCancel={() => {
|
||||
setOpen(false)
|
||||
setEditingConfig(null)
|
||||
form.resetFields()
|
||||
}}
|
||||
footer={null}>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleSubmit}
|
||||
initialValues={editingConfig}>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label={t("modal.name.label")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("modal.name.required")
|
||||
}
|
||||
]}>
|
||||
<Input size="large" placeholder={t("modal.name.placeholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="baseUrl"
|
||||
label={t("modal.baseUrl.label")}
|
||||
help={t("modal.baseUrl.help")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t("modal.baseUrl.required")
|
||||
}
|
||||
]}>
|
||||
<Input
|
||||
size="large"
|
||||
placeholder={t("modal.baseUrl.placeholder")}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="apiKey" label={t("modal.apiKey.label")}>
|
||||
<Input.Password
|
||||
size="large"
|
||||
placeholder={t("modal.apiKey.placeholder")}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
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">
|
||||
{editingConfig ? t("modal.update") : t("modal.submit")}
|
||||
</button>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user