Skip to content

Commit

Permalink
Merge pull request #24 from weseek/feat/cms
Browse files Browse the repository at this point in the history
Feat/cms
  • Loading branch information
atsuki-t authored Oct 13, 2023
2 parents 51ff9e4 + e905836 commit a16adec
Show file tree
Hide file tree
Showing 11 changed files with 215 additions and 20 deletions.
Binary file added apps/app/public/images/weseek-blog-header.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
78 changes: 78 additions & 0 deletions apps/app/src/components/PageEditor/ChatGPTHelper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import fm from 'front-matter';
import yaml from 'js-yaml';

import axios from '~/utils/axios';
import loggerFactory from '~/utils/logger';

const logger = loggerFactory('growi:PageEditor:ChatGPTHelper');

// TODO: このメソッドの代わりに '~/features/cms/server/utils' を元にした CMS と LMS の両方に対応した utils を作成してそれを使用する
const extractMetadata = (markdown: string): Record<string, any> | undefined => {
const extractedData = fm(markdown);

const { frontmatter } = extractedData;
if (frontmatter == null) {
return undefined;
}

return yaml.load(frontmatter);
};

// cms もしくは lms の frontmatter が存在しない場合 unefined を返す
const getCMSOrLMSMetadataAndType = (metadata) => {
if (metadata?.cms != null) {
return { cmsOrLMSMetadata: metadata.cms, generateTextType: '記事' };
}
if (metadata?.lms != null) {
return { cmsOrLMSMetadata: metadata.lms, generateTextType: '教材' };
}
};

