Improve conversation (#609)
* fix conversation form * refactor conversation list
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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": "正在生成语音",
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { MessageCircleIcon, SpeechIcon } from "lucide-react";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
export const ConversationCard = (props: { conversation: ConversationType }) => {
|
||||
const { conversation } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="bg-background hover:bg-muted hover:text-muted-foreground border rounded-full w-full mb-2 px-4 py-2 cursor-pointer flex items-center"
|
||||
style={{
|
||||
borderLeftColor: `#${conversation.id.replaceAll("-", "").slice(0, 6)}`,
|
||||
borderLeftWidth: 3,
|
||||
}}
|
||||
>
|
||||
<div className="">
|
||||
{conversation.type === "gpt" && <MessageCircleIcon className="mr-2" />}
|
||||
|
||||
{conversation.type === "tts" && <SpeechIcon className="mr-2" />}
|
||||
</div>
|
||||
<div className="flex-1 flex items-center justify-between space-x-4">
|
||||
<div className="">
|
||||
<div className="line-clamp-1 text-sm">{conversation.name}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{conversation.engine} /{" "}
|
||||
{conversation.type === "tts"
|
||||
? conversation.configuration?.tts?.model
|
||||
: conversation.model}
|
||||
</div>
|
||||
</div>
|
||||
<span className="min-w-fit text-sm text-muted-foreground">
|
||||
{dayjs(conversation.createdAt).format("HH:mm l")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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<boolean>(false);
|
||||
const [gptProviders, setGptProviders] = useState<any>(GPT_PROVIDERS);
|
||||
const [ttsProviders, setTtsProviders] = useState<any>(TTS_PROVIDERS);
|
||||
const [gptProviders, setGptProviders] = useState<any>([]);
|
||||
const [ttsProviders, setTtsProviders] = useState<any>([]);
|
||||
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<typeof conversationFormSchema>) => {
|
||||
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<typeof conversationFormSchema>
|
||||
) => {
|
||||
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 (
|
||||
<Form {...form}>
|
||||
<form
|
||||
@@ -367,7 +397,7 @@ export const ConversationForm = (props: {
|
||||
)}
|
||||
/>
|
||||
|
||||
{GPT_PROVIDERS[form.watch("engine")]?.configurable.includes(
|
||||
{gptProviders[form.watch("engine")]?.configurable.includes(
|
||||
"temperature"
|
||||
) && (
|
||||
<FormField
|
||||
@@ -401,7 +431,7 @@ export const ConversationForm = (props: {
|
||||
/>
|
||||
)}
|
||||
|
||||
{GPT_PROVIDERS[form.watch("engine")]?.configurable.includes(
|
||||
{gptProviders[form.watch("engine")]?.configurable.includes(
|
||||
"maxTokens"
|
||||
) && (
|
||||
<FormField
|
||||
@@ -430,7 +460,7 @@ export const ConversationForm = (props: {
|
||||
/>
|
||||
)}
|
||||
|
||||
{GPT_PROVIDERS[form.watch("engine")]?.configurable.includes(
|
||||
{gptProviders[form.watch("engine")]?.configurable.includes(
|
||||
"presencePenalty"
|
||||
) && (
|
||||
<FormField
|
||||
@@ -461,7 +491,7 @@ export const ConversationForm = (props: {
|
||||
/>
|
||||
)}
|
||||
|
||||
{GPT_PROVIDERS[form.watch("engine")]?.configurable.includes(
|
||||
{gptProviders[form.watch("engine")]?.configurable.includes(
|
||||
"frequencyPenalty"
|
||||
) && (
|
||||
<FormField
|
||||
@@ -492,7 +522,7 @@ export const ConversationForm = (props: {
|
||||
/>
|
||||
)}
|
||||
|
||||
{GPT_PROVIDERS[form.watch("engine")]?.configurable.includes(
|
||||
{gptProviders[form.watch("engine")]?.configurable.includes(
|
||||
"numberOfChoices"
|
||||
) && (
|
||||
<FormField
|
||||
@@ -555,7 +585,7 @@ export const ConversationForm = (props: {
|
||||
)}
|
||||
/>
|
||||
|
||||
{GPT_PROVIDERS[form.watch("engine")]?.configurable.includes(
|
||||
{gptProviders[form.watch("engine")]?.configurable.includes(
|
||||
"baseUrl"
|
||||
) && (
|
||||
<FormField
|
||||
@@ -611,7 +641,7 @@ export const ConversationForm = (props: {
|
||||
|
||||
{ttsProviders[
|
||||
form.watch("configuration.tts.engine")
|
||||
]?.configurable.includes("model") && (
|
||||
]?.configurable?.includes("model") && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="configuration.tts.model"
|
||||
@@ -647,7 +677,7 @@ export const ConversationForm = (props: {
|
||||
|
||||
{ttsProviders[
|
||||
form.watch("configuration.tts.engine")
|
||||
]?.configurable.includes("voice") && (
|
||||
]?.configurable?.includes("voice") && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="configuration.tts.voice"
|
||||
|
||||
@@ -11,13 +11,8 @@ import {
|
||||
ScrollArea,
|
||||
toast,
|
||||
} from "@renderer/components/ui";
|
||||
import { LoaderSpin } from "@renderer/components";
|
||||
import {
|
||||
MessageCircleIcon,
|
||||
LoaderIcon,
|
||||
SpeechIcon,
|
||||
CheckCircleIcon,
|
||||
} from "lucide-react";
|
||||
import { ConversationCard, LoaderSpin } from "@renderer/components";
|
||||
import { LoaderIcon, CheckCircleIcon } from "lucide-react";
|
||||
import { t } from "i18next";
|
||||
import { useConversation } from "@renderer/hooks";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
@@ -43,35 +38,34 @@ export const ConversationShortcuts = (props: {
|
||||
} = props;
|
||||
const [conversations, setConversations] = useState<ConversationType[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [offset, setOffset] = useState<number>(0);
|
||||
const [hasMore, setHasMore] = useState<boolean>(false);
|
||||
const { chat } = useConversation();
|
||||
const [replies, setReplies] = useState<Partial<MessageType>[]>([]);
|
||||
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 (
|
||||
<div
|
||||
key={conversation.id}
|
||||
onClick={() => 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,
|
||||
}}
|
||||
>
|
||||
<div className="">
|
||||
{conversation.type === "gpt" && (
|
||||
<MessageCircleIcon className="mr-2" />
|
||||
)}
|
||||
|
||||
{conversation.type === "tts" && (
|
||||
<SpeechIcon className="mr-2" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 truncated">{conversation.name}</div>
|
||||
<div key={conversation.id} onClick={() => ask(conversation)}>
|
||||
<ConversationCard conversation={conversation} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{offset > -1 && (
|
||||
{hasMore && (
|
||||
<div className="flex justify-center">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => fetchConversations()}
|
||||
disabled={loading || offset === -1}
|
||||
disabled={loading || !hasMore}
|
||||
className="px-4 py-2"
|
||||
>
|
||||
{t("loadMore")}
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { AppSettingsProviderContext } from "@renderer/context";
|
||||
import { Button, ScrollArea } from "@renderer/components/ui";
|
||||
import { LoaderSpin } from "@renderer/components";
|
||||
import { MessageCircleIcon, LoaderIcon, SpeechIcon } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { t } from "i18next";
|
||||
|
||||
export const ConversationsList = (props: {
|
||||
prompt: string;
|
||||
excludedIds?: string[];
|
||||
}) => {
|
||||
const { EnjoyApp } = useContext(AppSettingsProviderContext);
|
||||
const { prompt, excludedIds = [] } = props;
|
||||
const [conversations, setConversations] = useState<ConversationType[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [offset, setOffset] = useState<number>(0);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const fetchConversations = () => {
|
||||
if (offset === -1) return;
|
||||
|
||||
const limit = 5;
|
||||
setLoading(true);
|
||||
EnjoyApp.conversations
|
||||
.findAll({
|
||||
order: [["updatedAt", "DESC"]],
|
||||
limit,
|
||||
offset,
|
||||
})
|
||||
.then((_conversations) => {
|
||||
if (_conversations.length === 0) {
|
||||
setOffset(-1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_conversations.length < limit) {
|
||||
setOffset(-1);
|
||||
} else {
|
||||
setOffset(offset + _conversations.length);
|
||||
}
|
||||
|
||||
if (offset === 0) {
|
||||
setConversations(_conversations);
|
||||
} else {
|
||||
setConversations([...conversations, ..._conversations]);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchConversations();
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return <LoaderSpin />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollArea>
|
||||
{conversations.filter((c) => !excludedIds.includes(c.id)).length ===
|
||||
0 && (
|
||||
<div className="text-center text-sm text-muted-foreground py-4">
|
||||
{t("noConversationsYet")}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{conversations
|
||||
.filter((c) => !excludedIds.includes(c.id))
|
||||
.map((conversation) => {
|
||||
return (
|
||||
<div
|
||||
key={conversation.id}
|
||||
onClick={() => {
|
||||
navigate(`/conversations/${conversation.id}?text=${prompt}`);
|
||||
}}
|
||||
className="bg-background text-primary rounded-full w-full mb-2 py-2 px-4 hover:bg-primary hover:text-white cursor-pointer flex items-center border"
|
||||
style={{
|
||||
borderLeftColor: `#${conversation.id
|
||||
.replaceAll("-", "")
|
||||
.substr(0, 6)}`,
|
||||
borderLeftWidth: 3,
|
||||
}}
|
||||
>
|
||||
<div className="">
|
||||
{conversation.type === "gpt" && (
|
||||
<MessageCircleIcon className="mr-2" />
|
||||
)}
|
||||
|
||||
{conversation.type === "tts" && <SpeechIcon className="mr-2" />}
|
||||
</div>
|
||||
<div className="flex-1 truncated">{conversation.name}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{offset > -1 && (
|
||||
<div className="flex justify-center">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => fetchConversations()}
|
||||
disabled={loading || offset === -1}
|
||||
className="px-4 py-2"
|
||||
>
|
||||
{t("loadMore")}
|
||||
{loading && <LoaderIcon className="w-4 h-4 animate-spin ml-2" />}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea>
|
||||
);
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./conversation-card";
|
||||
export * from "./conversation-form";
|
||||
export * from "./conversation-shortcuts";
|
||||
|
||||
|
||||
@@ -9,10 +9,11 @@ import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
ScrollArea,
|
||||
toast,
|
||||
} from "@renderer/components/ui";
|
||||
import { ConversationForm } from "@renderer/components";
|
||||
import { ConversationCard, ConversationForm } from "@renderer/components";
|
||||
import { useState, useEffect, useContext, useReducer } from "react";
|
||||
import { ChevronLeftIcon, MessageCircleIcon, SpeechIcon } from "lucide-react";
|
||||
import { ChevronLeftIcon, LoaderIcon } from "lucide-react";
|
||||
import { Link, useNavigate, useSearchParams } from "react-router-dom";
|
||||
import {
|
||||
DbProviderContext,
|
||||
@@ -20,8 +21,8 @@ import {
|
||||
AISettingsProviderContext,
|
||||
} from "@renderer/context";
|
||||
import { conversationsReducer } from "@renderer/reducers";
|
||||
import dayjs from "dayjs";
|
||||
import { CONVERSATION_PRESETS } from "@/constants";
|
||||
import { set } from "lodash";
|
||||
|
||||
export default () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
@@ -39,6 +40,8 @@ export default () => {
|
||||
conversationsReducer,
|
||||
[]
|
||||
);
|
||||
const [hasMore, setHasMore] = useState<boolean>(false);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -66,9 +69,39 @@ export default () => {
|
||||
}, [searchParams.get("postId")]);
|
||||
|
||||
const fetchConversations = async () => {
|
||||
const _conversations = await EnjoyApp.conversations.findAll({});
|
||||
const limit = 10;
|
||||
|
||||
dispatchConversations({ type: "set", records: _conversations });
|
||||
setLoading(true);
|
||||
EnjoyApp.conversations
|
||||
.findAll({
|
||||
order: [["updatedAt", "DESC"]],
|
||||
limit,
|
||||
offset: conversations.length,
|
||||
})
|
||||
.then((_conversations) => {
|
||||
if (_conversations.length === 0) {
|
||||
setHasMore(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_conversations.length < limit) {
|
||||
setHasMore(false);
|
||||
} else {
|
||||
setHasMore(true);
|
||||
}
|
||||
|
||||
if (conversations.length === 0) {
|
||||
dispatchConversations({ type: "set", records: _conversations });
|
||||
} else {
|
||||
dispatchConversations({ type: "append", records: _conversations });
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(error.message);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const onConversationsUpdate = (event: CustomEvent) => {
|
||||
@@ -253,31 +286,23 @@ export default () => {
|
||||
|
||||
{conversations.map((conversation) => (
|
||||
<Link key={conversation.id} to={`/conversations/${conversation.id}`}>
|
||||
<div
|
||||
className="bg-background hover:bg-muted hover:text-muted-foreground border rounded-full w-full mb-2 p-4 cursor-pointer flex items-center"
|
||||
style={{
|
||||
borderLeftColor: `#${conversation.id
|
||||
.replaceAll("-", "")
|
||||
.slice(0, 6)}`,
|
||||
borderLeftWidth: 3,
|
||||
}}
|
||||
>
|
||||
<div className="">
|
||||
{conversation.type === "gpt" && (
|
||||
<MessageCircleIcon className="mr-2" />
|
||||
)}
|
||||
|
||||
{conversation.type === "tts" && <SpeechIcon className="mr-2" />}
|
||||
</div>
|
||||
<div className="flex-1 flex items-center justify-between space-x-4">
|
||||
<span className="line-clamp-1">{conversation.name}</span>
|
||||
<span className="min-w-fit">
|
||||
{dayjs(conversation.createdAt).format("HH:mm L")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<ConversationCard conversation={conversation} />
|
||||
</Link>
|
||||
))}
|
||||
|
||||
{hasMore && (
|
||||
<div className="flex justify-center">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => fetchConversations()}
|
||||
disabled={loading || !hasMore}
|
||||
className="px-4 py-2"
|
||||
>
|
||||
{t("loadMore")}
|
||||
{loading && <LoaderIcon className="w-4 h-4 animate-spin ml-2" />}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user