new post type

This commit is contained in:
an-lee
2024-01-12 15:47:33 +08:00
parent e05f2c57eb
commit 3dff4330a1
10 changed files with 211 additions and 102 deletions

View File

@@ -7,7 +7,7 @@
"main": ".vite/build/main.js",
"types": "./src/types.d.ts",
"scripts": {
"dev": "rimraf .vite && WEB_API_URL=http://192.168.31.116:3000 electron-forge start",
"dev": "rimraf .vite && WEB_API_URL=http://localhost:3000 electron-forge start",
"start": "rimraf .vite && electron-forge start",
"package": "rimraf .vite && electron-forge package",
"make": "rimraf .vite && electron-forge make",

View File

@@ -95,7 +95,7 @@ export class Client {
}
createPost(params: {
content?: string;
metadata?: PostType["metadata"];
targetType?: string;
targetId?: string;
}): Promise<PostType> {

View File

@@ -1,2 +1,4 @@
export * from "./posts";
export * from "./post-audio-player";
export * from "./post-card";
export * from "./post-actions";

View File

@@ -0,0 +1,59 @@
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 {
DefaultVideoLayout,
defaultLayoutIcons,
} from "@vidstack/react/player/layouts/default";
import Markdown from "react-markdown";
import { BotIcon, CheckIcon, CopyPlusIcon, PlusCircleIcon } from "lucide-react";
import { useCopyToClipboard } from "@uidotdev/usehooks";
export const PostActions = (props: { post: PostType }) => {
const { post } = props;
const [_, copyToClipboard] = useCopyToClipboard();
const [copied, setCopied] = useState<boolean>(false);
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
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>
)}
</div>
);
};

View File