export const generateCMSOrLMSText = async(markdownText: string): Promise<string | undefined> => {
const URL = 'https://api.openai.com/v1/chat/completions';
const API_KEY = '';

const metadata = extractMetadata(markdownText);

const cmsOrLMSMetadataAndType = getCMSOrLMSMetadataAndType(metadata);
if (cmsOrLMSMetadataAndType == null) {
return;
}

const { cmsOrLMSMetadata, generateTextType } = cmsOrLMSMetadataAndType;

const title = cmsOrLMSMetadata?.title;
const autogen = cmsOrLMSMetadata?.autogen;

try {
// TODO: frontMatter と、frontMatter を切り離した markdown の両方を変数に持たせ、frontMatter は ChatGPT に渡さずに response にくっつけて返す
const requestText = `
${autogen == null ? '' : `${autogen}という要素を入れた、`}${title == null ? '以下の markdown 部分の内容に合う' : `「${title}」というタイトルの`}
${generateTextType}を日本語で markdown の形式で出力してください。
その際、最初に入っていた frontmatter をインデントや改行なども一切変えず、含まれた状態で出力してください。
さらに、その際に # から始まるセクションタイトルのみが入った markdown があった場合、そのセクションタイトルを雛形として適切な内容を入れてください。
${markdownText}
`;

const response = await axios.post(
URL,
{
model: 'gpt-4',
messages: [
{ role: 'user', content: requestText },
],
},
{
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${API_KEY}`,
},
},
);
return response.data.choices[0].message.content;
}
catch (error) {
logger.error(error);
}
};
19 changes: 19 additions & 0 deletions apps/app/src/components/PageEditor/CodeMirrorEditor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import loggerFactory from '~/utils/logger';
import { UncontrolledCodeMirror } from '../UncontrolledCodeMirror';

import AbstractEditor from './AbstractEditor';
import { generateCMSOrLMSText } from './ChatGPTHelper';
import CommentMentionHelper from './CommentMentionHelper';
import EditorIcon from './EditorIcon';
import EmojiPicker from './EmojiPicker';
Expand Down Expand Up @@ -852,6 +853,15 @@ class CodeMirrorEditor extends AbstractEditor {
this.props.onClickTemplateBtn({ onSubmit });
}

async renderChatGPTText() {
const cm = this.getCodeMirror();
const generatedText = await generateCMSOrLMSText(cm.getValue());

if (generatedText != null) {
cm.setValue(generatedText);
}
}

showLinkEditModal() {
const onSubmit = (linkText) => {
return markdownLinkUtil.replaceFocusedMarkdownLinkWithEditor(this.getCodeMirror(), linkText);
Expand Down Expand Up @@ -1051,6 +1061,15 @@ class CodeMirrorEditor extends AbstractEditor {
>
<EditorIcon icon="Template" />
</Button>,
<Button
key="nav-item-template"
color={null}
bssize="small"
title="ChatGPT"
onClick={() => this.renderChatGPTText()}
>
<EditorIcon icon="ChatGPT" />
</Button>,
];
}

Expand Down
6 changes: 6 additions & 0 deletions apps/app/src/components/PageEditor/EditorIcon.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,12 @@ const EditorIcon = (props) => {
<path fillRule="evenodd" d="M14 4.5V14a2 2 0 0 1-2 2H9v-1h3a1 1 0 0 0 1-1V4.5h-2A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v9H2V2a2 2 0 0 1 2-2h5.5L14 4.5ZM.706 13.189v2.66H0V11.85h.806l1.14 2.596h.026l1.14-2.596h.8v3.999h-.716v-2.66h-.038l-.946 2.159h-.516l-.952-2.16H.706Zm3.919 2.66V11.85h1.459c.406 0 .741.078 1.005.234.263.157.46.383.589.68.13.297.196.655.196 1.075 0 .422-.066.784-.196 1.084-.131.301-.33.53-.595.689-.264.158-.597.237-1 .237H4.626Zm1.353-3.354h-.562v2.707h.562c.186 0 .347-.028.484-.082a.8.8 0 0 0 .334-.252 1.14 1.14 0 0 0 .196-.422c.045-.168.067-.365.067-.592a2.1 2.1 0 0 0-.117-.753.89.89 0 0 0-.354-.454c-.159-.102-.362-.152-.61-.152Z" />
</svg>
);
case 'ChatGPT':
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -6 35 35" width="30" height="30">
<path d="M 11.134766 1.0175781 C 10.87173 1.0049844 10.606766 1.0088281 10.337891 1.0332031 C 8.1135321 1.2338971 6.3362243 2.7940749 5.609375 4.8203125 C 3.8970488 5.1768547 2.4372723 6.3040092 1.671875 7.9570312 C 0.73398779 9.9832932 1.1972842 12.300966 2.5878906 13.943359 C 2.0402798 15.605243 2.2847784 17.435582 3.3320312 18.923828 C 4.6182099 20.749715 6.8585216 21.506646 8.9765625 21.123047 C 10.141577 22.428211 11.848518 23.131209 13.662109 22.966797 C 15.886468 22.766103 17.663776 21.205925 18.390625 19.179688 C 20.102951 18.823145 21.562728 17.695991 22.328125 16.042969 C 23.266012 14.016707 22.802716 11.699034 21.412109 10.056641 C 21.95972 8.394757 21.715222 6.5644177 20.667969 5.0761719 C 19.38179 3.2502847 17.141478 2.4933536 15.023438 2.8769531 C 14.031143 1.7652868 12.645932 1.0899306 11.134766 1.0175781 z M 11.025391 2.5136719 C 11.920973 2.5488153 12.753413 2.8736921 13.429688 3.4199219 C 13.316626 3.4759644 13.19815 3.514457 13.087891 3.578125 L 8.5820312 6.1796875 L 8.5175781 12.238281 L 6.75 11.189453 L 6.75 6.7851562 C 6.75 4.6491563 8.3075938 2.74225 10.433594 2.53125 C 10.632969 2.5115 10.83048 2.5060234 11.025391 2.5136719 z M 16.125 4.2558594 C 17.398584 4.263418 18.639844 4.8251563 19.417969 5.9101562 C 20.070858 6.819587 20.310242 7.9019929 20.146484 8.9472656 C 20.04127 8.8772414 19.948325 8.7942374 19.837891 8.7304688 L 15.332031 6.1289062 L 10.052734 9.1035156 L 10.076172 7.0488281 L 13.890625 4.8476562 C 14.584375 4.4471562 15.36085 4.2513242 16.125 4.2558594 z M 5.2832031 6.4726562 C 5.2752078 6.5985272 5.25 6.7203978 5.25 6.8476562 L 5.25 12.050781 L 10.464844 15.136719 L 8.6738281 16.142578 L 4.859375 13.939453 C 3.009375 12.871453 2.1375781 10.567094 3.0175781 8.6210938 C 3.4795583 7.6006836 4.2963697 6.8535791 5.2832031 6.4726562 z M 15.326172 7.8574219 L 19.140625 10.060547 C 20.990625 11.128547 21.864375 13.432906 20.984375 15.378906 C 20.522287 16.399554 19.703941 17.146507 18.716797 17.527344 C 18.724792 17.401473 18.75 17.279602 18.75 17.152344 L 18.75 11.949219 L 13.535156 8.8632812 L 15.326172 7.8574219 z M 12.025391 9.7109375 L 13.994141 10.878906 L 13.966797 13.167969 L 11.974609 14.287109 L 10.005859 13.121094 L 10.03125 10.832031 L 12.025391 9.7109375 z M 15.482422 11.761719 L 17.25 12.810547 L 17.25 17.214844 C 17.25 19.350844 15.692406 21.25775 13.566406 21.46875 C 12.450934 21.579248 11.393768 21.245187 10.570312 20.580078 C 10.683374 20.524036 10.80185 20.485543 10.912109 20.421875 L 15.417969 17.820312 L 15.482422 11.761719 z M 13.947266 14.896484 L 13.923828 16.951172 L 10.109375 19.152344 C 8.259375 20.220344 5.8270313 19.825844 4.5820312 18.089844 C 3.9291425 17.180413 3.6897576 16.098007 3.8535156 15.052734 C 3.9587303 15.122759 4.0516754 15.205763 4.1621094 15.269531 L 8.6679688 17.871094 L 13.947266 14.896484 z" />
</svg>
);
}


Expand Down
6 changes: 5 additions & 1 deletion apps/app/src/components/PageEditor/Preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { RendererOptions } from '~/interfaces/renderer-options';

import RevisionRenderer from '../Page/RevisionRenderer';

import PreviewCMSHeader from './PreviewCMSHeader';

type Props = {
rendererOptions: RendererOptions,
Expand All @@ -23,14 +24,17 @@ const Preview = React.forwardRef((props: Props, ref: RefObject<HTMLDivElement>):

return (
<div
className={`page-editor-preview-body ${pagePath === '/Sidebar' ? 'preview-sidebar' : ''}`}
className={`page-editor-preview-body ${pagePath === '/Sidebar' ? 'preview-sidebar' : ''} ${pagePath?.startsWith('/_cms') ? 'preview-cms' : ''}`}
ref={ref}
onScroll={(event: SyntheticEvent<HTMLDivElement>) => {
if (props.onScroll != null) {
props.onScroll(event.currentTarget.scrollTop);
}
}}
>
{pagePath?.startsWith('/_cms') && (
<PreviewCMSHeader />
) }
{ markdown != null && (
<RevisionRenderer rendererOptions={rendererOptions} markdown={markdown}></RevisionRenderer>
) }
Expand Down
13 changes: 13 additions & 0 deletions apps/app/src/components/PageEditor/PreviewCMSHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const PreviewCMSHeader = (): JSX.Element => {
return (
<div className="preview-cms-header container py-5">
<p className="h2 text-info">WESEEK Blog</p>
<p className="mt-3 mb-0">WESEEKのエンジニアブログです</p>
</div>
);

};

PreviewCMSHeader.displayName = 'PreviewCMSHeader';

export default PreviewCMSHeader;
20 changes: 20 additions & 0 deletions apps/app/src/styles/_editor.scss
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,26 @@
}
}

/*********************
* CMS styles
*/
.preview-cms {
padding: 0 !important;
background-color: #f9f9f9;
.wiki{
padding: 32px;
margin: 32px !important;
background-color: #fff;
border: solid 1px #ccc;
}
}

.preview-cms-header {
background-image: url('../../public/images/weseek-blog-header.jpg');
background-position: center;
background-size: cover;
}

// .builtin-editor .tab-pane#edit

&.hackmd {
Expand Down
40 changes: 33 additions & 7 deletions apps/mobliz-clone/src/components/SideMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,24 @@ import React, { useState, useEffect } from 'react';
import axios from '~/utils/axios';

const SideMenu: React.FC = () => {
const [data, setData] = useState<any[]>();
const [tag, setTag] = useState<any[]>();
const [newPost, setNewPost] = useState<any[]>();
const [error, setError] = useState<string>();

useEffect(() => {
axios.get(`${process.env.NEXT_PUBLIC_APP_SITE_URL}/_cms/tags`)
.then((response) => {
setData(response.data.data);
setTag(response.data.data);
})
.catch((error) => {
setError(`データの取得に失敗しました。\n${JSON.stringify(error)}`);
});
}, []);

useEffect(() => {
axios.get(`${process.env.NEXT_PUBLIC_APP_SITE_URL}/_cms/list.json`)
.then((response) => {
setNewPost(response.data);
})
.catch((error) => {
setError(`データの取得に失敗しました。\n${JSON.stringify(error)}`);
Expand All @@ -19,24 +30,39 @@ const SideMenu: React.FC = () => {
<>
{error == null ? (
<>
{data == null ? (
{tag == null || newPost == null ? (
<div className="spinner-border" role="status">
<span className="visually-hidden">Loading...</span>
</div>
) : (
<>
<div>
<p>最新記事</p>
<div>最新記事</div>
<ul className="p-2">
{newPost.map((newPostData, index) => {
return (
<li className="container">
<div className="row">
<div className={`col-10 ${newPost.length - 1 === index ? '' : ' border-bottom'}`}>
<a href={`/${newPostData.page._id}`} className="new-post-list text-decoration-none">
<p className="my-2 ml-5">{`${newPostData.title}`}</p>
</a>
</div>
</div>
</li>
);
})}
</ul>
</div>
<div>
<div>タグ</div>
<ul className="p-2">
{data.map((pageData, index) => {
{tag.map((tagData, index) => {
return (
<li className="container">
<div className="row">
<div className={`col-8 ${index === 0 ? ' border-bottom' : ''}`}>
<p className="my-2 ml-5">{`${pageData.name}`}</p>
<div className={`col-10 ${tag.length - 1 === index ? '' : ' border-bottom'}`}>
<p className="my-2 ml-5">{`${tagData.name}`}</p>
</div>
</div>
</li>
Expand Down
18 changes: 13 additions & 5 deletions apps/mobliz-clone/src/pages/[pageId].tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { useState, useEffect } from 'react';

import dateFnsFormat from 'date-fns/format';
import parse from 'html-react-parser';
import { NextPage, GetServerSideProps } from 'next';
import { useRouter } from 'next/router';
Expand All @@ -13,13 +14,13 @@ type Props = {
const DetailPage: NextPage<Props> = (props: Props) => {
const router = useRouter();
const pageId = router.query.pageId ?? props.pageId;
const [pageData, setPageData] = useState<any>();
const [resData, setResData] = useState<any>();
const [error, setError] = useState<string>();

useEffect(() => {
axios.get(`${process.env.NEXT_PUBLIC_APP_SITE_URL}/_cms/${pageId}.json`)
.then((response) => {
setPageData(response.data);
setResData(response.data);
})
.catch((error) => {
setError(`データの取得に失敗しました。\n${JSON.stringify(error)}`);
Expand All @@ -30,14 +31,21 @@ const DetailPage: NextPage<Props> = (props: Props) => {
<div className="border bg-white p-5">
{error == null ? (
<>
{pageData == null ? (
{resData == null ? (
<div className="spinner-border" role="status">
<span className="visually-hidden">Loading...</span>
</div>
) : (
<>
<h2 className="pb-5 fw-bold">{pageData.title}</h2>
{parse(pageData.htmlString)}
<div className="list-inline d-flex mb-4">
<p className="me-4">{dateFnsFormat(new Date(resData.page.createdAt), 'yyyy.MM.dd')}</p>
<p>{dateFnsFormat(new Date(resData.page.updatedAt), 'yyyy.MM.dd')}</p>
</div>
<h2 className="pb-5 fw-bold">{resData.title}</h2>
{parse(resData.htmlString)}
<hr />
<img src={resData.page.creator.imageUrlCached} width="100" height="100" alt="" />
<p><strong>{resData.page.creator.name}</strong></p>
</>
)}
</>
Expand Down
21 changes: 14 additions & 7 deletions apps/mobliz-clone/src/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import React, { useState, useEffect } from 'react';

import dateFnsFormat from 'date-fns/format';
import parse from 'html-react-parser';
import { NextPage } from 'next';

import axios from '~/utils/axios';

const TopPage: NextPage = () => {
const [data, setData] = useState<any[]>();
const [resData, setResData] = useState<any[]>();
const [error, setError] = useState<string>();

useEffect(() => {
axios.get(`${process.env.NEXT_PUBLIC_APP_SITE_URL}/_cms/list.json`)
.then((response) => {
setData(response.data);
setResData(response.data);
})
.catch((error) => {
setError(`データの取得に失敗しました。\n${JSON.stringify(error)}`);
Expand All @@ -23,20 +24,26 @@ const TopPage: NextPage = () => {
<>
{error == null ? (
<>
{data == null ? (
{resData == null ? (
<div className="spinner-border" role="status">
<span className="visually-hidden">Loading...</span>
</div>
) : (
<>
{data.map((pageData, index) => {
{resData.map((data, index) => {
return (
<div className={`border bg-white p-5${index === 0 ? '' : ' mt-5'}`}>
<div className="list-inline d-flex mb-4">
<p className="me-4">{dateFnsFormat(new Date(data.page.createdAt), 'yyyy.MM.dd')}</p>
<p>{dateFnsFormat(new Date(data.page.updatedAt), 'yyyy.MM.dd')}</p>
</div>
<div className="index-preview overflow-hidden">
<h2 className="pb-5 fw-bold">{pageData.title}</h2>
{parse(pageData.htmlString)}
<a href={`/${data.page._id}`} className="post-title text-decoration-none">
<h2 className="post-title mb-5 fw-bold">{data.title}</h2>
</a>
{parse(data.htmlString)}
</div>
<a href={`/${pageData.page._id}`} className="btn btn-outline-primary text-decoration-none rounded-0 mt-4">
<a href={`/${data.page._id}`} className="btn btn-outline-primary text-decoration-none rounded-0 mt-4">
続きを読む
</a>
</div>
Expand Down
14 changes: 14 additions & 0 deletions apps/mobliz-clone/src/styles/globals.scss
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,17 @@ img {
.index-preview {
max-height: 500px;
}

.post-title {
color: #383838;
&:hover {
color: #f24e4f;
}
}

.new-post-list {
color: #383838;
&:hover {
color: #2581c4;
}
}

0 comments on commit a16adec

Please sign in to comment.