add audio/video & send prompt from share zone

This commit is contained in:
an-lee
2024-01-13 01:09:40 +08:00
parent 3dff4330a1
commit 1243076bbb
10 changed files with 312 additions and 60 deletions

View File

@@ -1,59 +1,215 @@
import { useContext, useEffect, useState } from "react";
import { AppSettingsProviderContext } from "@renderer/context";
import { PostAudioPlayer } from "@renderer/components";
import { Button } from "@renderer/components/ui";
import { formatDateTime } from "@renderer/lib/utils";
import { t } from "i18next";
import { MediaPlayer, MediaProvider } from "@vidstack/react";
import { ConversationsShortcut } from "@renderer/components";
import {
DefaultVideoLayout,
defaultLayoutIcons,
} from "@vidstack/react/player/layouts/default";
AlertDialog,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogDescription,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogAction,
AlertDialogCancel,
AlertDialogFooter,
Button,
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
DialogTitle,
ScrollArea,
useToast,
} from "@renderer/components/ui";
import { t } from "i18next";
import Markdown from "react-markdown";
import { BotIcon, CheckIcon, CopyPlusIcon, PlusCircleIcon } from "lucide-react";
import {
BotIcon,
CheckIcon,
CopyPlusIcon,
PlusCircleIcon,
ChevronRightIcon,
} from "lucide-react";
import { useCopyToClipboard } from "@uidotdev/usehooks";
import { Link } from "react-router-dom";
export const PostActions = (props: { post: PostType }) => {
const { post } = props;
const [_, copyToClipboard] = useCopyToClipboard();
const [copied, setCopied] = useState<boolean>(false);
const { EnjoyApp } = useContext(AppSettingsProviderContext);
const { toast } = useToast();
const [asking, setAsking] = useState<boolean>(false);
const [aiReplies, setAiReplies] = useState<MessageType[]>([]);
const handleAddMedium = async () => {
if (post.targetType !== "Medium") return;
const medium = post.target as MediumType;
if (!medium) return;
if (medium.mediumType === "Video") {
try {
const video = await EnjoyApp.videos.findOne({ md5: medium.md5 });
if (video) {
toast({
description: t("videoAlreadyAddedToLibrary"),
});
return;
}
} catch (error) {
console.error(error);
}
EnjoyApp.videos
.create(medium.sourceUrl, {
coverUrl: medium.coverUrl,
md5: medium.md5,
})
.then(() => {
toast({
description: t("videoSuccessfullyAddedToLibrary"),
});
});
} else if (medium.mediumType === "Audio") {
try {
const audio = await EnjoyApp.audios.findOne({ md5: medium.md5 });
if (audio) {
toast({
description: t("audioAlreadyAddedToLibrary"),
});
return;
}
} catch (error) {
console.error(error);
}
EnjoyApp.audios
.create(medium.sourceUrl, {
coverUrl: medium.coverUrl,
md5: medium.md5,
})
.then(() => {
toast({
description: t("audioSuccessfullyAddedToLibrary"),
});
});
}
};
return (
<div className="flex items-center space-x-2 justify-end">
{post.target && post.targetType === "Medium" && (
<Button variant="ghost" size="sm" className="px-1.5 rounded-full">
<PlusCircleIcon
<>
<div className="flex items-center space-x-2 justify-end">
{post.target && post.targetType === "Medium" && (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
data-tooltip-id="global-tooltip"
data-tooltip-content={t("addToLibary")}
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>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t("addRecourse")}</AlertDialogTitle>
<AlertDialogDescription>
{(post.target as MediumType).mediumType === "Video" &&
t("areYouSureToAddThisVideoToYourLibrary")}
{(post.target as MediumType).mediumType === "Audio" &&
t("areYouSureToAddThisAudioToYourLibrary")}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{t("cancel")}</AlertDialogCancel>
<AlertDialogAction onClick={handleAddMedium}>
{t("confirm")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)}
{typeof post.metadata?.content === "string" && (
<Button
data-tooltip-id="global-tooltip"
data-tooltip-content={t("addToLibary")}
className="w-5 h-5 text-muted-foreground hover:text-primary"
/>
</Button>
)}
{typeof post.metadata?.content === "string" && (
<Button variant="ghost" size="sm" className="px-1.5 rounded-full">
{copied ? (
<CheckIcon className="w-5 h-5 text-green-500" />
) : (
<CopyPlusIcon
data-tooltip-id="global-tooltip"
data-tooltip-content={t("copy")}
className="w-5 h-5 text-muted-foreground hover:text-primary"
onClick={() => {
copyToClipboard(post.metadata.content as string);
setCopied(true);
setTimeout(() => {
setCopied(false);
}, 3000);
}}
/>
)}
</Button>
)}
{post.metadata?.type === "prompt" && (
<Button variant="ghost" size="sm" className="px-1.5 rounded-full">
<BotIcon className="w-5 h-5 text-muted-foreground hover:text-primary" />
</Button>
)}
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 string);
setCopied(true);
setTimeout(() => {
setCopied(false);
}, 3000);
}}
/>
)}
</Button>
)}
{post.metadata?.type === "prompt" && (
<Dialog open={asking} onOpenChange={setAsking}>
<DialogTrigger asChild>
<Button
data-tooltip-id="global-tooltip"
data-tooltip-content={t("sendToAIAssistant")}
data-tooltip-place="bottom"
variant="ghost"
size="sm"
className="px-1.5 rounded-full"
>
<BotIcon className="w-5 h-5 text-muted-foreground hover:text-primary" />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{t("sendToAIAssistant")}</DialogTitle>
</DialogHeader>
<ConversationsShortcut
prompt={post.metadata.content as string}
onReply={(replies) => {
setAiReplies([...aiReplies, ...replies]);
setAsking(false);
}}
/>
</DialogContent>
<ScrollArea></ScrollArea>
</Dialog>
)}
</div>
{aiReplies.length > 0 && <AIReplies replies={aiReplies} />}
</>
);
};
const AIReplies = (props: { replies: MessageType[] }) => {
return (
<div>
<div className="space-y-2">
{props.replies.map((reply) => (
<div key={reply.id} className="bg-muted py-2 px-4 rounded">
<div className="mb-2 flex items-center justify-between">
<BotIcon className="w-5 h-5 text-blue-500" />
<Link to={`/conversations/${reply.conversationId}`}>
<ChevronRightIcon className="w-5 h-5 text-muted-foreground" />
</Link>
</div>
<Markdown className="prose select-text">{reply.content}</Markdown>
</div>
))}
</div>
</div>
);
};

View File

@@ -20,7 +20,7 @@ export const PostCard = (props: { post: PostType }) => {
const { post } = props;
return (
<div className="rounded p-4 bg-white space-y-2">
<div className="rounded p-4 bg-white space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<Avatar>
@@ -40,7 +40,7 @@ export const PostCard = (props: { post: PostType }) => {
{post.metadata?.type === "prompt" && (
<>
<div className="text-sm text-muted-foreground">
<div className="text-xs text-muted-foreground">
{t("sharedPrompt")}
</div>
<Markdown className="prose prose-slate prose-pre:whitespace-normal select-text">
@@ -66,7 +66,7 @@ const PostMedium = (props: { medium: MediumType }) => {
<div className="space-y-2">
{medium.mediumType == "Video" && (
<>
<div className="text-sm text-muted-foreground">
<div className="text-xs text-muted-foreground">
{t("sharedAudio")}
</div>
<MediaPlayer
@@ -86,7 +86,7 @@ const PostMedium = (props: { medium: MediumType }) => {
{medium.mediumType == "Audio" && (
<>
<div className="text-sm text-muted-foreground">
<div className="text-xs text-muted-foreground">
{t("sharedAudio")}
</div>
<PostAudioPlayer src={medium.sourceUrl} />