@@ -0,0 +1,105 @@
import { useContext, useEffect, useState } from "react";
import { AppSettingsProviderContext } from "@renderer/context";
import { PostAudioPlayer, PostActions } from "@renderer/components";
import {
Avatar,
AvatarImage,
AvatarFallback,
Button,
} from "@renderer/components/ui";
import { formatDateTime } from "@renderer/lib/utils";
import { t } from "i18next";
import { MediaPlayer, MediaProvider } from "@vidstack/react";
import {
DefaultVideoLayout,
defaultLayoutIcons,
} from "@vidstack/react/player/layouts/default";
import Markdown from "react-markdown";
export const PostCard = (props: { post: PostType }) => {
const { post } = props;
return (
<div className="rounded p-4 bg-white space-y-2">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<Avatar>
<AvatarImage src={post.user.avatarUrl} />
<AvatarFallback className="text-xl">
{post.user.name[0].toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="flex flex-col justify-between">
<div className="">{post.user.name}</div>
<div className="text-xs text-muted-foreground">
{formatDateTime(post.createdAt)}
</div>
</div>
</div>
</div>
{post.metadata?.type === "prompt" && (
<>
<div className="text-sm text-muted-foreground">
{t("sharedPrompt")}
</div>
<Markdown className="prose prose-slate prose-pre:whitespace-normal select-text">
{"```prompt\n" + post.metadata.content + "\n```"}
</Markdown>
</>
)}
{post.targetType == "Medium" && (
<PostMedium medium={post.target as MediumType} />
)}
<PostActions post={post} />
</div>
);
};
const PostMedium = (props: { medium: MediumType }) => {
const { medium } = props;
if (!medium.sourceUrl) return null;
return (
<div className="space-y-2">
{medium.mediumType == "Video" && (
<>
<div className="text-sm text-muted-foreground">
{t("sharedAudio")}
</div>
<MediaPlayer
poster={medium.coverUrl}
src={{
type: `${medium.mediumType.toLowerCase()}/${
medium.extname.replace(".", "") || "mp4"
}`,
src: medium.sourceUrl,
}}
>
<MediaProvider />
<DefaultVideoLayout icons={defaultLayoutIcons} />
</MediaPlayer>
</>
)}
{medium.mediumType == "Audio" && (
<>
<div className="text-sm text-muted-foreground">
{t("sharedAudio")}
</div>
<PostAudioPlayer src={medium.sourceUrl} />
</>
)}
{medium.coverUrl && medium.mediumType == "Audio" && (
<div className="">
<img src={medium.coverUrl} className="w-full rounded" />
</div>
)}
</div>
);
};
const PostOptions = (props: { post: PostType }) => {};

View File

@@ -1,14 +1,7 @@
import { useContext, useEffect, useState } from "react";
import { AppSettingsProviderContext } from "@renderer/context";
import { PostAudioPlayer } from "@renderer/components";
import { Avatar, AvatarImage, AvatarFallback } from "@renderer/components/ui";
import { PostCard } from "@renderer/components";
import { t } from "i18next";
import { MediaPlayer, MediaProvider } from "@vidstack/react";
import {
DefaultVideoLayout,
defaultLayoutIcons,
} from "@vidstack/react/player/layouts/default";
import Markdown from "react-markdown";
export const Posts = () => {
const { webApi } = useContext(AppSettingsProviderContext);
@@ -43,63 +36,3 @@ export const Posts = () => {
</div>
);
};
const PostCard = (props: { post: PostType }) => {
const { post } = props;
return (
<div className="rounded p-4 bg-white">
<div className="flex items-center mb-4 justify-between">
<div className="flex items-center space-x-2">
<Avatar>
<AvatarImage src={post.user.avatarUrl} />
<AvatarFallback className="text-xl">
{post.user.name[0].toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="">{post.user.name}</div>
</div>
</div>
{post.content && (
<Markdown className="prose select-text mb-4">{post.content}</Markdown>
)}
{post.targetType == "Medium" && <PostMedium medium={post.target} />}
</div>
);
};
const PostMedium = (props: { medium: MediumType }) => {
const { medium } = props;
if (!medium.sourceUrl) return null;
return (
<>
<div className="mb-2">
{medium.mediumType == "Video" && (
<MediaPlayer
poster={medium.coverUrl}
src={{
type: `${medium.mediumType.toLowerCase()}/${
medium.extname.replace(".", "") || "mp4"
}`,
src: medium.sourceUrl,
}}
>
<MediaProvider />
<DefaultVideoLayout icons={defaultLayoutIcons} />
</MediaPlayer>
)}
{medium.mediumType == "Audio" && (
<PostAudioPlayer src={medium.sourceUrl} />
)}
</div>
{medium.coverUrl && medium.mediumType == "Audio" && (
<div className="">
<img src={medium.coverUrl} className="w-full rounded" />
</div>
)}
</>
);
};

View File

@@ -179,7 +179,10 @@ export default () => {
const content = message.content;
webApi
.createPost({
content,
metadata: {
type: "prompt",
content,
},
})
.then(() => {
toast({

25
enjoy/src/types.d.ts vendored
View File

@@ -105,31 +105,6 @@ type MeaningType = {
lookups: LookupType[];
};
type StoryType = {
id: string;
url: string;
title: string;
content: string;
metadata: {
[key: string]: string;
};
vocabulary?: string[];
extracted?: boolean;
starred?: boolean;
createdAt: Date;
updatedAt: Date;
};
type CreateStoryParamsType = {
title: string;
content: string;
url: string;
html: string;
metadata: {
[key: string]: string;
};
};
type PagyResponseType = {
page: number;
next: number | null;

View File

@@ -1,9 +1,17 @@
type PostType = {
id: string;
content?: string;
metadata: {
type: 'text' | 'prompt' | 'llm_configuration';
content:
| string
| {
[key: string]: any;
};
};
user: UserType;
targetType: string;
target?: MediumType;
createdAt: string;
updatedAt: string;
}
targetType?: string;
targetId?: string;
target?: MediumType | StoryType;
createdAt: Date;
updatedAt: Date;
};

24
enjoy/src/types/story.d.ts vendored Normal file
View File

@@ -0,0 +1,24 @@
type StoryType = {
id: string;
url: string;
title: string;
content: string;
metadata: {
[key: string]: string;
};
vocabulary?: string[];
extracted?: boolean;
starred?: boolean;
createdAt: Date;
updatedAt: Date;
};
type CreateStoryParamsType = {
title: string;
content: string;
url: string;
html: string;
metadata: {
[key: string]: string;
};
};