add audio/video & send prompt from share zone
This commit is contained in:
@@ -333,9 +333,20 @@
|
||||
"sharedSuccessfully": "Shared successfully",
|
||||
"shareFailed": "Share failed",
|
||||
"shareAudio": "Share audio",
|
||||
"sharedAudio": "Shared an audio resource",
|
||||
"areYouSureToShareThisAudioToCommunity": "Are you sure to share this audio to community?",
|
||||
"shareVideo": "Share video",
|
||||
"sharedVideo": "Shared a video resource",
|
||||
"areYouSureToShareThisVideoToCommunity": "Are you sure to share this video to community?",
|
||||
"sharePrompt": "Share prompt",
|
||||
"areYouSureToShareThisPromptToCommunity": "Are you sure to share this prompt to community?"
|
||||
"sharedPrompt": "Shared a prompt",
|
||||
"areYouSureToShareThisPromptToCommunity": "Are you sure to share this prompt to community?",
|
||||
"addToLibary": "Add to library",
|
||||
"areYouSureToAddThisVideoToYourLibrary": "Are you sure to add this video to library?",
|
||||
"areYouSureToAddThisAudioToYourLibrary": "Are you sure to add this audio to library?",
|
||||
"audioAlreadyAddedToLibrary": "Audio already added to library",
|
||||
"videoAlreadyAddedToLibrary": "Video already added to library",
|
||||
"audioSuccessfullyAddedToLibrary": "Audio successfully added to library",
|
||||
"videoSuccessfullyAddedToLibrary": "Video successfully added to library",
|
||||
"sendToAIAssistant": "Send to AI assistant"
|
||||
}
|
||||
|
||||
@@ -333,9 +333,20 @@
|
||||
"sharedSuccessfully": "分享成功",
|
||||
"sharedFailed": "分享失败",
|
||||
"shareAudio": "分享音频",
|
||||
"sharedAudio": "分享了一个音频材料",
|
||||
"areYouSureToShareThisAudioToCommunity": "您确定要分享此音频到社区吗?",
|
||||
"shareVideo": "分享视频",
|
||||
"sharedVideo": "分享了一个视频材料",
|
||||
"areYouSureToShareThisVideoToCommunity": "您确定要分享此视频到社区吗?",
|
||||
"sharePrompt": "分享提示语",
|
||||
"areYouSureToShareThisPromptToCommunity": "您确定要分享此提示语到社区吗?"
|
||||
"sharedPrompt": "分享了一条提示语",
|
||||
"areYouSureToShareThisPromptToCommunity": "您确定要分享此提示语到社区吗?",
|
||||
"addToLibary": "添加到资源库",
|
||||
"areYouSureToAddThisVideoToYourLibrary": "您确定要添加此视频到资料库吗?",
|
||||
"areYouSureToAddThisAudioToYourLibrary": "您确定要添加此音频到资料库吗?",
|
||||
"audioAlreadyAddedToLibrary": "资料库已经存在此音频",
|
||||
"videoAlreadyAddedToLibrary": "资料库已经存在此视频",
|
||||
"audioSuccessfullyAddedToLibrary": "音频成功添加到资料库",
|
||||
"videoSuccessfullyAddedToLibrary": "视频成功添加到资料库",
|
||||
"sendToAIAssistant": "发送到智能助手"
|
||||
}
|
||||
|
||||
@@ -94,6 +94,7 @@ class VideosHandler {
|
||||
params: {
|
||||
name?: string;
|
||||
coverUrl?: string;
|
||||
md5?: string;
|
||||
} = {}
|
||||
) {
|
||||
let file = uri;
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
AllowNull,
|
||||
} from "sequelize-typescript";
|
||||
import { Message, Speech } from "@main/db/models";
|
||||
import { ChatMessageHistory , BufferMemory } from "langchain/memory";
|
||||
import { ChatMessageHistory, BufferMemory } from "langchain/memory";
|
||||
import { ConversationChain } from "langchain/chains";
|
||||
import { ChatOpenAI } from "langchain/chat_models/openai";
|
||||
import { ChatOllama } from "langchain/chat_models/ollama";
|
||||
@@ -294,9 +294,9 @@ export class Conversation extends Model<Conversation> {
|
||||
}
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
const replies = await Promise.all(
|
||||
response.map(async (generation) => {
|
||||
await Message.create(
|
||||
return await Message.create(
|
||||
{
|
||||
conversationId: this.id,
|
||||
role: "assistant",
|
||||
@@ -330,5 +330,7 @@ export class Conversation extends Model<Conversation> {
|
||||
}
|
||||
|
||||
await transaction.commit();
|
||||
|
||||
return replies.map((reply) => reply.toJSON());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { AppSettingsProviderContext } from "@renderer/context";
|
||||
import { ScrollArea } from "@renderer/components/ui";
|
||||
import { LoaderSpin } from "@renderer/components";
|
||||
import { MessageCircleIcon } from "lucide-react";
|
||||
|
||||
export const ConversationsShortcut = (props: {
|
||||
prompt: string;
|
||||
onReply?: (reply: MessageType[]) => void;
|
||||
}) => {
|
||||
const { EnjoyApp } = useContext(AppSettingsProviderContext);
|
||||
const { prompt, onReply } = props;
|
||||
const [conversations, setConversations] = useState<ConversationType[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
|
||||
const ask = (conversation: ConversationType) => {
|
||||
setLoading(true);
|
||||
EnjoyApp.conversations
|
||||
.ask(conversation.id, {
|
||||
content: prompt,
|
||||
})
|
||||
.then((replies) => {
|
||||
console.log(replies);
|
||||
onReply(replies);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
EnjoyApp.conversations.findAll({ limit: 10 }).then((conversations) => {
|
||||
setConversations(conversations);
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return <LoaderSpin />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollArea>
|
||||
{conversations.map((conversation) => {
|
||||
return (
|
||||
<div
|
||||
key={conversation.id}
|
||||
onClick={() => ask(conversation)}
|
||||
className="bg-white 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="">
|
||||
<MessageCircleIcon className="mr-2" />
|
||||
</div>
|
||||
<div className="flex-1 truncated">{conversation.name}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</ScrollArea>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './conversation-form';
|
||||
export * from "./conversation-form";
|
||||
export * from "./conversations-shortcut";
|
||||
|
||||
export * from './speech-form';
|
||||
export * from "./speech-form";
|
||||
export * from "./speech-player";
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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} />
|
||||
|
||||
12
enjoy/src/types/enjoy-app.d.ts
vendored
12
enjoy/src/types/enjoy-app.d.ts
vendored
@@ -102,8 +102,8 @@ type EnjoyAppType = {
|
||||
videos: {
|
||||
findAll: (params: object) => Promise<VideoType[]>;
|
||||
findOne: (params: object) => Promise<VideoType>;
|
||||
create: (uri: string, params?: object) => Promise<VideoType>;
|
||||
update: (id: string, params: object) => Promise<VideoType | undefined>;
|
||||
create: (uri: string, params?: any) => Promise<VideoType>;
|
||||
update: (id: string, params: any) => Promise<VideoType | undefined>;
|
||||
destroy: (id: string) => Promise<undefined>;
|
||||
transcribe: (id: string) => Promise<void>;
|
||||
upload: (id: string) => Promise<void>;
|
||||
@@ -143,9 +143,9 @@ type EnjoyAppType = {
|
||||
) => Promise<SegementRecordingStatsType>;
|
||||
};
|
||||
conversations: {
|
||||
findAll: (params: object) => Promise<ConversationType[]>;
|
||||
findOne: (params: object) => Promise<ConversationType>;
|
||||
create: (params: object) => Promise<ConversationType>;
|
||||
findAll: (params: any) => Promise<ConversationType[]>;
|
||||
findOne: (params: any) => Promise<ConversationType>;
|
||||
create: (params: any) => Promise<ConversationType>;
|
||||
update: (id: string, params: object) => Promise<ConversationType>;
|
||||
destroy: (id: string) => Promise<void>;
|
||||
ask: (
|
||||
@@ -159,7 +159,7 @@ type EnjoyAppType = {
|
||||
arrayBuffer: ArrayBuffer;
|
||||
};
|
||||
}
|
||||
) => Promise<MessageType>;
|
||||
) => Promise<MessageType[]>;
|
||||
};
|
||||
messages: {
|
||||
findAll: (params: object) => Promise<MessageType[]>;
|
||||
|
||||
1
enjoy/src/types/medium.d.ts
vendored
1
enjoy/src/types/medium.d.ts
vendored
@@ -1,5 +1,6 @@
|
||||
type MediumType = {
|
||||
id: string;
|
||||
md5: string;
|
||||
mediumType: string;
|
||||
coverUrl?: string;
|
||||
sourceUrl?: string;
|
||||
|
||||
Reference in New Issue
Block a user