Feat: share GPT (#502)

* may share GPT

* add ai assistant from shared gpt
This commit is contained in:
an-lee
2024-04-08 16:42:41 +08:00
committed by GitHub
parent b795e1e3eb
commit e7df154f84
8 changed files with 426 additions and 260 deletions

View File

@@ -454,6 +454,10 @@
"shareStory": "Share story",
"sharedStory": "Shared a story",
"areYouSureToShareThisStoryToCommunity": "Are you sure to share this story to community?",
"shareGpt": "Share GPT",
"sharedGpt": "Shared a GPT",
"areYouSureToShareThisGptToCommunity": "Are you sure to share this GPT to community?",
"saveAiAssistant": "Save this AI assistant",
"addToLibary": "Add to library",
"areYouSureToAddThisVideoToYourLibrary": "Are you sure to add this video to library?",
"areYouSureToAddThisAudioToYourLibrary": "Are you sure to add this audio to library?",

View File

@@ -453,6 +453,10 @@
"shareStory": "分享文章",
"sharedStory": "分享了一篇文章",
"areYouSureToShareThisStoryToCommunity": "您确定要分享此文章到社区吗?",
"shareGpt": "分享智能助手",
"sharedGpt": "分享了一个智能助手",
"areYouSureToShareThisGptToCommunity": "您确定要将这个智能助手分享到社区吗?",
"saveAiAssistant": "保存智能助手",
"addToLibary": "添加到资源库",
"areYouSureToAddThisVideoToYourLibrary": "您确定要添加此视频到资料库吗?",
"areYouSureToAddThisAudioToYourLibrary": "您确定要添加此音频到资料库吗?",

View File

@@ -28,13 +28,14 @@ import {
SelectContent,
SelectItem,
Textarea,
toast,
} from "@renderer/components/ui";
import { useState, useEffect, useContext } from "react";
import {
AppSettingsProviderContext,
AISettingsProviderContext,
} from "@renderer/context";
import { LoaderIcon } from "lucide-react";
import { LoaderIcon, Share2Icon } from "lucide-react";
import { useNavigate } from "react-router-dom";
const conversationFormSchema = z.object({
@@ -125,20 +126,20 @@ export const ConversationForm = (props: {
// @ts-ignore
values: conversation?.id
? {
name: conversation.name,
engine: conversation.engine,
configuration: {
type: conversation.configuration.type || "gpt",
...conversation.configuration,
},
}
: {
name: defaultConfig.name,
engine: defaultConfig.engine,
configuration: {
...defaultConfig.configuration,
},
name: conversation.name,
engine: conversation.engine,
configuration: {
type: conversation.configuration.type || "gpt",
...conversation.configuration,
},
}
: {
name: defaultConfig.name,
engine: defaultConfig.engine,
configuration: {
...defaultConfig.configuration,
},
},
});
const onSubmit = async (data: z.infer<typeof conversationFormSchema>) => {
@@ -203,8 +204,11 @@ export const ConversationForm = (props: {
className="h-full flex flex-col pt-6"
data-testid="conversation-form"
>
<div className="mb-4 px-6 text-lg font-bold">
{conversation.id ? t("editConversation") : t("startConversation")}
<div className="mb-4 px-6 flex items-center space-x-4">
<div className="text-lg font-bold">
{conversation.id ? t("editConversation") : t("startConversation")}
</div>
<GPTShareButton conversation={conversation} />
</div>
<ScrollArea className="flex-1 px-4">
<div className="space-y-4 px-2 mb-6">
@@ -342,160 +346,160 @@ export const ConversationForm = (props: {
{LLM_PROVIDERS[form.watch("engine")]?.configurable.includes(
"temperature"
) && (
<FormField
control={form.control}
name="configuration.temperature"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("models.conversation.temperature")}
</FormLabel>
<Input
type="number"
min="0"
max="1.0"
step="0.1"
value={field.value}
onChange={(event) => {
field.onChange(
event.target.value
? parseFloat(event.target.value)
: 0.0
);
}}
/>
<FormDescription>
{t("models.conversation.temperatureDescription")}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name="configuration.temperature"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("models.conversation.temperature")}
</FormLabel>
<Input
type="number"
min="0"
max="1.0"
step="0.1"
value={field.value}
onChange={(event) => {
field.onChange(
event.target.value
? parseFloat(event.target.value)
: 0.0
);
}}
/>
<FormDescription>
{t("models.conversation.temperatureDescription")}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
{LLM_PROVIDERS[form.watch("engine")]?.configurable.includes(
"maxTokens"
) && (
<FormField
control={form.control}
name="configuration.maxTokens"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("models.conversation.maxTokens")}
</FormLabel>
<Input
type="number"
min="0"
value={field.value}
onChange={(event) => {
if (!event.target.value) return;
field.onChange(parseInt(event.target.value));
}}
/>
<FormDescription>
{t("models.conversation.maxTokensDescription")}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name="configuration.maxTokens"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("models.conversation.maxTokens")}
</FormLabel>
<Input
type="number"
min="0"
value={field.value}
onChange={(event) => {
if (!event.target.value) return;
field.onChange(parseInt(event.target.value));
}}
/>
<FormDescription>
{t("models.conversation.maxTokensDescription")}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
{LLM_PROVIDERS[form.watch("engine")]?.configurable.includes(
"presencePenalty"
) && (
<FormField
control={form.control}
name="configuration.presencePenalty"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("models.conversation.presencePenalty")}
</FormLabel>
<Input
type="number"
min="-2"
step="0.1"
max="2"
value={field.value}
onChange={(event) => {
if (!event.target.value) return;
field.onChange(parseInt(event.target.value));
}}
/>
<FormDescription>
{t("models.conversation.presencePenaltyDescription")}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name="configuration.presencePenalty"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("models.conversation.presencePenalty")}
</FormLabel>
<Input
type="number"
min="-2"
step="0.1"
max="2"
value={field.value}
onChange={(event) => {
if (!event.target.value) return;
field.onChange(parseInt(event.target.value));
}}
/>
<FormDescription>
{t("models.conversation.presencePenaltyDescription")}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
{LLM_PROVIDERS[form.watch("engine")]?.configurable.includes(
"frequencyPenalty"
) && (
<FormField
control={form.control}
name="configuration.frequencyPenalty"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("models.conversation.frequencyPenalty")}
</FormLabel>
<Input
type="number"
min="-2"
step="0.1"
max="2"
value={field.value}
onChange={(event) => {
if (!event.target.value) return;
field.onChange(parseInt(event.target.value));
}}
/>
<FormDescription>
{t("models.conversation.frequencyPenaltyDescription")}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name="configuration.frequencyPenalty"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("models.conversation.frequencyPenalty")}
</FormLabel>
<Input
type="number"
min="-2"
step="0.1"
max="2"
value={field.value}
onChange={(event) => {
if (!event.target.value) return;
field.onChange(parseInt(event.target.value));
}}
/>
<FormDescription>
{t("models.conversation.frequencyPenaltyDescription")}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
{LLM_PROVIDERS[form.watch("engine")]?.configurable.includes(
"numberOfChoices"
) && (
<FormField
control={form.control}
name="configuration.numberOfChoices"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("models.conversation.numberOfChoices")}
</FormLabel>
<Input
type="number"
min="1"
step="1.0"
value={field.value}
onChange={(event) => {
field.onChange(
event.target.value
? parseInt(event.target.value)
: 1.0
);
}}
/>
<FormDescription>
{t("models.conversation.numberOfChoicesDescription")}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name="configuration.numberOfChoices"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("models.conversation.numberOfChoices")}
</FormLabel>
<Input
type="number"
min="1"
step="1.0"
value={field.value}
onChange={(event) => {
field.onChange(
event.target.value
? parseInt(event.target.value)
: 1.0
);
}}
/>
<FormDescription>
{t("models.conversation.numberOfChoicesDescription")}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
<FormField
control={form.control}
@@ -530,25 +534,25 @@ export const ConversationForm = (props: {
{LLM_PROVIDERS[form.watch("engine")]?.configurable.includes(
"baseUrl"
) && (
<FormField
control={form.control}
name="configuration.baseUrl"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("models.conversation.baseUrl")}
</FormLabel>
<Input
{...field}
placeholder={t(
"models.conversation.baseUrlDescription"
)}
/>
<FormMessage />
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name="configuration.baseUrl"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("models.conversation.baseUrl")}
</FormLabel>
<Input
{...field}
placeholder={t(
"models.conversation.baseUrlDescription"
)}
/>
<FormMessage />
</FormItem>
)}
/>
)}
</>
)}
@@ -584,95 +588,95 @@ export const ConversationForm = (props: {
{TTS_PROVIDERS[
form.watch("configuration.tts.engine")
]?.configurable.includes("model") && (
<FormField
control={form.control}
name="configuration.tts.model"
render={({ field }) => (
<FormItem>
<FormLabel>{t("models.conversation.ttsModel")}</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t("selectTtsModel")} />
</SelectTrigger>
</FormControl>
<SelectContent>
{(
TTS_PROVIDERS[form.watch("configuration.tts.engine")]
?.models || []
).map((model: string) => (
<SelectItem key={model} value={model}>
{model}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name="configuration.tts.model"
render={({ field }) => (
<FormItem>
<FormLabel>{t("models.conversation.ttsModel")}</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t("selectTtsModel")} />
</SelectTrigger>
</FormControl>
<SelectContent>
{(
TTS_PROVIDERS[form.watch("configuration.tts.engine")]
?.models || []
).map((model: string) => (
<SelectItem key={model} value={model}>
{model}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
)}
{TTS_PROVIDERS[
form.watch("configuration.tts.engine")
]?.configurable.includes("voice") && (
<FormField
control={form.control}
name="configuration.tts.voice"
render={({ field }) => (
<FormItem>
<FormLabel>{t("models.conversation.ttsVoice")}</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t("selectTtsVoice")} />
</SelectTrigger>
</FormControl>
<SelectContent>
{(
TTS_PROVIDERS[form.watch("configuration.tts.engine")]
?.voices || []
).map((voice: string) => (
<SelectItem key={voice} value={voice}>
<span className="capitalize">{voice}</span>
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name="configuration.tts.voice"
render={({ field }) => (
<FormItem>
<FormLabel>{t("models.conversation.ttsVoice")}</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t("selectTtsVoice")} />
</SelectTrigger>
</FormControl>
<SelectContent>
{(
TTS_PROVIDERS[form.watch("configuration.tts.engine")]
?.voices || []
).map((voice: string) => (
<SelectItem key={voice} value={voice}>
<span className="capitalize">{voice}</span>
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
)}
{TTS_PROVIDERS[
form.watch("configuration.tts.engine")
]?.configurable.includes("baseUrl") && (
<FormField
control={form.control}
name="configuration.tts.baseUrl"
render={({ field }) => (
<FormItem>
<FormLabel>{t("models.conversation.ttsBaseUrl")}</FormLabel>
<Input
{...field}
placeholder={t(
"models.conversation.ttsBaseUrlDescription"
)}
/>
<FormMessage />
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name="configuration.tts.baseUrl"
render={({ field }) => (
<FormItem>
<FormLabel>{t("models.conversation.ttsBaseUrl")}</FormLabel>
<Input
{...field}
placeholder={t(
"models.conversation.ttsBaseUrlDescription"
)}
/>
<FormMessage />
</FormItem>
)}
/>
)}
</div>
</ScrollArea>
@@ -838,3 +842,80 @@ export const TTS_PROVIDERS: { [key: string]: any } = {
configurable: ["model", "voice", "baseUrl"],
},
};
const GPTShareButton = (props: {
conversation: Partial<ConversationType>;
}) => {
const { conversation } = props;
const { webApi } = useContext(AppSettingsProviderContext);
const navigate = useNavigate();
const handleShare = () => {
const { configuration } = conversation;
delete configuration.baseUrl
delete configuration?.tts?.baseUrl
if (!configuration.roleDefinition) {
toast.error('shareFailed');
return;
}
webApi
.createPost({
metadata: {
type: "gpt",
content: {
name: conversation.name,
engine: conversation.engine,
configuration,
},
},
})
.then(() => {
toast.success(t("sharedSuccessfully"), {
description: t("sharedGpt"),
action: {
label: t("view"),
onClick: () => {
navigate("/community");
},
},
actionButtonStyle: {
backgroundColor: "var(--primary)",
},
});
})
.catch((err) => {
toast.error(t("shareFailed"), { description: err.message });
});
}
if (!conversation.id) return null;
if (conversation.type !== "gpt") return null;
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="link" size="icon" className="rounded-full p-0 w-6 h-6">
<Share2Icon className="w-4 h-4 text-muted-foreground" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t("shareGpt")}</AlertDialogTitle>
<AlertDialogDescription>
{t("areYouSureToShareThisGptToCommunity")}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{t("cancel")}</AlertDialogCancel>
<AlertDialogAction asChild>
<Button variant="default" onClick={handleShare}>
{t("share")}
</Button>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}

View File

@@ -143,6 +143,7 @@ export const PostActions = (props: { post: PostType }) => {
)}
</Button>
)}
{post.metadata?.type === "prompt" && (
<ConversationShortcuts
open={asking}
@@ -166,6 +167,47 @@ export const PostActions = (props: { post: PostType }) => {
}
/>
)}
{post.metadata?.type === "gpt" && (
<>
<Button
data-tooltip-id="global-tooltip"
data-tooltip-content={t("copy")}
data-tooltip-place="bottom"
variant="ghost"
size="sm"
className="px-1.5 rounded-full"
>
{copied ? (
<CheckIcon className="w-5 h-5 text-green-500" />
) : (
<CopyPlusIcon
className="w-5 h-5 text-muted-foreground hover:text-primary"
onClick={() => {
copyToClipboard((post.metadata.content as { [key: string]: any }).configuration.roleDefinition as string);
setCopied(true);
setTimeout(() => {
setCopied(false);
}, 3000);
}}
/>
)}
</Button>
<Link to={`/conversations?postId=${post.id}`}>
<Button
data-tooltip-id="global-tooltip"
data-tooltip-content={t("saveAiAssistant")}
data-tooltip-place="bottom"
variant="ghost"
size="sm"
className="px-1.5 rounded-full"
>
<PlusCircleIcon className="w-5 h-5 text-muted-foreground hover:text-primary" />
</Button>
</Link>
</>
)}
</div>
{aiReplies.length > 0 && <AIReplies replies={aiReplies} />}

View File

@@ -12,6 +12,7 @@ import { formatDateTime } from "@renderer/lib/utils";
import { t } from "i18next";
import Markdown from "react-markdown";
import { Link } from "react-router-dom";
import { BotIcon } from "lucide-react";
export const PostCard = (props: {
post: PostType;
@@ -50,12 +51,30 @@ export const PostCard = (props: {
<div className="text-xs text-muted-foreground">
{t("sharedPrompt")}
</div>
<Markdown className="prose prose-slate prose-pre:whitespace-normal select-text">
<Markdown className="prose prose-slate prose-pre:whitespace-pre-line select-text">
{"```prompt\n" + post.metadata.content + "\n```"}
</Markdown>
</>
)}
{post.metadata?.type === "gpt" && (
<>
<div className="text-xs text-muted-foreground">
{t("sharedGpt")}
</div>
<div className="text-sm">
{t('models.conversation.roleDefinition')}:
</div>
<div className="prose prose-stone prose-pre:whitespace-pre-line select-text">
<blockquote className="not-italic whitespace-pre-line">
<Markdown>
{(post.metadata.content as { [key: string]: any }).configuration?.roleDefinition}
</Markdown>
</blockquote>
</div>
</>
)}
{post.targetType == "Medium" && (
<PostMedium medium={post.target as MediumType} />
)}

View File

@@ -213,7 +213,7 @@ export default () => {
inputRef.current.focus();
return () => {
inputRef.current?.removeEventListener("keypress", () => {});
inputRef.current?.removeEventListener("keypress", () => { });
autosize.destroy(inputRef.current);
};
}, [id, inputRef.current]);
@@ -319,7 +319,7 @@ export default () => {
onChange={(e) => setContent(e.target.value)}
placeholder={t("pressEnterToSend")}
data-testid="conversation-page-input"
className="text-base px-4 py-0 shadow-none border-none focus-visible:outline-0 focus-visible:ring-0 border-none bg-muted focus:bg-background min-h-[1rem] max-h-[70vh] scrollbar-thin scrollbar-thumb-sky-500 !overflow-x-hidden"
className="text-base px-4 py-0 shadow-none focus-visible:outline-0 focus-visible:ring-0 border-none bg-muted focus:bg-background min-h-[1rem] max-h-[70vh] scrollbar-thin scrollbar-thumb-sky-500 !overflow-x-hidden"
/>
<div className="h-12 py-1">
<Button

View File

@@ -13,7 +13,7 @@ import {
import { ConversationForm } from "@renderer/components";
import { useState, useEffect, useContext, useReducer } from "react";
import { ChevronLeftIcon, MessageCircleIcon, SpeechIcon } from "lucide-react";
import { Link, useNavigate } from "react-router-dom";
import { Link, useNavigate, useSearchParams } from "react-router-dom";
import {
DbProviderContext,
AppSettingsProviderContext,
@@ -24,10 +24,11 @@ import dayjs from "dayjs";
import { CONVERSATION_PRESETS } from "@/constants";
export default () => {
const [searchParams] = useSearchParams();
const [creating, setCreating] = useState<boolean>(false);
const [preset, setPreset] = useState<any>({});
const { addDblistener, removeDbListener } = useContext(DbProviderContext);
const { EnjoyApp } = useContext(AppSettingsProviderContext);
const { EnjoyApp, webApi } = useContext(AppSettingsProviderContext);
const { currentEngine } = useContext(AISettingsProviderContext);
const [conversations, dispatchConversations] = useReducer(
conversationsReducer,
@@ -44,6 +45,21 @@ export default () => {
};
}, []);
useEffect(() => {
const postId = searchParams.get('postId');
if (!postId) return;
webApi.post(postId).then((post) => {
const preset: any = post.metadata.content;
if (!preset?.configuration?.roleDefinition) {
return;
}
setPreset(preset);
setCreating(true);
})
}, [searchParams.get('postId')])
const fetchConversations = async () => {
const _conversations = await EnjoyApp.conversations.findAll({});

View File

@@ -1,7 +1,7 @@
type PostType = {
id: string;
metadata: {
type: 'text' | 'prompt' | 'llm_configuration';
type: "text" | "prompt" | "gpt";
content:
| string
| {