feat: merge with polonto

This commit is contained in:
Nevo David 2024-07-31 12:37:31 +07:00
commit 1b235652ac
16 changed files with 15717 additions and 38 deletions

View File

@ -1,5 +1,5 @@
import {
Controller, Get, Param, Post, Query, Req, Res
Body, Controller, Get, Param, Post, Query, Req, Res, UploadedFile, UseInterceptors, UsePipes
} from '@nestjs/common';
import { Request, Response } from 'express';
import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request';
@ -7,11 +7,38 @@ import { Organization } from '@prisma/client';
import { MediaService } from '@gitroom/nestjs-libraries/database/prisma/media/media.service';
import { ApiTags } from '@nestjs/swagger';
import handleR2Upload from '@gitroom/nestjs-libraries/upload/r2.uploader';
import { FileInterceptor } from '@nestjs/platform-express';
import { CustomFileValidationPipe } from '@gitroom/nestjs-libraries/upload/custom.upload.validation';
@ApiTags('Media')
@Controller('/media')
export class MediaController {
constructor(private _mediaService: MediaService) {}
@Post('/generate-image')
async generateImage(
@GetOrgFromRequest() org: Organization,
@Req() req: Request,
@Body('prompt') prompt: string
) {
return {output: 'data:image/png;base64,' + await this._mediaService.generateImage(prompt)};
}
@Post('/upload-simple')
@UseInterceptors(FileInterceptor('file'))
@UsePipes(new CustomFileValidationPipe())
async uploadSimple(
@GetOrgFromRequest() org: Organization,
@UploadedFile('file')
file: Express.Multer.File
) {
const filePath =
file.path.indexOf('http') === 0
? file.path
: file.path.replace(process.env.UPLOAD_DIRECTORY, '');
return this._mediaService.saveFile(org.id, file.originalname, filePath);
}
@Post('/:endpoint')
// @UseInterceptors(FileInterceptor('file'))
// @UsePipes(new CustomFileValidationPipe())

View File

@ -1,6 +1,7 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import "./polonto.css";
body,
html {
@ -288,10 +289,22 @@ html {
color: white;
}
.editor .polonto * {
color: black !important;
}
.bp5-portal {
z-index: 9999 !important;
}
:empty + .existing-empty {
display: none;
}
.mantine-Paper-root {
outline: none !important;
}
:root {
--copilot-kit-primary-color: #612ad5 !important;
--copilot-kit-background-color: #0b0f1c !important;

File diff suppressed because it is too large Load Diff

View File

@ -434,7 +434,7 @@ export const AddEditModal: FC<{
// .getCommands()
// .filter((f) => f.name === 'image'),
// newImage,
postSelector(dateState),
// postSelector(dateState),
]}
value={p.content}
preview="edit"

View File

@ -80,6 +80,9 @@ export const LinkedinCompany: FC<{
const [company, setCompany] = useState<any>(null);
const getCompany = async () => {
if (!company) {
return ;
}
const {options} = await (
await fetch('/integrations/function', {
method: 'POST',
@ -141,7 +144,7 @@ export const LinkedinCompany: FC<{
};
export const linkedinCompany = (identifier: string, id: string): ICommand[] => {
if (identifier !== 'linkedin') {
if (identifier !== 'linkedin' && identifier !== 'linkedin-page') {
return [];
}

View File

@ -0,0 +1,142 @@
import {
createContext,
FC,
useContext,
useEffect,
useMemo,
useState,
} from 'react';
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
import { createStore } from 'polotno/model/store';
import Workspace from 'polotno/canvas/workspace';
import { PolotnoContainer, SidePanelWrap, WorkspaceWrap } from 'polotno';
import { SidePanel, DEFAULT_SECTIONS } from 'polotno/side-panel';
import Toolbar from 'polotno/toolbar/toolbar';
import ZoomButtons from 'polotno/toolbar/zoom-buttons';
import { Button } from '@gitroom/react/form/button';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import { PictureGeneratorSection } from '@gitroom/frontend/components/launches/polonto/polonto.picture.generation';
import { useUser } from '@gitroom/frontend/components/layout/user.context';
const store = createStore({
key: 'Aqml_02mqf6YTKC0jYZ8',
showCredit: false,
});
// @ts-ignore
const CloseContext = createContext({ close: {} as any, setMedia: {} as any });
const ActionControls = ({ store }: any) => {
const close = useContext(CloseContext);
const [load, setLoad] = useState(false);
const fetch = useFetch();
return (
<div>
<Button
loading={load}
className="outline-none"
innerClassName="invert outline-none"
onClick={async () => {
setLoad(true);
const blob = await store.toBlob();
const formData = new FormData();
formData.append('file', blob, 'media.png');
const data = await (
await fetch('/media/upload-simple', {
method: 'POST',
body: formData,
})
).json();
close.setMedia({ id: data.id, path: data.path });
close.close();
}}
>
Use this media
</Button>
</div>
);
};
const Polonto: FC<{
setMedia: (params: { id: string; path: string }) => void;
type?: 'image' | 'video';
closeModal: () => void;
width?: number;
height?: number;
}> = (props) => {
const { setMedia, type, closeModal } = props;
const user = useUser();
console.log(user);
const features = useMemo(() => {
return [
...DEFAULT_SECTIONS,
...(user?.tier?.image_generator ? [PictureGeneratorSection] : []),
] as any[];
}, [user?.tier?.image_generator]);
useEffect(() => {
store.addPage({
width: props.width || 540,
height: props.height || 675,
});
return () => {
store.clear();
};
}, []);
return (
<div className="fixed left-0 top-0 bg-black/80 z-[300] w-full min-h-full p-[60px] animate-fade">
<div className="w-full h-full bg-[#0B101B] border-tableBorder border-2 rounded-xl pb-[20px] px-[20px] relative">
<div className="flex">
<div className="flex-1">
<TopTitle title="Design Media" />
</div>
<button
onClick={closeModal}
className="outline-none absolute right-[20px] top-[20px] mantine-UnstyledButton-root mantine-ActionIcon-root bg-black hover:bg-tableBorder cursor-pointer mantine-Modal-close mantine-1dcetaa"
type="button"
>
<svg
viewBox="0 0 15 15"
fill="none"
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
>
<path
d="M11.7816 4.03157C12.0062 3.80702 12.0062 3.44295 11.7816 3.2184C11.5571 2.99385 11.193 2.99385 10.9685 3.2184L7.50005 6.68682L4.03164 3.2184C3.80708 2.99385 3.44301 2.99385 3.21846 3.2184C2.99391 3.44295 2.99391 3.80702 3.21846 4.03157L6.68688 7.49999L3.21846 10.9684C2.99391 11.193 2.99391 11.557 3.21846 11.7816C3.44301 12.0061 3.80708 12.0061 4.03164 11.7816L7.50005 8.31316L10.9685 11.7816C11.193 12.0061 11.5571 12.0061 11.7816 11.7816C12.0062 11.557 12.0062 11.193 11.7816 10.9684L8.31322 7.49999L11.7816 4.03157Z"
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
></path>
</svg>
</button>
</div>
<div className="bg-white text-black relative z-[400] polonto">
<CloseContext.Provider
value={{ close: () => closeModal(), setMedia }}
>
<PolotnoContainer style={{ width: '100%', height: '1000px' }}>
<SidePanelWrap>
<SidePanel store={store} sections={features} />
</SidePanelWrap>
<WorkspaceWrap>
<Toolbar
store={store}
components={{
ActionControls,
}}
/>
<Workspace store={store} />
<ZoomButtons store={store} />
</WorkspaceWrap>
</PolotnoContainer>
</CloseContext.Provider>
</div>
</div>
</div>
);
};
export default Polonto;

View File

@ -0,0 +1,130 @@
import React from 'react';
import { observer } from 'mobx-react-lite';
import { InputGroup, Button } from '@blueprintjs/core';
import { Clean } from '@blueprintjs/icons';
import { SectionTab } from 'polotno/side-panel';
import { getKey } from 'polotno/utils/validate-key';
import { getImageSize } from 'polotno/utils/image';
import { ImagesGrid } from 'polotno/side-panel/images-grid';
import { getAPI } from 'polotno/utils/api';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
const GenerateTab = observer(({ store }: any) => {
const inputRef = React.useRef<any>(null);
const [image, setImage] = React.useState(null);
const [loading, setLoading] = React.useState(false);
const fetch = useFetch();
const handleGenerate = async () => {
setLoading(true);
setImage(null);
const req = await fetch(`/media/generate-image`, {
method: 'POST',
body: JSON.stringify({
prompt: inputRef.current.value,
}),
});
setLoading(false);
if (!req.ok) {
alert('Something went wrong, please try again later...');
return;
}
const data = await req.json();
setImage(data.output);
};
return (
<>
<div style={{ height: '40px', paddingTop: '5px' }}>
Generate image with AI
</div>
<InputGroup
placeholder="Type your image generation prompt here..."
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleGenerate();
}
}}
style={{
marginBottom: '20px',
}}
inputRef={inputRef}
/>
<Button
onClick={handleGenerate}
intent="primary"
loading={loading}
style={{ marginBottom: '40px' }}
>
Generate
</Button>
{image && (
<ImagesGrid
shadowEnabled={false}
images={image ? [image] : []}
getPreview={(item) => item}
isLoading={loading}
onSelect={async (item, pos, element) => {
const src = item;
if (element && element.type === 'svg' && element.contentEditable) {
element.set({ maskSrc: src });
return;
}
if (
element &&
element.type === 'image' &&
element.contentEditable
) {
element.set({ src: src });
return;
}
const { width, height } = await getImageSize(src);
const x = (pos?.x || store.width / 2) - width / 2;
const y = (pos?.y || store.height / 2) - height / 2;
store.activePage?.addElement({
type: 'image',
src: src,
width,
height,
x,
y,
});
}}
rowsNumber={1}
/>
)}
</>
);
});
const PictureGeneratorPanel = observer(({ store }: any) => {
return (
<div
style={{
height: '100%',
display: 'flex',
flexDirection: 'column',
}}
>
<GenerateTab store={store} />
</div>
);
});
// define the new custom section
export const PictureGeneratorSection = {
name: 'picture-generator-ai',
Tab: (props: any) => (
<SectionTab name="AI Img" {...props}>
<Clean />
</SectionTab>
),
// we need observer to update component automatically on any store changes
Panel: PictureGeneratorPanel,
};

View File

@ -49,6 +49,7 @@ export const MediumTags: FC<{
<div>
<div className={`${interClass} text-[14px] mb-[6px]`}>{label}</div>
<ReactTags
placeholderText="Add a tag"
suggestions={suggestionsArray}
selected={tagValue}
onAdd={onAddition}

View File

@ -38,6 +38,8 @@ const YoutubeSettings: FC = () => {
<div className="mt-[20px]">
<MediaComponent
type="image"
width={1280}
height={720}
label="Thumbnail"
description="Thumbnail picture (optional)"
{...register('thumbnail')}

View File

@ -13,8 +13,12 @@ import clsx from 'clsx';
import { VideoFrame } from '@gitroom/react/helpers/video.frame';
import { useToaster } from '@gitroom/react/toaster/toaster';
import { LoadingComponent } from '@gitroom/frontend/components/layout/loading';
import dynamic from 'next/dynamic';
import { MultipartFileUploader } from '@gitroom/frontend/components/media/new.uploader';
import dynamic from 'next/dynamic';
import { useUser } from '@gitroom/frontend/components/layout/user.context';
const Polonto = dynamic(
() => import('@gitroom/frontend/components/launches/polonto')
);
const showModalEmitter = new EventEmitter();
export const ShowMediaBoxModal: FC = () => {
@ -210,6 +214,7 @@ export const MultiMediaComponent: FC<{
}) => void;
}> = (props) => {
const { name, label, error, description, onChange, value } = props;
const user = useUser();
useEffect(() => {
if (value) {
setCurrentMedia(value);
@ -217,6 +222,8 @@ export const MultiMediaComponent: FC<{
}, []);
const [modal, setShowModal] = useState(false);
const [mediaModal, setMediaModal] = useState(false);
const [currentMedia, setCurrentMedia] = useState(value);
const mediaDirectory = useMediaDirectory();
@ -233,6 +240,10 @@ export const MultiMediaComponent: FC<{
setShowModal(!modal);
}, [modal]);
const closeDesignModal = useCallback(() => {
setMediaModal(false);
}, [modal]);
const clearMedia = useCallback(
(topIndex: number) => () => {
const newMedia = currentMedia?.filter((f, index) => index !== topIndex);
@ -242,10 +253,17 @@ export const MultiMediaComponent: FC<{
[currentMedia]
);
const designMedia = useCallback(() => {
setMediaModal(true);
}, []);
return (
<>
<div className="flex flex-col gap-[8px] bg-[#131B2C] rounded-bl-[8px]">
{modal && <MediaBox setMedia={changeMedia} closeModal={showModal} />}
{mediaModal && !!user?.tier?.ai && (
<Polonto setMedia={changeMedia} closeModal={closeDesignModal} />
)}
<div className="flex gap-[10px]">
<div className="flex">
<Button
@ -268,6 +286,27 @@ export const MultiMediaComponent: FC<{
</div>
<div className="text-[12px] font-[500]">Insert Media</div>
</Button>
<Button
onClick={designMedia}
className="ml-[10px] rounded-[4px] mb-[10px] gap-[8px] justify-center items-center w-[127px] flex border border-dashed border-[#506490] !bg-[#832ad5]"
>
<div>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
>
<path
d="M19.5 3H7.5C7.10218 3 6.72064 3.15804 6.43934 3.43934C6.15804 3.72064 6 4.10218 6 4.5V6H4.5C4.10218 6 3.72064 6.15804 3.43934 6.43934C3.15804 6.72064 3 7.10218 3 7.5V19.5C3 19.8978 3.15804 20.2794 3.43934 20.5607C3.72064 20.842 4.10218 21 4.5 21H16.5C16.8978 21 17.2794 20.842 17.5607 20.5607C17.842 20.2794 18 19.8978 18 19.5V18H19.5C19.8978 18 20.2794 17.842 20.5607 17.5607C20.842 17.2794 21 16.8978 21 16.5V4.5C21 4.10218 20.842 3.72064 20.5607 3.43934C20.2794 3.15804 19.8978 3 19.5 3ZM7.5 4.5H19.5V11.0044L17.9344 9.43875C17.6531 9.15766 17.2717 8.99976 16.8741 8.99976C16.4764 8.99976 16.095 9.15766 15.8137 9.43875L8.75344 16.5H7.5V4.5ZM16.5 19.5H4.5V7.5H6V16.5C6 16.8978 6.15804 17.2794 6.43934 17.5607C6.72064 17.842 7.10218 18 7.5 18H16.5V19.5ZM19.5 16.5H10.875L16.875 10.5L19.5 13.125V16.5ZM11.25 10.5C11.695 10.5 12.13 10.368 12.5 10.1208C12.87 9.87357 13.1584 9.52217 13.3287 9.11104C13.499 8.6999 13.5436 8.2475 13.4568 7.81105C13.37 7.37459 13.1557 6.97368 12.841 6.65901C12.5263 6.34434 12.1254 6.13005 11.689 6.04323C11.2525 5.95642 10.8001 6.00097 10.389 6.17127C9.97783 6.34157 9.62643 6.62996 9.37919 6.99997C9.13196 7.36998 9 7.80499 9 8.25C9 8.84674 9.23705 9.41903 9.65901 9.84099C10.081 10.2629 10.6533 10.5 11.25 10.5ZM11.25 7.5C11.3983 7.5 11.5433 7.54399 11.6667 7.6264C11.79 7.70881 11.8861 7.82594 11.9429 7.96299C11.9997 8.10003 12.0145 8.25083 11.9856 8.39632C11.9566 8.5418 11.8852 8.67544 11.7803 8.78033C11.6754 8.88522 11.5418 8.95665 11.3963 8.98559C11.2508 9.01453 11.1 8.99968 10.963 8.94291C10.8259 8.88614 10.7088 8.79001 10.6264 8.66668C10.544 8.54334 10.5 8.39834 10.5 8.25C10.5 8.05109 10.579 7.86032 10.7197 7.71967C10.8603 7.57902 11.0511 7.5 11.25 7.5Z"
fill="white"
/>
</svg>
</div>
<div className="text-[12px] font-[500]">Design Media</div>
</Button>
</div>
{!!currentMedia &&
@ -311,9 +350,12 @@ export const MediaComponent: FC<{
target: { name: string; value?: { id: string; path: string } };
}) => void;
type?: 'image' | 'video';
width?: number;
height?: number;
}> = (props) => {
const { name, type, label, description, onChange, value } = props;
const { name, type, label, description, onChange, value, width, height } = props;
const { getValues } = useSettings();
const user = useUser();
useEffect(() => {
const settings = getValues()[props.name];
if (settings) {
@ -321,9 +363,18 @@ export const MediaComponent: FC<{
}
}, []);
const [modal, setShowModal] = useState(false);
const [mediaModal, setMediaModal] = useState(false);
const [currentMedia, setCurrentMedia] = useState(value);
const mediaDirectory = useMediaDirectory();
const closeDesignModal = useCallback(() => {
setMediaModal(false);
}, [modal]);
const showDesignModal = useCallback(() => {
setMediaModal(true);
}, [modal]);
const changeMedia = useCallback((m: { path: string; id: string }) => {
setCurrentMedia(m);
onChange({ target: { name, value: m } });
@ -343,6 +394,14 @@ export const MediaComponent: FC<{
{modal && (
<MediaBox setMedia={changeMedia} closeModal={showModal} type={type} />
)}
{mediaModal && !!user?.tier?.ai && (
<Polonto
width={width}
height={height}
setMedia={changeMedia}
closeModal={closeDesignModal}
/>
)}
<div className="text-[14px]">{label}</div>
<div className="text-[12px]">{description}</div>
{!!currentMedia && (
@ -354,8 +413,11 @@ export const MediaComponent: FC<{
/>
</div>
)}
<div className="flex">
<div className="flex gap-[5px]">
<Button onClick={showModal}>Select</Button>
<Button onClick={showDesignModal} className="!bg-[#832AD5]">
Editor
</Button>
<Button secondary={true} onClick={clearMedia}>
Clear
</Button>

View File

@ -1,12 +1,18 @@
import {Injectable} from "@nestjs/common";
import {MediaRepository} from "@gitroom/nestjs-libraries/database/prisma/media/media.repository";
import { OpenaiService } from '@gitroom/nestjs-libraries/openai/openai.service';
@Injectable()
export class MediaService {
constructor(
private _mediaRepository: MediaRepository
private _mediaRepository: MediaRepository,
private _openAi: OpenaiService
){}
generateImage(prompt: string) {
return this._openAi.generateImage(prompt);
}
saveFile(org: string, fileName: string, filePath: string) {
return this._mediaRepository.saveFile(org, fileName, filePath);
}

View File

@ -9,6 +9,8 @@ export interface PricingInnerInterface {
featured_by_gitroom: boolean;
ai: boolean;
import_from_channels: boolean;
image_generator?: boolean;
image_generation_count: number;
}
export interface PricingInterface {
[key: string]: PricingInnerInterface;
@ -19,12 +21,14 @@ export const pricing: PricingInterface = {
month_price: 0,
year_price: 0,
channel: 1,
image_generation_count: 0,
posts_per_month: 30,
team_members: false,
community_features: false,
featured_by_gitroom: false,
ai: false,
import_from_channels: false,
image_generator: false,
},
STANDARD: {
current: 'STANDARD',
@ -32,11 +36,13 @@ export const pricing: PricingInterface = {
year_price: 288,
channel: 5,
posts_per_month: 400,
image_generation_count: 20,
team_members: false,
ai: true,
community_features: false,
featured_by_gitroom: false,
import_from_channels: true,
image_generator: false,
},
PRO: {
current: 'PRO',
@ -44,10 +50,12 @@ export const pricing: PricingInterface = {
year_price: 384,
channel: 8,
posts_per_month: 1000000,
image_generation_count: 100,
community_features: true,
team_members: true,
featured_by_gitroom: true,
ai: true,
import_from_channels: true,
image_generator: true,
},
};

View File

@ -8,6 +8,14 @@ const openai = new OpenAI({
@Injectable()
export class OpenaiService {
async generateImage(prompt: string) {
return (await openai.images.generate({
prompt,
response_format: 'b64_json',
model: 'dall-e-3',
})).data[0].b64_json;
}
async generatePosts(content: string) {
const posts = (
await Promise.all([

View File

@ -15,8 +15,8 @@ export const Button: FC<
DetailedHTMLProps<
ButtonHTMLAttributes<HTMLButtonElement>,
HTMLButtonElement
> & { secondary?: boolean; loading?: boolean }
> = ({ children, loading, ...props }) => {
> & { secondary?: boolean; loading?: boolean; innerClassName?: string }
> = ({ children, loading, innerClassName, ...props }) => {
const ref = useRef<HTMLButtonElement | null>(null);
const [height, setHeight] = useState<number | null>(null);
@ -49,6 +49,7 @@ export const Button: FC<
)}
<div
className={clsx(
innerClassName,
'flex-1 items-center justify-center flex',
loading && 'invisible'
)}

779
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -90,6 +90,7 @@
"next": "^14.2.4",
"next-plausible": "^3.12.0",
"openai": "^4.47.1",
"polotno": "^2.10.5",
"prisma-paginate": "^5.2.1",
"react": "18.2.0",
"react-colorful": "^5.6.1",