mirror of
https://github.com/sqlchat/sqlchat.git
synced 2026-03-13 07:59:44 +08:00
Merge branch 'main' into payment
This commit is contained in:
@@ -34,11 +34,7 @@ SQL Chat is built by [Next.js](https://nextjs.org/), it supports the following d
|
||||
|
||||
## Data Privacy
|
||||
|
||||
- All database connection configs are stored locally in your browser. You can also visit settings to clear the data.
|
||||
|
||||
- Only the database schema will be sent to the OpenAI API. No table data will be sent there.
|
||||
|
||||
- If you use [sqlchat.ai](https://sqlchat.ai), it will record the anonymised conversations.
|
||||
See [SQL Chat Privacy Policy](https://sqlchat.ai/privacy).
|
||||
|
||||
## IP Whitelisting
|
||||
|
||||
|
||||
46
public/privacy.md
Normal file
46
public/privacy.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# SQL Chat Privacy Policy
|
||||
|
||||
*Last Updated: May 21, 2023*
|
||||
|
||||
We respect your privacy and are committed to protecting your personal information. This Privacy Policy explains how we collect, use, and disclose your personal information when you use our application that calls LLM provider's API.
|
||||
|
||||
## Collection of Application Data
|
||||
|
||||
* All database connection configs are stored locally in your browser. You can also visit settings to clear the data.
|
||||
* We send the database schema to the LLM provider. No table data will be sent there.
|
||||
* We collect the conversation message to improve the product.
|
||||
|
||||
## Collection of Personal Information
|
||||
|
||||
We may collect certain personal information from you when you use our application, such as your name and email address. We collect this information in order to communicate with you about our services and to provide you with technical support.
|
||||
|
||||
## Use of Personal Information
|
||||
|
||||
We may use your personal information to:
|
||||
|
||||
* Communicate with you about our services
|
||||
* Provide you with technical support
|
||||
* Improve our application and services
|
||||
* Comply with legal obligations
|
||||
|
||||
## Disclosure of Personal Information
|
||||
|
||||
We do not sell, trade, or otherwise transfer your personal information to third parties. However, we may disclose your personal information in the following circumstances:
|
||||
|
||||
* To our service providers who assist us in providing our services to you
|
||||
* To comply with legal obligations or to protect our rights or the rights of others
|
||||
|
||||
## Security of Personal Information
|
||||
|
||||
We take reasonable measures to protect your personal information from unauthorized access, disclosure, or destruction. However, no method of transmission over the internet or electronic storage is completely secure, and we cannot guarantee absolute security.
|
||||
|
||||
|
||||
## Changes to this Privacy Policy
|
||||
|
||||
We may update this Privacy Policy from time to time by posting a new versionon our website. We encourage you to review this Privacy Policy periodically for any changes. Your continued use of our application and services after any modifications to this Privacy Policy will constitute your acceptance of such modifications.
|
||||
|
||||
For more information about our privacy and data protection practices, including how to exercise your privacy rights, please visit the [Bytebase Privacy Policy](https://bytebase.com/privacy).
|
||||
|
||||
## Contact Us
|
||||
|
||||
Please contact us at support@bytebase.com if you have questions about this Privacy Policy.
|
||||
12
public/terms.md
Normal file
12
public/terms.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# Terms of Service
|
||||
|
||||
*Last Updated: May 21, 2023*
|
||||
|
||||
## Refund Policy
|
||||
|
||||
All fees and charges are earned upon receipt by us and are nonrefundable.
|
||||
|
||||
|
||||
## Contact Us
|
||||
|
||||
Please contact us at support@bytebase.com if you have questions about this Terms of Service.
|
||||
@@ -49,9 +49,11 @@ const Header = (props: Props) => {
|
||||
>
|
||||
<img className="h-5 sm:h-6 w-auto" src={isDarkMode ? "/craft-by-bytebase-dark-mode.webp" : "/craft-by-bytebase.webp"} alt="" />
|
||||
</a>
|
||||
<button className="p-2 rounded cursor-pointer hover:bg-gray-100 dark:hover:bg-zinc-700">
|
||||
<Icon.FiSettings className="w-5 h-auto" onClick={() => setShowSchemaDrawer(true)} />
|
||||
</button>
|
||||
{hasFeature("debug") && (
|
||||
<button className="p-2 rounded cursor-pointer hover:bg-gray-100 dark:hover:bg-zinc-700">
|
||||
<Icon.FiSettings className="w-5 h-auto" onClick={() => setShowSchemaDrawer(true)} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -217,6 +217,9 @@ const ConversationView = () => {
|
||||
if (settingStore.setting.openAIApiConfig?.endpoint) {
|
||||
requestHeaders["x-openai-endpoint"] = settingStore.setting.openAIApiConfig?.endpoint;
|
||||
}
|
||||
if (settingStore.setting.openAIApiConfig?.model) {
|
||||
requestHeaders["x-openai-model"] = settingStore.setting.openAIApiConfig?.model;
|
||||
}
|
||||
const rawRes = await fetch("/api/chat", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
@@ -311,7 +314,7 @@ const ConversationView = () => {
|
||||
messages: usageMessageList,
|
||||
},
|
||||
{
|
||||
headers: session?.user.id ? { Authorization: `Bearer ${session?.user.id}` } : undefined,
|
||||
headers: requestHeaders,
|
||||
}
|
||||
)
|
||||
.catch(() => {
|
||||
|
||||
@@ -4,27 +4,28 @@ import { toast } from "react-hot-toast";
|
||||
import TextareaAutosize from "react-textarea-autosize";
|
||||
import { useConnectionStore } from "@/store";
|
||||
import { Connection, Engine, ResponseObject, SSLOptions } from "@/types";
|
||||
import Select from "./kit/Select";
|
||||
import Radio from "./kit/Radio";
|
||||
import TextField from "./kit/TextField";
|
||||
import Modal from "./kit/Modal";
|
||||
import Icon from "./Icon";
|
||||
import DataStorageBanner from "./DataStorageBanner";
|
||||
import ActionConfirmModal from "./ActionConfirmModal";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import EngineIcon from "./EngineIcon";
|
||||
|
||||
interface Props {
|
||||
connection?: Connection;
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
type SSLType = "none" | "ca-only" | "full";
|
||||
type SSLType = "preferred" | "ca-only" | "full";
|
||||
|
||||
type SSLFieldType = "ca" | "cert" | "key";
|
||||
|
||||
const SSLTypeOptions = [
|
||||
{
|
||||
label: "None",
|
||||
value: "none",
|
||||
label: "Preferred",
|
||||
value: "preferred",
|
||||
},
|
||||
{
|
||||
label: "CA Only",
|
||||
@@ -36,19 +37,35 @@ const SSLTypeOptions = [
|
||||
},
|
||||
];
|
||||
|
||||
const defaultPort = {
|
||||
[Engine.MySQL]: "3306",
|
||||
[Engine.PostgreSQL]: "5432",
|
||||
[Engine.MSSQL]: "1433",
|
||||
[Engine.TiDBServerless]: "4000",
|
||||
};
|
||||
const engines = [
|
||||
{
|
||||
type: Engine.MySQL,
|
||||
name: "MySQL",
|
||||
defaultPort: "3306",
|
||||
},
|
||||
{
|
||||
type: Engine.PostgreSQL,
|
||||
name: "PostgreSQL",
|
||||
defaultPort: "5432",
|
||||
},
|
||||
{
|
||||
type: Engine.MSSQL,
|
||||
name: "SQL Server",
|
||||
defaultPort: "1433",
|
||||
},
|
||||
{
|
||||
type: Engine.TiDBServerless,
|
||||
name: "TiDB Serverless",
|
||||
defaultPort: "4000",
|
||||
},
|
||||
];
|
||||
|
||||
const defaultConnection: Connection = {
|
||||
id: "",
|
||||
title: "",
|
||||
engineType: Engine.MySQL,
|
||||
host: "",
|
||||
port: defaultPort[Engine.MySQL],
|
||||
port: "3306",
|
||||
username: "",
|
||||
password: "",
|
||||
};
|
||||
@@ -59,7 +76,7 @@ const CreateConnectionModal = (props: Props) => {
|
||||
const connectionStore = useConnectionStore();
|
||||
const [connection, setConnection] = useState<Connection>(defaultConnection);
|
||||
const [showDeleteConnectionModal, setShowDeleteConnectionModal] = useState(false);
|
||||
const [sslType, setSSLType] = useState<SSLType>("none");
|
||||
const [sslType, setSSLType] = useState<SSLType>("preferred");
|
||||
const [selectedSSLField, setSelectedSSLField] = useState<SSLFieldType>("ca");
|
||||
const [isRequesting, setIsRequesting] = useState(false);
|
||||
const showDatabaseField = connection.engineType === Engine.PostgreSQL;
|
||||
@@ -102,7 +119,6 @@ const CreateConnectionModal = (props: Props) => {
|
||||
setConnection({
|
||||
...connection,
|
||||
...state,
|
||||
port: defaultPort[state.engineType || Engine.MySQL],
|
||||
});
|
||||
};
|
||||
|
||||
@@ -216,17 +232,24 @@ const CreateConnectionModal = (props: Props) => {
|
||||
<DataStorageBanner className="rounded-lg bg-white border dark:border-zinc-700 py-2 !justify-start" alwaysShow={true} />
|
||||
<div className="w-full flex flex-col">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">{t("connection.database-type")}</label>
|
||||
<Select
|
||||
className="w-full"
|
||||
value={connection.engineType}
|
||||
itemList={[
|
||||
{ value: Engine.MySQL, label: "MySQL" },
|
||||
{ value: Engine.PostgreSQL, label: "PostgreSQL" },
|
||||
{ value: Engine.MSSQL, label: "MSSQL" },
|
||||
{ value: Engine.TiDBServerless, label: "TiDB Serverless Tier" },
|
||||
]}
|
||||
onValueChange={(value) => setPartialConnection({ engineType: value as Engine })}
|
||||
/>
|
||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
{engines.map((engine) => (
|
||||
<div
|
||||
key={engine.type}
|
||||
className="relative flex items-center space-x-3 rounded-lg border border-gray-300 bg-white px-4 py-2 shadow-sm focus-within:ring-2 focus-within:ring-indigo-500 focus-within:ring-offset-2 hover:border-gray-400"
|
||||
>
|
||||
<Radio
|
||||
value={engine.type}
|
||||
checked={connection.engineType === engine.type}
|
||||
onChange={(value) => setPartialConnection({ engineType: value as Engine, port: engine.defaultPort as string })}
|
||||
/>
|
||||
<EngineIcon className="h-8 w-8 rounded-full" engine={engine.type} />
|
||||
<label htmlFor={engine.type} className="ml-3 block text-sm font-medium leading-6 text-gray-900">
|
||||
{engine.name}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full flex flex-col">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">{t("connection.title")}</label>
|
||||
@@ -277,18 +300,17 @@ const CreateConnectionModal = (props: Props) => {
|
||||
<div className="w-full flex flex-row justify-start items-start flex-wrap">
|
||||
{SSLTypeOptions.map((option) => (
|
||||
<label key={option.value} className="w-auto flex flex-row justify-start items-center cursor-pointer mr-3 mb-3">
|
||||
<input
|
||||
type="radio"
|
||||
<Radio
|
||||
className="radio w-4 h-4 mr-1"
|
||||
value={option.value}
|
||||
checked={sslType === option.value}
|
||||
onChange={(e) => setSSLType(e.target.value as SSLType)}
|
||||
onChange={(value) => setSSLType(value as SSLType)}
|
||||
/>
|
||||
<span className="text-sm">{option.label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
{sslType !== "none" && (
|
||||
{sslType !== "preferred" && (
|
||||
<>
|
||||
<div className="text-sm space-x-3 mb-2">
|
||||
<span
|
||||
@@ -366,7 +388,7 @@ const CreateConnectionModal = (props: Props) => {
|
||||
<div>
|
||||
{isEditing && (
|
||||
<button className="btn btn-outline" onClick={() => setShowDeleteConnectionModal(true)}>
|
||||
Delete
|
||||
{t("common.delete")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
33
src/components/MarkdownRenderer.tsx
Normal file
33
src/components/MarkdownRenderer.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
|
||||
interface MarkdownRendererProps {
|
||||
url: string;
|
||||
}
|
||||
|
||||
function MarkdownRenderer(props: MarkdownRendererProps) {
|
||||
const [markdown, setMarkdown] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
const response = await fetch(props.url);
|
||||
const data = await response.text();
|
||||
setMarkdown(data);
|
||||
};
|
||||
fetchData();
|
||||
}, [props.url]);
|
||||
|
||||
return (
|
||||
<ReactMarkdown
|
||||
className="w-auto max-w-full px-4 py-2 prose prose-neutral"
|
||||
components={{
|
||||
p: ({ children }) => <p style={{ color: "black" }}>{children}</p>,
|
||||
li: ({ children }) => <li style={{ color: "black" }}>{children}</li>,
|
||||
}}
|
||||
>
|
||||
{markdown}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
}
|
||||
|
||||
export default MarkdownRenderer;
|
||||
@@ -3,14 +3,31 @@ import { useTranslation } from "react-i18next";
|
||||
import { useDebounce } from "react-use";
|
||||
import { useSettingStore } from "@/store";
|
||||
import { OpenAIApiConfig } from "@/types";
|
||||
import Radio from "./kit/Radio";
|
||||
import TextField from "./kit/TextField";
|
||||
|
||||
import Tooltip from "./kit/Tooltip";
|
||||
const OpenAIApiConfigView = () => {
|
||||
const { t } = useTranslation();
|
||||
const settingStore = useSettingStore();
|
||||
const [openAIApiConfig, setOpenAIApiConfig] = useState(settingStore.setting.openAIApiConfig);
|
||||
const [maskKey, setMaskKey] = useState(true);
|
||||
|
||||
const models = [
|
||||
{
|
||||
id: "gpt-3.5-turbo",
|
||||
title: `GPT-3.5 (${t("setting.openai-api-configuration.quota-per-ask", { count: 1 })})`,
|
||||
disabled: false,
|
||||
tooltip: "",
|
||||
},
|
||||
// Disable GPT-4 if user doesn't provide key (because SQL Chat own key hasn't been whitelisted yet).
|
||||
{
|
||||
id: "gpt-4",
|
||||
title: `GPT-4 (${t("setting.openai-api-configuration.quota-per-ask", { count: 10 })})`,
|
||||
disabled: !settingStore.setting.openAIApiConfig.key,
|
||||
tooltip: t("setting.openai-api-configuration.provide-gpt4-key"),
|
||||
},
|
||||
];
|
||||
|
||||
const maskedKey = (str: string) => {
|
||||
if (str.length < 7) {
|
||||
return str;
|
||||
@@ -37,21 +54,58 @@ const OpenAIApiConfigView = () => {
|
||||
setMaskKey(false);
|
||||
};
|
||||
|
||||
const modelRadio = (model: any) => {
|
||||
return (
|
||||
<div key={model.id} className="flex items-center">
|
||||
<Radio
|
||||
value={model.id}
|
||||
disabled={model.disabled}
|
||||
checked={openAIApiConfig.model === model.id}
|
||||
onChange={(value) => handleSetOpenAIApiConfig({ model: value })}
|
||||
/>
|
||||
<label htmlFor={model.id} className="ml-3 block text-sm font-medium leading-6 text-gray-900">
|
||||
{model.title}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-full border border-gray-200 dark:border-zinc-700 p-4 rounded-lg">
|
||||
<div className="flex flex-col">
|
||||
<label className="mb-1">OpenAI API Key</label>
|
||||
<div>
|
||||
<label className="text-base font-semibold ">{t("setting.openai-api-configuration.model")}</label>
|
||||
<p className="text-sm text-gray-500">{t("setting.openai-api-configuration.model-description")}</p>
|
||||
<fieldset className="mt-4">
|
||||
<div className="space-y-4 sm:flex sm:items-center sm:space-x-10 sm:space-y-0">
|
||||
{models.map((model) =>
|
||||
model.disabled ? (
|
||||
<Tooltip key={model.id} title={model.tooltip} side="top">
|
||||
{modelRadio(model)}
|
||||
</Tooltip>
|
||||
) : (
|
||||
modelRadio(model)
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div className="flex flex-col mt-4">
|
||||
<label className="text-base font-semibold">OpenAI API Key</label>
|
||||
<p className="text-sm text-gray-500">{t("setting.openai-api-configuration.key-description")}</p>
|
||||
<TextField
|
||||
className="mt-4"
|
||||
placeholder="OpenAI API Key"
|
||||
value={maskKey ? maskedKey(openAIApiConfig.key) : openAIApiConfig.key}
|
||||
onChange={(value) => handleSetOpenAIApiConfig({ key: value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col mt-3">
|
||||
<label className="mb-1">API Endpoint</label>
|
||||
<div className="flex flex-col mt-4">
|
||||
<label className="text-base font-semibold">OpenAI API Endpoint</label>
|
||||
<p className="text-sm text-gray-500">{t("setting.openai-api-configuration.endpoint-description")}</p>
|
||||
<TextField
|
||||
placeholder="OpenAI API Endpoint"
|
||||
className="mt-4"
|
||||
placeholder="API Endpoint"
|
||||
value={openAIApiConfig.endpoint}
|
||||
onChange={(value) => handleSetOpenAIApiConfig({ endpoint: value })}
|
||||
/>
|
||||
|
||||
@@ -58,7 +58,7 @@ const QuotaView = (props: Props) => {
|
||||
</div>
|
||||
<div className="flex justify-between pt-1">
|
||||
<div>{t("common.quota")}</div>
|
||||
<div className={quota.current >= quota.limit ? "text-red-600" : "text-black"}>
|
||||
<div className={quota.current >= quota.limit ? "text-red-600" : "text-black dark:text-gray-300"}>
|
||||
{quota.current}/{quota.limit}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -12,12 +12,12 @@ import LocaleSelector from "./LocaleSelector";
|
||||
import ThemeSelector from "./ThemeSelector";
|
||||
import OpenAIApiConfigView from "./OpenAIApiConfigView";
|
||||
|
||||
const SettingView = () => {
|
||||
const SettingGeneralView = () => {
|
||||
const { t } = useTranslation();
|
||||
const { data: session } = useSession();
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-col justify-start items-start space-y-3 py-4 sm:py-8 dark:bg-zinc-800">
|
||||
<div className="w-full flex flex-col justify-start items-start space-y-3 py-4 sm:py-8">
|
||||
<div className="w-full flex flex-row justify-start items-start flex-wrap gap-2">
|
||||
<a
|
||||
href="https://discord.gg/z6kakemDjm"
|
||||
@@ -61,6 +61,19 @@ const SettingView = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full border border-gray-200 dark:border-zinc-700 p-4 rounded-lg space-y-4">
|
||||
<div className="w-full flex flex-row justify-between items-center gap-2">
|
||||
<a href={"privacy"} target="_blank">
|
||||
Privacy
|
||||
</a>
|
||||
</div>
|
||||
<div className="w-full flex flex-row justify-between items-center gap-22">
|
||||
<a href={"terms"} target="_blank">
|
||||
Terms
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full border border-red-200 dark:border-zinc-700 p-4 rounded-lg">
|
||||
<div className="w-full flex flex-row justify-between items-center gap-2">
|
||||
<span>{t("setting.data.clear-all-data")}</span>
|
||||
@@ -71,4 +84,4 @@ const SettingView = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingView;
|
||||
export default SettingGeneralView;
|
||||
@@ -65,7 +65,7 @@ const SubscriptionHistoryTable = () => {
|
||||
</td>
|
||||
<td className="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-0">
|
||||
<a href={subscription.receipt} target="_blank" className="text-indigo-600 hover:text-indigo-900">
|
||||
{t("setting.billing.view-receipt")}
|
||||
{t("setting.subscription.view-receipt")}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
39
src/components/kit/Radio.tsx
Normal file
39
src/components/kit/Radio.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { HTMLInputTypeAttribute } from "react";
|
||||
|
||||
interface Props {
|
||||
value: string;
|
||||
onChange?: (value: string) => void;
|
||||
type?: HTMLInputTypeAttribute;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
checked?: boolean;
|
||||
}
|
||||
|
||||
const getDefaultProps = () => ({
|
||||
value: "",
|
||||
onChange: () => {},
|
||||
type: "radio",
|
||||
className: "",
|
||||
disabled: false,
|
||||
checked: false,
|
||||
});
|
||||
|
||||
const Radio = (props: Props) => {
|
||||
const { value, disabled, className, type, checked, onChange } = {
|
||||
...getDefaultProps(),
|
||||
...props,
|
||||
};
|
||||
|
||||
return (
|
||||
<input
|
||||
className={`${className || ""} h-4 w-4 border-gray-300 text-indigo-600 focus:ring-indigo-600 cursor-pointer`}
|
||||
type={type}
|
||||
disabled={disabled}
|
||||
value={value}
|
||||
checked={checked}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Radio;
|
||||
@@ -19,6 +19,11 @@ const getMySQLConnection = async (connection: Connection): Promise<mysql.Connect
|
||||
cert: connection.ssl?.cert,
|
||||
key: connection.ssl?.key,
|
||||
};
|
||||
} else {
|
||||
// rejectUnauthorized=false to infer sslmode=prefer since hosted MySQL venders have SSL enabled.
|
||||
connectionOptions.ssl = {
|
||||
rejectUnauthorized: false,
|
||||
};
|
||||
}
|
||||
const conn = await mysql.createConnection(connectionOptions);
|
||||
return conn;
|
||||
|
||||
@@ -2,13 +2,19 @@ import { Client, ClientConfig } from "pg";
|
||||
import { Connection, ExecutionResult } from "@/types";
|
||||
import { Connector } from "..";
|
||||
|
||||
const newPostgresClient = (connection: Connection) => {
|
||||
const systemSchemas =
|
||||
"'information_schema', 'pg_catalog', 'pg_toast', '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_internal', '_timescaledb_config', 'timescaledb_information', 'timescaledb_experimental'";
|
||||
|
||||
const systemTables = "'_prisma_migrations'";
|
||||
|
||||
const newPostgresClient = async (connection: Connection) => {
|
||||
const clientConfig: ClientConfig = {
|
||||
host: connection.host,
|
||||
port: Number(connection.port),
|
||||
user: connection.username,
|
||||
password: connection.password,
|
||||
database: connection.database,
|
||||
application_name: "sqlchat",
|
||||
};
|
||||
if (connection.ssl) {
|
||||
clientConfig.ssl = {
|
||||
@@ -16,21 +22,27 @@ const newPostgresClient = (connection: Connection) => {
|
||||
cert: connection.ssl?.cert,
|
||||
key: connection.ssl?.key,
|
||||
};
|
||||
} else {
|
||||
// rejectUnauthorized=false to infer sslmode=prefer since hosted PG venders have SSL enabled.
|
||||
clientConfig.ssl = {
|
||||
rejectUnauthorized: false,
|
||||
};
|
||||
}
|
||||
return new Client(clientConfig);
|
||||
|
||||
let client = new Client(clientConfig);
|
||||
await client.connect();
|
||||
return client;
|
||||
};
|
||||
|
||||
const testConnection = async (connection: Connection): Promise<boolean> => {
|
||||
const client = newPostgresClient(connection);
|
||||
await client.connect();
|
||||
const client = await newPostgresClient(connection);
|
||||
await client.end();
|
||||
return true;
|
||||
};
|
||||
|
||||
const execute = async (connection: Connection, databaseName: string, statement: string): Promise<any> => {
|
||||
connection.database = databaseName;
|
||||
const client = newPostgresClient(connection);
|
||||
await client.connect();
|
||||
const client = await newPostgresClient(connection);
|
||||
const { rows, rowCount } = await client.query(statement);
|
||||
await client.end();
|
||||
|
||||
@@ -46,8 +58,7 @@ const execute = async (connection: Connection, databaseName: string, statement:
|
||||
};
|
||||
|
||||
const getDatabases = async (connection: Connection): Promise<string[]> => {
|
||||
const client = newPostgresClient(connection);
|
||||
await client.connect();
|
||||
const client = await newPostgresClient(connection);
|
||||
if (connection.database) {
|
||||
await client.end();
|
||||
return [connection.database];
|
||||
@@ -67,16 +78,19 @@ const getDatabases = async (connection: Connection): Promise<string[]> => {
|
||||
|
||||
const getTables = async (connection: Connection, databaseName: string): Promise<string[]> => {
|
||||
connection.database = databaseName;
|
||||
const client = newPostgresClient(connection);
|
||||
await client.connect();
|
||||
const client = await newPostgresClient(connection);
|
||||
const { rows } = await client.query(
|
||||
`SELECT table_name FROM information_schema.tables WHERE table_schema='public' AND table_type='BASE TABLE' AND table_catalog=$1;`,
|
||||
`SELECT table_schema, table_name FROM information_schema.tables WHERE table_schema NOT IN (${systemSchemas}) AND table_name NOT IN (${systemTables}) AND table_type='BASE TABLE' AND table_catalog=$1;`,
|
||||
[databaseName]
|
||||
);
|
||||
await client.end();
|
||||
const tableList = [];
|
||||
for (const row of rows) {
|
||||
if (row["table_name"]) {
|
||||
if (row["table_schema"] !== "public") {
|
||||
tableList.push(`${row["table_schema"]}.${row["table_name"]}`);
|
||||
continue;
|
||||
}
|
||||
tableList.push(row["table_name"]);
|
||||
}
|
||||
}
|
||||
@@ -90,10 +104,9 @@ const getTableStructure = async (
|
||||
structureFetched: (tableName: string, structure: string) => void
|
||||
): Promise<void> => {
|
||||
connection.database = databaseName;
|
||||
const client = newPostgresClient(connection);
|
||||
await client.connect();
|
||||
const client = await newPostgresClient(connection);
|
||||
const { rows } = await client.query(
|
||||
`SELECT column_name, data_type, is_nullable FROM information_schema.columns WHERE table_schema='public' AND table_name=$1;`,
|
||||
`SELECT column_name, data_type, is_nullable FROM information_schema.columns WHERE table_schema NOT IN (${systemSchemas}) AND table_name=$1;`,
|
||||
[tableName]
|
||||
);
|
||||
await client.end();
|
||||
@@ -119,12 +132,11 @@ const getTableStructureBatch = async (
|
||||
structureFetched: (tableName: string, structure: string) => void
|
||||
): Promise<void> => {
|
||||
connection.database = databaseName;
|
||||
const client = newPostgresClient(connection);
|
||||
await client.connect();
|
||||
const client = await newPostgresClient(connection);
|
||||
await Promise.all(
|
||||
tableNameList.map(async (tableName) => {
|
||||
const { rows } = await client.query(
|
||||
`SELECT column_name, data_type, is_nullable FROM information_schema.columns WHERE table_schema='public' AND table_name=$1;`,
|
||||
`SELECT column_name, data_type, is_nullable FROM information_schema.columns WHERE table_schema NOT IN (${systemSchemas}) AND table_name=$1;`,
|
||||
[tableName]
|
||||
);
|
||||
const columnList = [];
|
||||
|
||||
@@ -68,11 +68,11 @@
|
||||
"upgrade": "Upgrade",
|
||||
"renew": "Renew",
|
||||
"expired": "Expired",
|
||||
"n-question-per-month": "{{count}} questions / month",
|
||||
"n-question-per-month": "{{count}} quota / month",
|
||||
"early-bird-checkout": "Early bird discount, 50% off for 1 year"
|
||||
},
|
||||
"billing": {
|
||||
"self": "Billing",
|
||||
"subscription": {
|
||||
"self": "Subscription",
|
||||
"view-receipt": "View receipt"
|
||||
},
|
||||
"account": {
|
||||
@@ -89,7 +89,13 @@
|
||||
"dark": "Dark"
|
||||
},
|
||||
"openai-api-configuration": {
|
||||
"self": "OpenAI API configuration"
|
||||
"self": "OpenAI API configuration",
|
||||
"model": "Model",
|
||||
"model-description": "Quota won't be consumed if you provide your own key below.",
|
||||
"quota-per-ask": "{{count}} quata per ask",
|
||||
"provide-gpt4-key": "Require your own GPT-4 enabled API key",
|
||||
"key-description": "Bring your own key to waive quota requirement.",
|
||||
"endpoint-description": "Optional endpoint pointing to your own compatible server or gateway."
|
||||
},
|
||||
"data": {
|
||||
"self": "Data",
|
||||
|
||||
@@ -66,11 +66,11 @@
|
||||
"upgrade": "Mejora",
|
||||
"renew": "Renovar",
|
||||
"expired": "Expirado",
|
||||
"n-question-per-month": "{{count}} preguntas / mes",
|
||||
"n-question-per-month": "{{count}} Cuota / mes",
|
||||
"early-bird-checkout": "Descuento por reserva anticipada, 50 % de descuento durante 1 año"
|
||||
},
|
||||
"billing": {
|
||||
"self": "Facturación",
|
||||
"subscription": {
|
||||
"self": "Suscripción",
|
||||
"view-receipt": "Ver recibo"
|
||||
},
|
||||
"account": {
|
||||
@@ -87,7 +87,13 @@
|
||||
"dark": "Oscuro"
|
||||
},
|
||||
"openai-api-configuration": {
|
||||
"self": "Configuración del API de OpenAI"
|
||||
"self": "Configuración del API de OpenAI",
|
||||
"model": "Modelo",
|
||||
"model-description": "La cuota no se consumirá si proporciona su propia clave a continuación.",
|
||||
"quota-per-ask": "{{count}} cuotas por pedido",
|
||||
"provide-gpt4-key": "Requerir su propia clave API habilitada para GPT-4",
|
||||
"key-description": "Traiga su propia llave para renunciar al requisito de cuota.",
|
||||
"endpoint-description": "Punto final opcional que apunta a su propio servidor o puerta de enlace compatible."
|
||||
},
|
||||
"data": {
|
||||
"self": "Datos",
|
||||
|
||||
@@ -68,11 +68,11 @@
|
||||
"upgrade": "升级",
|
||||
"renew": "续费",
|
||||
"expired": "已过期",
|
||||
"n-question-per-month": "{{count}} 次提问 / 月",
|
||||
"n-question-per-month": "{{count}} 点额度 / 月",
|
||||
"early-bird-checkout": "早鸟优惠,5 折购买 1 年"
|
||||
},
|
||||
"billing": {
|
||||
"self": "计费",
|
||||
"subscription": {
|
||||
"self": "订阅",
|
||||
"view-receipt": "查看收据"
|
||||
},
|
||||
"account": {
|
||||
@@ -89,7 +89,13 @@
|
||||
"dark": "深色"
|
||||
},
|
||||
"openai-api-configuration": {
|
||||
"self": "OpenAI API 配置"
|
||||
"self": "OpenAI API 配置",
|
||||
"model": "模型",
|
||||
"model-description": "如果您提供自己的 key,额度是不会消耗的。",
|
||||
"quota-per-ask": "每一个提问消耗 {{count}} 点额度",
|
||||
"provide-gpt4-key": "需提供您自己的,可以使用 GPT-4 的 key",
|
||||
"key-description": "一旦您提供了自己的 key,额度就不受限制了。",
|
||||
"endpoint-description": "可选的 endpoint 指向接口兼容的服务器或者网关。"
|
||||
},
|
||||
"data": {
|
||||
"self": "数据",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createParser, ParsedEvent, ReconnectInterval } from "eventsource-parser";
|
||||
import { NextRequest } from "next/server";
|
||||
import { openAIApiEndpoint, openAIApiKey, gpt35, hasFeature } from "@/utils";
|
||||
import { openAIApiEndpoint, openAIApiKey, hasFeature, getModel } from "@/utils";
|
||||
|
||||
// Needs Edge for streaming response.
|
||||
export const config = {
|
||||
@@ -90,6 +90,7 @@ const handler = async (req: NextRequest) => {
|
||||
}
|
||||
|
||||
const apiEndpoint = getApiEndpoint(req.headers.get("x-openai-endpoint") || openAIApiEndpoint);
|
||||
const model = getModel(req.headers.get("x-openai-model") || "");
|
||||
const remoteRes = await fetch(apiEndpoint, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -97,13 +98,14 @@ const handler = async (req: NextRequest) => {
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
model: gpt35.name,
|
||||
model: model.name,
|
||||
messages: reqBody.messages,
|
||||
temperature: gpt35.temperature,
|
||||
frequency_penalty: gpt35.frequency_penalty,
|
||||
presence_penalty: gpt35.presence_penalty,
|
||||
temperature: model.temperature,
|
||||
frequency_penalty: model.frequency_penalty,
|
||||
presence_penalty: model.presence_penalty,
|
||||
stream: true,
|
||||
// Send end-user ID to help OpenAI monitor and detect abuse.
|
||||
// Send end-user IP to help OpenAI monitor and detect abuse.
|
||||
// It's intentionally not using email to avoid leaking user's email to OpenAI.
|
||||
user: req.ip,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -30,15 +30,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
const params: Stripe.Checkout.SessionCreateParams = {
|
||||
submit_type: "pay",
|
||||
mode: "payment",
|
||||
payment_method_types: [
|
||||
"affirm",
|
||||
// "alipay",
|
||||
"card",
|
||||
"cashapp",
|
||||
"klarna",
|
||||
"link",
|
||||
// "wechat_pay",
|
||||
],
|
||||
payment_method_types: ["affirm", "alipay", "card", "cashapp", "klarna", "link", "wechat_pay"],
|
||||
line_items: [
|
||||
{
|
||||
price: process.env.STRIPE_PRICE_ID_PRO_1_YEAR_SUBSCRIPTION,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { Conversation, Message } from "@/types";
|
||||
import { gpt35 } from "@/utils";
|
||||
import { getModel, gpt35 } from "@/utils";
|
||||
import { getEndUser } from "./auth/end-user";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
@@ -36,7 +36,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
data: {
|
||||
id: conversation.id,
|
||||
createdAt: new Date(conversation.createdAt),
|
||||
model: gpt35,
|
||||
model: getModel((req.headers["x-openai-model"] as string) || ""),
|
||||
ctx: {},
|
||||
messages: {
|
||||
create: messages.map((message) => ({
|
||||
|
||||
8
src/pages/privacy/index.tsx
Normal file
8
src/pages/privacy/index.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import MarkdownRenderer from "@/components/MarkdownRenderer";
|
||||
import { NextPage } from "next";
|
||||
|
||||
const PrivacyPage: NextPage = () => {
|
||||
return <MarkdownRenderer url="/privacy.md" />;
|
||||
};
|
||||
|
||||
export default PrivacyPage;
|
||||
@@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next";
|
||||
import Link from "next/link";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { ArrowUturnLeftIcon, Bars3Icon, Cog6ToothIcon, XMarkIcon, CreditCardIcon } from "@heroicons/react/24/outline";
|
||||
import SettingView from "../../components/SettingView";
|
||||
import SettingGeneralView from "../../components/SettingGeneralView";
|
||||
import StripeCheckPaymentBanner from "../../components/StripeCheckPaymentBanner";
|
||||
|
||||
function classNames(...classes: string[]) {
|
||||
@@ -174,8 +174,8 @@ const SettingPage: NextPage = () => {
|
||||
|
||||
<main className="lg:pl-72">
|
||||
{router.query.session_id && <StripeCheckPaymentBanner sessionId={router.query.session_id as string} />}
|
||||
<div className="px-4 sm:px-6 lg:px-8">
|
||||
<SettingView />
|
||||
<div className="px-4 sm:px-6 lg:px-8 dark:bg-zinc-800">
|
||||
<SettingGeneralView />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
8
src/pages/terms/index.tsx
Normal file
8
src/pages/terms/index.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import MarkdownRenderer from "@/components/MarkdownRenderer";
|
||||
import { NextPage } from "next";
|
||||
|
||||
const PrivacyPage: NextPage = () => {
|
||||
return <MarkdownRenderer url="/terms.md" />;
|
||||
};
|
||||
|
||||
export default PrivacyPage;
|
||||
@@ -10,6 +10,7 @@ const getDefaultSetting = (): Setting => {
|
||||
openAIApiConfig: {
|
||||
key: "",
|
||||
endpoint: "",
|
||||
model: "gpt-3.5-turbo",
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ export type Theme = "light" | "dark" | "system";
|
||||
export interface OpenAIApiConfig {
|
||||
key: string;
|
||||
endpoint: string;
|
||||
model: string;
|
||||
}
|
||||
|
||||
export interface Setting {
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
export const gpt35 = {
|
||||
const gpt35 = {
|
||||
name: "gpt-3.5-turbo",
|
||||
temperature: 0,
|
||||
frequency_penalty: 0.0,
|
||||
presence_penalty: 0.0,
|
||||
};
|
||||
|
||||
const gpt4 = {
|
||||
name: "gpt-4",
|
||||
temperature: 0,
|
||||
frequency_penalty: 0.0,
|
||||
presence_penalty: 0.0,
|
||||
};
|
||||
|
||||
export const models = [gpt35, gpt4];
|
||||
|
||||
export const getModel = (name: string) => {
|
||||
for (const model of models) {
|
||||
if (model.name === name) {
|
||||
return model;
|
||||
}
|
||||
}
|
||||
return gpt35;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user