diff --git a/enjoy/src/i18n/en.json b/enjoy/src/i18n/en.json
index 36d5f560..1ca078c1 100644
--- a/enjoy/src/i18n/en.json
+++ b/enjoy/src/i18n/en.json
@@ -401,6 +401,9 @@
"selectScenario": "Select scenario",
"selectAiEngine": "Select AI engine",
"selectAiModel": "Select AI model",
+ "selectTtsEngine": "Select TTS engine",
+ "selectTtsModel": "Select TTS model",
+ "selectTtsVoice": "Select TTS voice",
"youNeedToSetupApiKeyBeforeUsingOpenAI": "You need to setup API key before using OpenAI",
"ensureYouHaveOllamaRunningLocallyAndHasAtLeastOneModel": "Ensure you have Ollama running locally and has at least one model",
"creatingSpeech": "Speech is creating",
diff --git a/enjoy/src/i18n/zh-CN.json b/enjoy/src/i18n/zh-CN.json
index ef6c2120..23f89ef9 100644
--- a/enjoy/src/i18n/zh-CN.json
+++ b/enjoy/src/i18n/zh-CN.json
@@ -188,6 +188,7 @@
"confirm": "确认",
"continue": "继续",
"save": "保存",
+ "delete": "删除",
"edit": "修改",
"retry": "重试",
"failedToLogin": "登录失败",
@@ -200,7 +201,6 @@
"inputMixinId": "请输入您的 Mixin ID",
"dontHaveMixinAccount": "没有 Mixin 账号?",
"youCanAlsoLoginWith": "您也可以使用以下方式登录",
- "delete": "删除",
"transcribe": "语音转文本",
"stillTranscribing": "语音转文本仍在进行中,请耐心等候。",
"unableToSetLibraryPath": "无法设置资源库保存路径 {{path}}",
@@ -332,6 +332,7 @@
"accountSettings": "账户设置",
"advancedSettingsShort": "高级设置",
"advancedSettings": "高级设置",
+ "advanced": "高级设置",
"language": "语言",
"editEmail": "修改邮箱地址",
"editUserName": "修改用户名",
@@ -400,6 +401,9 @@
"selectScenario": "选择场景",
"selectAiEngine": "选择 AI 引擎",
"selectAiModel": "选择 AI 模型",
+ "selectTtsEngine": "选择 TTS 引擎",
+ "selectTtsModel": "选择 TTS 模型",
+ "selectTtsVoice": "选择 TTS 角色",
"youNeedToSetupApiKeyBeforeUsingOpenAI": "在使用 OpenAI 之前您需要设置 API 密钥",
"ensureYouHaveOllamaRunningLocallyAndHasAtLeastOneModel": "确保您已经在本地运行 Ollama 并且至少有一个模型",
"creatingSpeech": "正在生成语音",
diff --git a/enjoy/src/renderer/components/conversations/conversation-card.tsx b/enjoy/src/renderer/components/conversations/conversation-card.tsx
new file mode 100644
index 00000000..ec61d81b
--- /dev/null
+++ b/enjoy/src/renderer/components/conversations/conversation-card.tsx
@@ -0,0 +1,36 @@
+import { MessageCircleIcon, SpeechIcon } from "lucide-react";
+import dayjs from "dayjs";
+
+export const ConversationCard = (props: { conversation: ConversationType }) => {
+ const { conversation } = props;
+
+ return (
+
+
+ {conversation.type === "gpt" && }
+
+ {conversation.type === "tts" && }
+
+
+
+
{conversation.name}
+
+ {conversation.engine} /{" "}
+ {conversation.type === "tts"
+ ? conversation.configuration?.tts?.model
+ : conversation.model}
+
+
+
+ {dayjs(conversation.createdAt).format("HH:mm l")}
+
+
+
+ );
+};
diff --git a/enjoy/src/renderer/components/conversations/conversation-form.tsx b/enjoy/src/renderer/components/conversations/conversation-form.tsx
index f92b7768..5af528ae 100644
--- a/enjoy/src/renderer/components/conversations/conversation-form.tsx
+++ b/enjoy/src/renderer/components/conversations/conversation-form.tsx
@@ -28,6 +28,7 @@ import {
SelectContent,
SelectItem,
Textarea,
+ toast,
} from "@renderer/components/ui";
import { useState, useEffect, useContext } from "react";
import {
@@ -47,28 +48,24 @@ const conversationFormSchema = z.object({
engine: z
.enum(["enjoyai", "openai", "ollama", "googleGenerativeAi"])
.default("openai"),
- configuration: z
- .object({
- type: z.enum(["gpt", "tts"]),
- model: z.string().optional(),
+ configuration: z.object({
+ type: z.enum(["gpt", "tts"]),
+ model: z.string().optional(),
+ baseUrl: z.string().optional(),
+ roleDefinition: z.string().optional(),
+ temperature: z.number().min(0).max(1).default(0.2),
+ numberOfChoices: z.number().min(1).default(1),
+ maxTokens: z.number().min(-1).default(2000),
+ presencePenalty: z.number().min(-2).max(2).default(0),
+ frequencyPenalty: z.number().min(-2).max(2).default(0),
+ historyBufferSize: z.number().min(0).default(10),
+ tts: z.object({
+ engine: z.enum(["openai", "enjoyai"]).default("enjoyai"),
+ model: z.string().default("tts-1"),
+ voice: z.string(),
baseUrl: z.string().optional(),
- roleDefinition: z.string().optional(),
- temperature: z.number().min(0).max(1).default(0.2),
- numberOfChoices: z.number().min(1).default(1),
- maxTokens: z.number().min(-1).default(2000),
- presencePenalty: z.number().min(-2).max(2).default(0),
- frequencyPenalty: z.number().min(-2).max(2).default(0),
- historyBufferSize: z.number().min(0).default(10),
- tts: z
- .object({
- engine: z.enum(["openai", "enjoyai"]).default("openai"),
- model: z.string().default("tts-1"),
- voice: z.string().optional(),
- baseUrl: z.string().optional(),
- })
- .optional(),
- })
- .optional(),
+ }),
+ }),
});
export const ConversationForm = (props: {
@@ -77,8 +74,8 @@ export const ConversationForm = (props: {
}) => {
const { conversation, onFinish } = props;
const [submitting, setSubmitting] = useState(false);
- const [gptProviders, setGptProviders] = useState(GPT_PROVIDERS);
- const [ttsProviders, setTtsProviders] = useState(TTS_PROVIDERS);
+ const [gptProviders, setGptProviders] = useState([]);
+ const [ttsProviders, setTtsProviders] = useState([]);
const { EnjoyApp, webApi } = useContext(AppSettingsProviderContext);
const { openai } = useContext(AISettingsProviderContext);
const navigate = useNavigate();
@@ -132,6 +129,7 @@ export const ConversationForm = (props: {
}, []);
const defaultConfig = JSON.parse(JSON.stringify(conversation || {}));
+
if (defaultConfig.engine === "openai" && openai) {
if (!defaultConfig.configuration) {
defaultConfig.configuration = {};
@@ -172,31 +170,15 @@ export const ConversationForm = (props: {
});
const onSubmit = async (data: z.infer) => {
- const { name, engine, configuration } = data;
+ let { name, engine, configuration } = data;
setSubmitting(true);
- Object.keys(configuration).forEach((key) => {
- if (key === "type") return;
-
- if (!GPT_PROVIDERS[engine]?.configurable.includes(key)) {
- // @ts-ignore
- delete configuration[key];
- }
- });
-
- if (configuration.type === "tts") {
- conversation.model = configuration.tts.model;
- }
-
- // use default base url if not set
- if (!configuration.baseUrl) {
- configuration.baseUrl = GPT_PROVIDERS[engine]?.baseUrl;
- }
-
- // use default base url if not set
- if (!configuration?.tts?.baseUrl) {
- configuration.tts ||= {};
- configuration.tts.baseUrl = GPT_PROVIDERS[engine]?.baseUrl;
+ try {
+ configuration = validateConfiguration(data);
+ } catch (e) {
+ toast.error(e.message);
+ setSubmitting(false);
+ return;
}
if (conversation?.id) {
@@ -227,6 +209,54 @@ export const ConversationForm = (props: {
}
};
+ const validateConfiguration = (
+ data: z.infer
+ ) => {
+ const { engine, configuration } = data;
+
+ Object.keys(configuration).forEach((key) => {
+ if (key === "type") return;
+
+ if (
+ configuration.type === "gpt" &&
+ !gptProviders[engine]?.configurable.includes(key)
+ ) {
+ // @ts-ignore
+ delete configuration[key];
+ }
+
+ if (
+ configuration.type === "tts" &&
+ !ttsProviders[engine]?.configurable.includes(key)
+ ) {
+ // @ts-ignore
+ delete configuration.tts[key];
+ }
+ });
+
+ if (configuration.type === "tts") {
+ if (!configuration.tts?.engine) {
+ throw new Error(t("models.conversation.ttsEngineRequired"));
+ }
+ if (!configuration.tts?.model) {
+ throw new Error(t("models.conversation.ttsModelRequired"));
+ }
+ }
+
+ // use default base url if not set
+ if (!configuration.baseUrl) {
+ configuration.baseUrl = gptProviders[engine]?.baseUrl;
+ }
+
+ // use default base url if not set
+ if (!configuration?.tts?.baseUrl) {
+ configuration.tts ||= {};
+ configuration.tts.baseUrl = gptProviders[engine]?.baseUrl;
+ }
+
+ return configuration;
+ };
+
return (
- {GPT_PROVIDERS[form.watch("engine")]?.configurable.includes(
+ {gptProviders[form.watch("engine")]?.configurable.includes(
"temperature"
) && (
)}
- {GPT_PROVIDERS[form.watch("engine")]?.configurable.includes(
+ {gptProviders[form.watch("engine")]?.configurable.includes(
"maxTokens"
) && (
)}
- {GPT_PROVIDERS[form.watch("engine")]?.configurable.includes(
+ {gptProviders[form.watch("engine")]?.configurable.includes(
"presencePenalty"
) && (
)}
- {GPT_PROVIDERS[form.watch("engine")]?.configurable.includes(
+ {gptProviders[form.watch("engine")]?.configurable.includes(
"frequencyPenalty"
) && (
)}
- {GPT_PROVIDERS[form.watch("engine")]?.configurable.includes(
+ {gptProviders[form.watch("engine")]?.configurable.includes(
"numberOfChoices"
) && (
- {GPT_PROVIDERS[form.watch("engine")]?.configurable.includes(
+ {gptProviders[form.watch("engine")]?.configurable.includes(
"baseUrl"
) && (
([]);
const [loading, setLoading] = useState(false);
- const [offset, setOffset] = useState(0);
+ const [hasMore, setHasMore] = useState(false);
const { chat } = useConversation();
const [replies, setReplies] = useState[]>([]);
const navigate = useNavigate();
const fetchConversations = () => {
- if (offset === -1) return;
-
const limit = 5;
+
setLoading(true);
EnjoyApp.conversations
.findAll({
order: [["updatedAt", "DESC"]],
limit,
- offset,
+ offset: conversations.length,
})
.then((_conversations) => {
if (_conversations.length === 0) {
- setOffset(-1);
+ setHasMore(false);
return;
}
if (_conversations.length < limit) {
- setOffset(-1);
+ setHasMore(false);
} else {
- setOffset(offset + _conversations.length);
+ setHasMore(true);
}
- if (offset === 0) {
+ if (conversations.length === 0) {
setConversations(_conversations);
} else {
setConversations([...conversations, ..._conversations]);
@@ -99,7 +93,7 @@ export const ConversationShortcuts = (props: {
useEffect(() => {
fetchConversations();
- }, [excludedIds]);
+ }, []);
const dialogContent = () => {
if (loading) {
@@ -151,37 +145,18 @@ export const ConversationShortcuts = (props: {
.filter((c) => !excludedIds.includes(c.id))
.map((conversation) => {
return (
- ask(conversation)}
- className="bg-background text-primary rounded-full w-full mb-2 py-2 px-4 hover:bg-muted hover:text-muted-foreground cursor-pointer flex items-center border"
- style={{
- borderLeftColor: `#${conversation.id
- .replaceAll("-", "")
- .substr(0, 6)}`,
- borderLeftWidth: 3,
- }}
- >
-
- {conversation.type === "gpt" && (
-
- )}
-
- {conversation.type === "tts" && (
-
- )}
-
-
{conversation.name}
+
ask(conversation)}>
+
);
})}
- {offset > -1 && (
+ {hasMore && (
);
diff --git a/enjoy/src/renderer/reducers/conversations-reducer.ts b/enjoy/src/renderer/reducers/conversations-reducer.ts
index 22100576..40df75f0 100644
--- a/enjoy/src/renderer/reducers/conversations-reducer.ts
+++ b/enjoy/src/renderer/reducers/conversations-reducer.ts
@@ -1,12 +1,19 @@
export const conversationsReducer = (
state: ConversationType[],
action: {
- type: "create" | "update" | "destroy" | "set";
+ type: "append" | "create" | "update" | "destroy" | "set";
record?: ConversationType;
records?: ConversationType[];
}
) => {
switch (action.type) {
+ case "append": {
+ if (action.record) {
+ return [...state, action.record];
+ } else if (action.records) {
+ return [...state, ...action.records];
+ }
+ }
case "create": {
return [action.record, ...state];
}