* fix(UI): #250 better chat left side bar

Signed-off-by: James <james@jan.ai>
Co-authored-by: James <james@jan.ai>
This commit is contained in:
NamH 2023-10-15 07:46:15 -07:00 committed by GitHub
parent 4c03946ef4
commit 84dd54a98c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 5922 additions and 6129 deletions

View File

@ -151,6 +151,7 @@ export function init({ register }: { register: RegisterExtensionPoint }) {
register(DataService.GetConversations, getConversations.name, getConversations); register(DataService.GetConversations, getConversations.name, getConversations);
register(DataService.CreateConversation, createConversation.name, createConversation); register(DataService.CreateConversation, createConversation.name, createConversation);
register(DataService.UpdateConversation, updateConversation.name, updateConversation);
register(DataService.UpdateMessage, updateMessage.name, updateMessage); register(DataService.UpdateMessage, updateMessage.name, updateMessage);
register(DataService.DeleteConversation, deleteConversation.name, deleteConversation); register(DataService.DeleteConversation, deleteConversation.name, deleteConversation);
register(DataService.CreateMessage, createMessage.name, createMessage); register(DataService.CreateMessage, createMessage.name, createMessage);
@ -160,13 +161,19 @@ export function init({ register }: { register: RegisterExtensionPoint }) {
function getConversations(): Promise<any> { function getConversations(): Promise<any> {
return store.findMany("conversations", {}, [{ updatedAt: "desc" }]); return store.findMany("conversations", {}, [{ updatedAt: "desc" }]);
} }
function createConversation(conversation: any): Promise<number | undefined> { function createConversation(conversation: any): Promise<number | undefined> {
return store.insertOne("conversations", conversation); return store.insertOne("conversations", conversation);
} }
function updateConversation(conversation: any): Promise<void> {
return store.updateOne("conversations", conversation._id, conversation);
}
function createMessage(message: any): Promise<number | undefined> { function createMessage(message: any): Promise<number | undefined> {
return store.insertOne("messages", message); return store.insertOne("messages", message);
} }
function updateMessage(message: any): Promise<void> { function updateMessage(message: any): Promise<void> {
return store.updateOne("messages", message._id, message); return store.updateOne("messages", message._id, message);
} }

View File

@ -59,13 +59,18 @@ function insertOne(collectionName: string, value: any): Promise<any> {
* *
*/ */
function updateOne(collectionName: string, key: string, value: any): Promise<void> { function updateOne(collectionName: string, key: string, value: any): Promise<void> {
console.debug(`updateOne ${collectionName}: ${key} - ${JSON.stringify(value)}`);
return dbs[collectionName].get(key).then((doc) => { return dbs[collectionName].get(key).then((doc) => {
return dbs[collectionName].put({ return dbs[collectionName].put({
_id: key, _id: key,
_rev: doc._rev, _rev: doc._rev,
force: true,
...value, ...value,
}); },
{ force: true });
}).then((res: any) => {
console.info(`updateOne ${collectionName} result: ${JSON.stringify(res)}`);
}).catch((err: any) => {
console.error(`updateOne ${collectionName} error: ${err}`);
}); });
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "data-plugin", "name": "data-plugin",
"version": "1.0.3", "version": "1.0.4",
"description": "The Data Connector provides easy access to a data API using the PouchDB engine. It offers accessible data management capabilities.", "description": "The Data Connector provides easy access to a data API using the PouchDB engine. It offers accessible data management capabilities.",
"icon": "https://raw.githubusercontent.com/tailwindlabs/heroicons/88e98b0c2b458553fbadccddc2d2f878edc0387b/src/20/solid/circle-stack.svg", "icon": "https://raw.githubusercontent.com/tailwindlabs/heroicons/88e98b0c2b458553fbadccddc2d2f878edc0387b/src/20/solid/circle-stack.svg",
"main": "dist/esm/index.js", "main": "dist/esm/index.js",

109
package-lock.json generated
View File

@ -326,6 +326,21 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/@emotion/is-prop-valid": {
"version": "0.8.8",
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz",
"integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==",
"optional": true,
"dependencies": {
"@emotion/memoize": "0.7.4"
}
},
"node_modules/@emotion/memoize": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz",
"integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==",
"optional": true
},
"node_modules/@eslint-community/eslint-utils": { "node_modules/@eslint-community/eslint-utils": {
"version": "4.4.0", "version": "4.4.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
@ -559,6 +574,10 @@
"integrity": "sha512-SQ7Kzhh9+D+ZW9MA0zkYv3VXhIDNx+LzM6EJ+/65I3QY+enU6Itte7E5XX7EWrqLW2FN4n06GWzBnPoC3th2aQ==", "integrity": "sha512-SQ7Kzhh9+D+ZW9MA0zkYv3VXhIDNx+LzM6EJ+/65I3QY+enU6Itte7E5XX7EWrqLW2FN4n06GWzBnPoC3th2aQ==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/@janhq/plugin-core": {
"resolved": "plugin-core",
"link": true
},
"node_modules/@jridgewell/gen-mapping": { "node_modules/@jridgewell/gen-mapping": {
"version": "0.3.3", "version": "0.3.3",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
@ -5454,6 +5473,29 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/framer-motion": {
"version": "10.16.4",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-10.16.4.tgz",
"integrity": "sha512-p9V9nGomS3m6/CALXqv6nFGMuFOxbWsmaOrdmhyQimMIlLl3LC7h7l86wge/Js/8cRu5ktutS/zlzgR7eBOtFA==",
"dependencies": {
"tslib": "^2.4.0"
},
"optionalDependencies": {
"@emotion/is-prop-valid": "^0.8.2"
},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/fs-extra": { "node_modules/fs-extra": {
"version": "10.1.0", "version": "10.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
@ -7532,9 +7574,7 @@
"node_modules/lodash": { "node_modules/lodash": {
"version": "4.17.21", "version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
"dev": true,
"license": "MIT"
}, },
"node_modules/lodash.castarray": { "node_modules/lodash.castarray": {
"version": "4.4.0", "version": "4.4.0",
@ -7666,6 +7706,25 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/marked": {
"version": "9.1.2",
"resolved": "https://registry.npmjs.org/marked/-/marked-9.1.2.tgz",
"integrity": "sha512-qoKMJqK0w6vkLk8+KnKZAH6neUZSNaQqVZ/h2yZ9S7CbLuFHyS2viB0jnqcWF9UKjwsAbMrQtnQhdmdvOVOw9w==",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 16"
}
},
"node_modules/marked-highlight": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/marked-highlight/-/marked-highlight-2.0.6.tgz",
"integrity": "sha512-xjA/C6xgXAfkkYg+YHnxdjmgFyTDtqqu8KbZiqh+COJ7PuzR15kqa+rPrs6pf/2jExXtG1jyCFUHmv9s0Bi/dQ==",
"peerDependencies": {
"marked": ">=4 <10"
}
},
"node_modules/matcher": { "node_modules/matcher": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
@ -10204,7 +10263,6 @@
"version": "1.29.0", "version": "1.29.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz",
"integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==",
"license": "MIT",
"engines": { "engines": {
"node": ">=6" "node": ">=6"
} }
@ -13819,12 +13877,41 @@
"url": "https://github.com/sponsors/wooorm" "url": "https://github.com/sponsors/wooorm"
} }
}, },
"plugin-core": {
"name": "@janhq/plugin-core",
"version": "0.1.5",
"license": "MIT",
"devDependencies": {
"@types/node": "^12.0.2",
"typescript": "^5.2.2"
}
},
"plugin-core/node_modules/@types/node": {
"version": "12.20.55",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz",
"integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==",
"dev": true
},
"plugin-core/node_modules/typescript": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
"integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"web": { "web": {
"name": "jan-web", "name": "jan-web",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@headlessui/react": "^1.7.15", "@headlessui/react": "^1.7.15",
"@heroicons/react": "^2.0.18", "@heroicons/react": "^2.0.18",
"@janhq/plugin-core": "file:../plugin-core",
"@tailwindcss/typography": "^0.5.9", "@tailwindcss/typography": "^0.5.9",
"@types/react": "18.2.15", "@types/react": "18.2.15",
"@types/react-dom": "18.2.7", "@types/react-dom": "18.2.7",
@ -13835,14 +13922,20 @@
"embla-carousel-react": "^8.0.0-rc11", "embla-carousel-react": "^8.0.0-rc11",
"eslint": "8.45.0", "eslint": "8.45.0",
"eslint-config-next": "13.4.10", "eslint-config-next": "13.4.10",
"framer-motion": "^10.16.4",
"highlight.js": "^11.9.0",
"jotai": "^2.4.0", "jotai": "^2.4.0",
"jotai-optics": "^0.3.1", "jotai-optics": "^0.3.1",
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
"lodash": "^4.17.21",
"marked": "^9.1.2",
"marked-highlight": "^2.0.6",
"next": "13.4.10", "next": "13.4.10",
"next-auth": "^4.23.1", "next-auth": "^4.23.1",
"next-themes": "^0.2.1", "next-themes": "^0.2.1",
"optics-ts": "^2.4.1", "optics-ts": "^2.4.1",
"postcss": "8.4.26", "postcss": "8.4.26",
"prismjs": "^1.29.0",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-hook-form": "^7.45.4", "react-hook-form": "^7.45.4",
@ -13898,6 +13991,14 @@
"postcss": "^8.1.0" "postcss": "^8.1.0"
} }
}, },
"web/node_modules/highlight.js": {
"version": "11.9.0",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.9.0.tgz",
"integrity": "sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==",
"engines": {
"node": ">=12.0.0"
}
},
"web/node_modules/postcss": { "web/node_modules/postcss": {
"version": "8.4.26", "version": "8.4.26",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.26.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.26.tgz",

View File

@ -76,6 +76,11 @@ export enum DataService {
*/ */
CreateConversation = "createConversation", CreateConversation = "createConversation",
/**
* Updates an existing conversation on the server.
*/
UpdateConversation = "updateConversation",
/** /**
* Deletes an existing conversation from the server. * Deletes an existing conversation from the server.
*/ */

View File

@ -1,5 +1,4 @@
import React from "react"; import React from "react";
import JanImage from "../JanImage";
import { useAtomValue, useSetAtom } from "jotai"; import { useAtomValue, useSetAtom } from "jotai";
import Image from "next/image"; import Image from "next/image";
import { Conversation } from "@/_models/Conversation"; import { Conversation } from "@/_models/Conversation";
@ -17,11 +16,13 @@ import {
MainViewState, MainViewState,
} from "@/_helpers/atoms/MainView.atom"; } from "@/_helpers/atoms/MainView.atom";
import useInitModel from "@/_hooks/useInitModel"; import useInitModel from "@/_hooks/useInitModel";
import { displayDate } from "@/_utils/datetime";
type Props = { type Props = {
conversation: Conversation; conversation: Conversation;
avatarUrl?: string; avatarUrl?: string;
name: string; name: string;
summary?: string;
updatedAt?: string; updatedAt?: string;
}; };
@ -29,6 +30,7 @@ const HistoryItem: React.FC<Props> = ({
conversation, conversation,
avatarUrl, avatarUrl,
name, name,
summary,
updatedAt, updatedAt,
}) => { }) => {
const setMainViewState = useSetAtom(setMainViewStateAtom); const setMainViewState = useSetAtom(setMainViewStateAtom);
@ -73,49 +75,39 @@ const HistoryItem: React.FC<Props> = ({
rightImageUrl = "icons/loading.svg"; rightImageUrl = "icons/loading.svg";
} }
const description = conversation?.lastMessage ?? "No new message";
return ( return (
<button <li
className={`flex flex-row mx-1 items-center gap-2.5 rounded-lg p-2 ${backgroundColor} hover:bg-hover-light`} role="button"
className={`flex flex-row ml-3 mr-2 rounded p-3 ${backgroundColor} hover:bg-hover-light`}
onClick={onClick} onClick={onClick}
> >
<Image <div className="w-8 h-8">
width={36} <Image
height={36} width={32}
src={avatarUrl ?? "icons/app_icon.svg"} height={32}
className="w-9 aspect-square rounded-full" src={avatarUrl ?? "icons/app_icon.svg"}
alt="" className="aspect-square rounded-full"
/> alt=""
<div className="flex flex-col justify-between text-sm leading-[20px] w-full"> />
<div className="flex flex-row items-center justify-between"> </div>
<span className="text-gray-900 text-left">{name}</span>
<span className="text-xs leading-[13px] tracking-[-0.4px] text-gray-400"> <div className="flex flex-col ml-2 flex-1">
{updatedAt && new Date(updatedAt).toDateString()} {/* title */}
<div className="flex">
<span className="flex-1 text-gray-900 line-clamp-1">
{summary ?? name}
</span>
<span className="text-xs leading-5 text-gray-500 line-clamp-1">
{updatedAt && displayDate(new Date(updatedAt).getTime())}
</span> </span>
</div> </div>
<div className="flex items-center justify-between gap-1">
<div className="flex-1"> {/* description */}
<span className="text-gray-400 hidden-text text-left"> <span className="mt-1 text-gray-400 line-clamp-2">{description}</span>
{conversation?.message ?? (
<span>
No new message
<br className="h-5 block" />
</span>
)}
</span>
</div>
<>
{rightImageUrl != null ? (
<JanImage
imageUrl={rightImageUrl ?? ""}
className="rounded"
width={24}
height={24}
/>
) : undefined}
</>
</div>
</div> </div>
</button> </li>
); );
}; };

View File

@ -24,8 +24,10 @@ const HistoryList: React.FC = () => {
expanded={expand} expanded={expand}
onClick={() => setExpand(!expand)} onClick={() => setExpand(!expand)}
/> />
<div <ul
className={`flex flex-col gap-1 mt-1 overflow-y-auto scroll ${!expand ? "hidden " : "block"}`} className={`flex flex-col gap-1 mt-1 overflow-y-auto scroll ${
!expand ? "hidden " : "block"
}`}
> >
{conversations.length > 0 ? ( {conversations.length > 0 ? (
conversations conversations
@ -38,6 +40,7 @@ const HistoryList: React.FC = () => {
<HistoryItem <HistoryItem
key={convo._id} key={convo._id}
conversation={convo} conversation={convo}
summary={convo.summary}
avatarUrl={convo.image} avatarUrl={convo.image}
name={convo.name || "Jan"} name={convo.name || "Jan"}
updatedAt={convo.updatedAt ?? ""} updatedAt={convo.updatedAt ?? ""}
@ -46,7 +49,7 @@ const HistoryList: React.FC = () => {
) : ( ) : (
<SidebarEmptyHistory /> <SidebarEmptyHistory />
)} )}
</div> </ul>
</div> </div>
); );
}; };

View File

@ -6,12 +6,10 @@ import { useAtomValue } from "jotai";
import { showingAdvancedPromptAtom } from "@/_helpers/atoms/Modal.atom"; import { showingAdvancedPromptAtom } from "@/_helpers/atoms/Modal.atom";
import SecondaryButton from "../SecondaryButton"; import SecondaryButton from "../SecondaryButton";
import { Fragment } from "react"; import { Fragment } from "react";
import { PlusIcon, FaceSmileIcon } from "@heroicons/react/24/outline"; import { PlusIcon } from "@heroicons/react/24/outline";
import useCreateConversation from "@/_hooks/useCreateConversation"; import useCreateConversation from "@/_hooks/useCreateConversation";
import { activeAssistantModelAtom } from "@/_helpers/atoms/Model.atom"; import { activeAssistantModelAtom } from "@/_helpers/atoms/Model.atom";
import LoadingIndicator from "../LoadingIndicator";
import { currentConvoStateAtom } from "@/_helpers/atoms/Conversation.atom"; import { currentConvoStateAtom } from "@/_helpers/atoms/Conversation.atom";
import SendButton from "../SendButton";
const InputToolbar: React.FC = () => { const InputToolbar: React.FC = () => {
const showingAdvancedPrompt = useAtomValue(showingAdvancedPromptAtom); const showingAdvancedPrompt = useAtomValue(showingAdvancedPromptAtom);

View File

@ -1,4 +1,4 @@
import React from "react"; import React, { Fragment } from "react";
import SidebarFooter from "../SidebarFooter"; import SidebarFooter from "../SidebarFooter";
import SidebarHeader from "../SidebarHeader"; import SidebarHeader from "../SidebarHeader";
import SidebarMenu from "../SidebarMenu"; import SidebarMenu from "../SidebarMenu";
@ -6,13 +6,13 @@ import HistoryList from "../HistoryList";
import NewChatButton from "../NewChatButton"; import NewChatButton from "../NewChatButton";
const LeftContainer: React.FC = () => ( const LeftContainer: React.FC = () => (
<div className="w-[323px] flex-shrink-0 py-3 h-screen border-r border-gray-200 flex flex-col"> <Fragment>
<SidebarHeader /> <SidebarHeader />
<NewChatButton /> <NewChatButton />
<HistoryList /> <HistoryList />
<SidebarMenu /> <SidebarMenu />
<SidebarFooter /> <SidebarFooter />
</div> </Fragment>
); );
export default React.memo(LeftContainer); export default React.memo(LeftContainer);

View File

@ -0,0 +1,45 @@
"use client";
import React from "react";
import LeftContainer from "../LeftContainer";
import RightContainer from "../RightContainer";
import { Variants, motion } from "framer-motion";
import { useAtomValue } from "jotai";
import { leftSideBarExpandStateAtom } from "@/_helpers/atoms/LeftSideBarExpand.atom";
const leftSideBarVariants: Variants = {
show: {
x: 0,
width: 320,
opacity: 1,
transition: { duration: 0.1 },
},
hide: {
x: "-100%",
width: 0,
opacity: 0,
transition: { duration: 0.1 },
},
};
const MainContainer: React.FC = () => {
const leftSideBarExpand = useAtomValue(leftSideBarExpandStateAtom);
return (
<div className="flex">
<motion.div
initial={false}
animate={leftSideBarExpand ? "show" : "hide"}
variants={leftSideBarVariants}
className="w-80 flex-shrink-0 py-3 h-screen border-r border-gray-200 flex flex-col"
>
<LeftContainer />
</motion.div>
<div className="flex flex-col flex-1 h-screen">
<RightContainer />
</div>
</div>
);
};
export default MainContainer;

View File

@ -1,11 +1,12 @@
import { Fragment } from "react";
import MainView from "../MainView"; import MainView from "../MainView";
import MonitorBar from "../MonitorBar"; import MonitorBar from "../MonitorBar";
const RightContainer = () => ( const RightContainer = () => (
<div className="flex flex-col flex-1 h-screen"> <Fragment>
<MainView /> <MainView />
<MonitorBar /> <MonitorBar />
</div> </Fragment>
); );
export default RightContainer; export default RightContainer;

View File

@ -1,7 +1,6 @@
import { currentPromptAtom } from "@/_helpers/JotaiWrapper"; import { currentPromptAtom } from "@/_helpers/JotaiWrapper";
import { currentConvoStateAtom } from "@/_helpers/atoms/Conversation.atom"; import { currentConvoStateAtom } from "@/_helpers/atoms/Conversation.atom";
import useSendChatMessage from "@/_hooks/useSendChatMessage"; import useSendChatMessage from "@/_hooks/useSendChatMessage";
import { ArrowRightIcon } from "@heroicons/react/24/outline";
import { useAtom, useAtomValue } from "jotai"; import { useAtom, useAtomValue } from "jotai";
const SendButton: React.FC = () => { const SendButton: React.FC = () => {

View File

@ -1,10 +1,11 @@
import React from "react"; import React from "react";
import { displayDate } from "@/_utils/datetime"; import { displayDate } from "@/_utils/datetime";
import { TextCode } from "../TextCode";
import { getMessageCode } from "@/_utils/message";
import Image from "next/image"; import Image from "next/image";
import { MessageSenderType } from "@/_models/ChatMessage"; import { MessageSenderType } from "@/_models/ChatMessage";
import LoadingIndicator from "../LoadingIndicator"; import LoadingIndicator from "../LoadingIndicator";
import { Marked } from "marked";
import { markedHighlight } from "marked-highlight";
import hljs from "highlight.js";
type Props = { type Props = {
avatarUrl: string; avatarUrl: string;
@ -14,16 +15,26 @@ type Props = {
text?: string; text?: string;
}; };
const renderMessageCode = (text: string) => { const marked = new Marked(
return getMessageCode(text).map((item, i) => ( markedHighlight({
<div className="flex gap-1 flex-col" key={i}> langPrefix: "hljs",
<p className="leading-[20px] whitespace-break-spaces text-sm font-normal dark:text-[#d1d5db]"> highlight(code, lang) {
{item.text} if (lang === undefined || lang === "") {
</p> return hljs.highlightAuto(code).value;
{item.code.trim().length > 0 && <TextCode text={item.code} />} }
</div> return hljs.highlight(code, { language: lang }).value;
)); },
}; }),
{
renderer: {
code(code, lang, escaped) {
return `<pre class="hljs"><code class="language-${escape(
lang ?? ""
)}">${escaped ? code : escape(code)}</code></pre>`;
},
},
}
);
const SimpleTextMessage: React.FC<Props> = ({ const SimpleTextMessage: React.FC<Props> = ({
senderName, senderName,
@ -35,6 +46,8 @@ const SimpleTextMessage: React.FC<Props> = ({
const backgroundColor = const backgroundColor =
senderType === MessageSenderType.User ? "" : "bg-gray-100"; senderType === MessageSenderType.User ? "" : "bg-gray-100";
const parsedText = marked.parse(text);
return ( return (
<div <div
className={`flex items-start gap-2 px-12 md:px-32 2xl:px-64 ${backgroundColor} py-5`} className={`flex items-start gap-2 px-12 md:px-32 2xl:px-64 ${backgroundColor} py-5`}
@ -46,7 +59,7 @@ const SimpleTextMessage: React.FC<Props> = ({
height={32} height={32}
alt="" alt=""
/> />
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1 w-full">
<div className="flex gap-1 justify-start items-baseline"> <div className="flex gap-1 justify-start items-baseline">
<div className="text-[#1B1B1B] text-sm font-extrabold leading-[15.2px] dark:text-[#d1d5db]"> <div className="text-[#1B1B1B] text-sm font-extrabold leading-[15.2px] dark:text-[#d1d5db]">
{senderName} {senderName}
@ -57,10 +70,11 @@ const SimpleTextMessage: React.FC<Props> = ({
</div> </div>
{text === "" ? ( {text === "" ? (
<LoadingIndicator /> <LoadingIndicator />
) : text.includes("```") ? (
renderMessageCode(text)
) : ( ) : (
<span className="text-sm leading-loose font-normal">{text}</span> <span
className="text-sm leading-loose font-normal"
dangerouslySetInnerHTML={{ __html: parsedText }}
/>
)} )}
</div> </div>
</div> </div>

View File

@ -1,66 +0,0 @@
import React from "react";
import { displayDate } from "@/_utils/datetime";
import { TextCode } from "../TextCode";
import { getMessageCode } from "@/_utils/message";
import Image from "next/image";
import { useAtomValue } from "jotai";
import { currentStreamingMessageAtom } from "@/_helpers/atoms/ChatMessage.atom";
type Props = {
id: string;
avatarUrl?: string;
senderName: string;
createdAt: number;
text?: string;
};
const StreamTextMessage: React.FC<Props> = ({
id,
senderName,
createdAt,
avatarUrl = "",
text = "",
}) => {
const message = useAtomValue(currentStreamingMessageAtom);
return message?.text && message?.text?.length > 0 ? (
<div className="flex items-start gap-2 ml-3">
<Image
className="rounded-full"
src={avatarUrl}
width={32}
height={32}
alt=""
/>
<div className="flex flex-col gap-1 w-full">
<div className="flex gap-1 justify-start items-baseline">
<div className="text-[#1B1B1B] text-sm font-extrabold leading-[15.2px] dark:text-[#d1d5db]">
{senderName}
</div>
<div className="text-xs leading-[13.2px] font-medium text-gray-400">
{displayDate(createdAt)}
</div>
</div>
{message.text.includes("```") ? (
getMessageCode(message.text).map((item, i) => (
<div className="flex gap-1 flex-col" key={i}>
<p className="leading-[20px] whitespace-break-spaces text-sm font-normal dark:text-[#d1d5db]">
{item.text}
</p>
{item.code.trim().length > 0 && <TextCode text={item.code} />}
</div>
))
) : (
<p
className="leading-[20px] whitespace-break-spaces text-sm font-normal dark:text-[#d1d5db]"
dangerouslySetInnerHTML={{ __html: message.text }}
/>
)}
</div>
</div>
) : (
<></>
);
};
export default React.memo(StreamTextMessage);

View File

@ -1,34 +0,0 @@
"use client";
import { Light as SyntaxHighlighter } from "react-syntax-highlighter";
import { atomOneDark } from "react-syntax-highlighter/dist/esm/styles/hljs";
import Image from "next/image";
type Props = {
text: string;
};
export const TextCode: React.FC<Props> = ({ text }) => (
<div className="w-full rounded-lg overflow-hidden bg-[#1F2A37] mr-3">
<div className="text-gray-200 bg-gray-800 flex items-center justify-between px-4 py-2 text-xs capitalize">
<button onClick={() => navigator.clipboard.writeText(text)}>
<Image
src={"icons/unicorn_clipboard-alt.svg"}
width={24}
height={24}
alt=""
/>
</button>
</div>
{/*
// @ts-ignore */}
<SyntaxHighlighter
className="w-full overflow-x-hidden resize-none"
language="jsx"
style={atomOneDark}
customStyle={{ padding: "12px", background: "transparent" }}
wrapLongLines={true}
>
{text}
</SyntaxHighlighter>
</div>
);

View File

@ -8,7 +8,7 @@ const UserToolbar: React.FC = () => {
const currentConvo = useAtomValue(currentConversationAtom); const currentConvo = useAtomValue(currentConversationAtom);
const avatarUrl = currentConvo?.image; const avatarUrl = currentConvo?.image;
const title = currentConvo?.name ?? ""; const title = currentConvo?.summary ?? currentConvo?.name ?? "";
return ( return (
<div className="flex items-center gap-3 p-1"> <div className="flex items-center gap-3 p-1">

View File

@ -103,7 +103,3 @@ export const updateLastMessageAsReadyAtom = atom(
set(chatMessages, newData); set(chatMessages, newData);
} }
); );
export const currentStreamingMessageAtom = atom<ChatMessage | undefined>(
undefined
);

View File

@ -33,7 +33,7 @@ export const currentConvoStateAtom = atom<ConversationState | undefined>(
(get) => { (get) => {
const activeConvoId = get(activeConversationIdAtom); const activeConvoId = get(activeConversationIdAtom);
if (!activeConvoId) { if (!activeConvoId) {
console.log("active convo id is undefined"); console.debug("Active convo id is undefined");
return undefined; return undefined;
} }
@ -80,6 +80,29 @@ export const updateConversationHasMoreAtom = atom(
} }
); );
export const updateConversationAtom = atom(
null,
(get, set, conversation: Conversation) => {
const id = conversation._id;
if (!id) return;
const convo = get(userConversationsAtom).find((c) => c._id === id);
if (!convo) return;
const newConversations: Conversation[] = get(userConversationsAtom).map(
(c) => (c._id === id ? conversation : c)
);
// sort new conversations based on updated at
newConversations.sort((a, b) => {
const aDate = new Date(a.updatedAt ?? 0);
const bDate = new Date(b.updatedAt ?? 0);
return bDate.getTime() - aDate.getTime();
});
set(userConversationsAtom, newConversations);
}
);
/** /**
* Stores all conversations for the current user * Stores all conversations for the current user
*/ */
@ -87,30 +110,3 @@ export const userConversationsAtom = atom<Conversation[]>([]);
export const currentConversationAtom = atom<Conversation | undefined>((get) => export const currentConversationAtom = atom<Conversation | undefined>((get) =>
get(userConversationsAtom).find((c) => c._id === get(getActiveConvoIdAtom)) get(userConversationsAtom).find((c) => c._id === get(getActiveConvoIdAtom))
); );
export const setConvoUpdatedAtAtom = atom(null, (get, set, convoId: string) => {
const convo = get(userConversationsAtom).find((c) => c._id === convoId);
if (!convo) return;
const newConvo: Conversation = {
...convo,
updatedAt: new Date().toISOString(),
};
const newConversations: Conversation[] = get(userConversationsAtom).map((c) =>
c._id === convoId ? newConvo : c
);
set(userConversationsAtom, newConversations);
});
export const setConvoLastImageAtom = atom(
null,
(get, set, convoId: string, lastImageUrl: string) => {
const convo = get(userConversationsAtom).find((c) => c._id === convoId);
if (!convo) return;
const newConvo: Conversation = { ...convo };
const newConversations: Conversation[] = get(userConversationsAtom).map(
(c) => (c._id === convoId ? newConvo : c)
);
set(userConversationsAtom, newConversations);
}
);

View File

@ -0,0 +1,6 @@
import { atom } from "jotai";
/**
* Stores expand state of conversation container. Default is true.
*/
export const leftSideBarExpandStateAtom = atom<boolean>(true);

View File

@ -1,75 +0,0 @@
export default function useGetModelApiInfo() {
const data = [
{
type: "cURL",
stringCode: `import SyntaxHighlighter from 'react-syntax-highlighter';
import { docco } from 'react-syntax-highlighter/dist/esm/styles/hljs';
const Component = () => {
const codeString = '(num) => num + 1';
return (
<SyntaxHighlighter language="javascript" style={docco}>
{codeString}
</SyntaxHighlighter>
);
};`,
},
{
type: "Python",
stringCode: `# This function adds two numbers
def add(x, y):
return x + y
# This function subtracts two numbers
def subtract(x, y):
return x - y
# This function multiplies two numbers
def multiply(x, y):
return x * y
# This function divides two numbers
def divide(x, y):
return x / y
print("Select operation.")
print("1.Add")
print("2.Subtract")
print("3.Multiply")
print("4.Divide")`,
},
{
type: "Node",
stringCode: `var express = require('express');
var app = express();
app.get('/', function (req, res) {
res.send('Hello World');
})
var server = app.listen(8081, function () {
var host = server.address().address
var port = server.address().port
})
`,
},
{
type: "JSON",
stringCode: `{"menu": {
"id": "file",
"value": "File",
"popup": {
"menuitem": [
{"value": "New", "onclick": "CreateNewDoc()"},
{"value": "Open", "onclick": "OpenDoc()"},
{"value": "Close", "onclick": "CloseDoc()"}
]
}
}}`,
},
];
return {
data,
};
}

View File

@ -13,12 +13,17 @@ export default function useInitModel() {
return; return;
} }
const currentTime = Date.now();
console.debug("Init model: ", model._id);
const res = await executeSerial(InferenceService.InitModel, model._id); const res = await executeSerial(InferenceService.InitModel, model._id);
if (res?.error) { if (res?.error) {
console.log("error occured: ", res); console.log("error occured: ", res);
return res; return res;
} else { } else {
console.log(`Init model successfully!`); console.debug(
`Init model successfully!, take ${Date.now() - currentTime}ms`
);
setActiveModel(model); setActiveModel(model);
return {}; return {};
} }

View File

@ -3,6 +3,7 @@ import { useAtom, useAtomValue, useSetAtom } from "jotai";
import { selectAtom } from "jotai/utils"; import { selectAtom } from "jotai/utils";
import { DataService, InferenceService } from "@janhq/plugin-core"; import { DataService, InferenceService } from "@janhq/plugin-core";
import { import {
ChatMessage,
MessageSenderType, MessageSenderType,
RawMessage, RawMessage,
toChatMessage, toChatMessage,
@ -13,17 +14,19 @@ import {
addNewMessageAtom, addNewMessageAtom,
updateMessageAtom, updateMessageAtom,
chatMessages, chatMessages,
currentStreamingMessageAtom, currentChatMessagesAtom,
} from "@/_helpers/atoms/ChatMessage.atom"; } from "@/_helpers/atoms/ChatMessage.atom";
import { import {
currentConversationAtom, currentConversationAtom,
getActiveConvoIdAtom, getActiveConvoIdAtom,
updateConversationAtom,
updateConversationWaitingForResponseAtom, updateConversationWaitingForResponseAtom,
} from "@/_helpers/atoms/Conversation.atom"; } from "@/_helpers/atoms/Conversation.atom";
import { Conversation } from "@/_models/Conversation";
export default function useSendChatMessage() { export default function useSendChatMessage() {
const currentConvo = useAtomValue(currentConversationAtom); const currentConvo = useAtomValue(currentConversationAtom);
const updateStreamMessage = useSetAtom(currentStreamingMessageAtom); const updateConversation = useSetAtom(updateConversationAtom);
const addNewMessage = useSetAtom(addNewMessageAtom); const addNewMessage = useSetAtom(addNewMessageAtom);
const updateMessage = useSetAtom(updateMessageAtom); const updateMessage = useSetAtom(updateMessageAtom);
const activeConversationId = useAtomValue(getActiveConvoIdAtom) ?? ""; const activeConversationId = useAtomValue(getActiveConvoIdAtom) ?? "";
@ -100,7 +103,7 @@ export default function useSendChatMessage() {
}; };
const respId = await executeSerial(DataService.CreateMessage, newResponse); const respId = await executeSerial(DataService.CreateMessage, newResponse);
newResponse._id = respId; newResponse._id = respId;
const responseChatMessage = await toChatMessage(newResponse); const responseChatMessage = toChatMessage(newResponse);
addNewMessage(responseChatMessage); addNewMessage(responseChatMessage);
while (true && reader) { while (true && reader) {
@ -119,10 +122,6 @@ export default function useSendChatMessage() {
if (answer.startsWith("assistant: ")) { if (answer.startsWith("assistant: ")) {
answer = answer.replace("assistant: ", ""); answer = answer.replace("assistant: ", "");
} }
updateStreamMessage({
...responseChatMessage,
text: answer,
});
updateMessage( updateMessage(
responseChatMessage.id, responseChatMessage.id,
responseChatMessage.conversationId, responseChatMessage.conversationId,
@ -144,8 +143,103 @@ export default function useSendChatMessage() {
.replace("T", " ") .replace("T", " ")
.replace(/\.\d+Z$/, ""), .replace(/\.\d+Z$/, ""),
}); });
const updatedConvo: Conversation = {
...currentConvo,
lastMessage: answer.trim(),
updatedAt: new Date().toISOString(),
};
await executeSerial(DataService.UpdateConversation, updatedConvo);
updateConversation(updatedConvo);
updateConvWaiting(conversationId, false); updateConvWaiting(conversationId, false);
newResponse.message = answer.trim();
const messages: RawMessage[] = [newMessage, newResponse];
inferConvoSummary(updatedConvo, messages);
}; };
const inferConvoSummary = async (
convo: Conversation,
lastMessages: RawMessage[]
) => {
if (convo.summary) return;
const newMessage: RawMessage = {
conversationId: currentConvo?._id,
message: "summary this conversation in 5 words",
user: "user",
createdAt: new Date().toISOString(),
};
const messageHistory = lastMessages.map((m) => toChatMessage(m));
const newChatMessage = toChatMessage(newMessage);
const recentMessages = [...messageHistory, newChatMessage]
.slice(-10)
.map((message) => ({
content: message.text,
role: message.messageSenderType,
}));
console.debug(`Sending ${JSON.stringify(recentMessages)}`);
const url = await executeSerial(InferenceService.InferenceUrl);
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "text/event-stream",
"Access-Control-Allow-Origi": "*",
},
body: JSON.stringify({
messages: recentMessages,
stream: true,
model: "gpt-3.5-turbo",
max_tokens: 500,
}),
});
const stream = response.body;
const decoder = new TextDecoder("utf-8");
const reader = stream?.getReader();
let answer = "";
// Cache received response
const newResponse: RawMessage = {
conversationId: currentConvo?._id,
message: answer,
user: "assistant",
createdAt: new Date().toISOString(),
};
while (reader) {
const { done, value } = await reader.read();
if (done) {
console.log("SSE stream closed");
break;
}
const text = decoder.decode(value);
const lines = text.trim().split("\n");
for (const line of lines) {
if (line.startsWith("data: ") && !line.includes("data: [DONE]")) {
const data = JSON.parse(line.replace("data: ", ""));
answer += data.choices[0]?.delta?.content ?? "";
if (answer.startsWith("assistant: ")) {
answer = answer.replace("assistant: ", "");
}
}
}
}
const updatedConvo: Conversation = {
...convo,
summary: answer.trim(),
};
console.debug(`Update convo: ${JSON.stringify(updatedConvo)}`);
await executeSerial(DataService.UpdateConversation, updatedConvo);
updateConversation(updatedConvo);
};
return { return {
sendChatMessage, sendChatMessage,
}; };

View File

@ -1,6 +1,3 @@
import { remark } from "remark";
import html from "remark-html";
export enum MessageType { export enum MessageType {
Text = "Text", Text = "Text",
Image = "Image", Image = "Image",
@ -9,8 +6,8 @@ export enum MessageType {
} }
export enum MessageSenderType { export enum MessageSenderType {
Ai = "Ai", Ai = "assistant",
User = "User", User = "user",
} }
export enum MessageStatus { export enum MessageStatus {
@ -26,7 +23,6 @@ export interface ChatMessage {
senderUid: string; senderUid: string;
senderName: string; senderName: string;
senderAvatarUrl: string; senderAvatarUrl: string;
htmlText?: string | undefined;
text: string | undefined; text: string | undefined;
imageUrls?: string[] | undefined; imageUrls?: string[] | undefined;
createdAt: number; createdAt: number;
@ -42,7 +38,7 @@ export interface RawMessage {
updatedAt?: string; updatedAt?: string;
} }
export const toChatMessage = async (m: RawMessage): Promise<ChatMessage> => { export const toChatMessage = (m: RawMessage): ChatMessage => {
const createdAt = new Date(m.createdAt ?? "").getTime(); const createdAt = new Date(m.createdAt ?? "").getTime();
const imageUrls: string[] = []; const imageUrls: string[] = [];
const imageUrl = undefined; const imageUrl = undefined;
@ -55,8 +51,6 @@ export const toChatMessage = async (m: RawMessage): Promise<ChatMessage> => {
m.user === "user" ? MessageSenderType.User : MessageSenderType.Ai; m.user === "user" ? MessageSenderType.User : MessageSenderType.Ai;
const content = m.message ?? ""; const content = m.message ?? "";
const processedContent = await remark().use(html).process(content);
const contentHtml = processedContent.toString();
return { return {
id: (m._id ?? 0).toString(), id: (m._id ?? 0).toString(),
@ -68,7 +62,6 @@ export const toChatMessage = async (m: RawMessage): Promise<ChatMessage> => {
senderAvatarUrl: senderAvatarUrl:
m.user === "user" ? "icons/avatar.svg" : "icons/app_icon.svg", m.user === "user" ? "icons/avatar.svg" : "icons/app_icon.svg",
text: content, text: content,
htmlText: contentHtml,
imageUrls: imageUrls, imageUrls: imageUrls,
createdAt: createdAt, createdAt: createdAt,
status: MessageStatus.Ready, status: MessageStatus.Ready,

View File

@ -4,6 +4,8 @@ export interface Conversation {
name?: string; name?: string;
image?: string; image?: string;
message?: string; message?: string;
lastMessage?: string;
summary?: string;
createdAt?: string; createdAt?: string;
updatedAt?: string; updatedAt?: string;
} }

View File

@ -25,18 +25,3 @@ export function mergeAndRemoveDuplicates(
return result.reverse(); return result.reverse();
} }
export function getMessageCode(stringCode: string) {
const blocks = stringCode.split("```");
const resultArray = [];
for (let i = 0; i < blocks.length; i += 2) {
const text = blocks[i] ? blocks[i].trim() : "";
const code = blocks[i + 1] ? blocks[i + 1].trim() : "";
if (text || code) {
resultArray.push({ text, code });
}
}
return resultArray;
}

97
web/app/code-block.css Normal file
View File

@ -0,0 +1,97 @@
.hljs-comment,
.hljs-quote {
color: #d4d0ab;
}
/* Red */
.hljs-variable,
.hljs-template-variable,
.hljs-tag,
.hljs-name,
.hljs-selector-id,
.hljs-selector-class,
.hljs-regexp,
.hljs-deletion {
color: #ffa07a;
}
/* Orange */
.hljs-number,
.hljs-built_in,
.hljs-builtin-name,
.hljs-literal,
.hljs-type,
.hljs-params,
.hljs-meta,
.hljs-link {
color: #f5ab35;
}
/* Yellow */
.hljs-attribute {
color: #ffd700;
}
/* Green */
.hljs-string,
.hljs-symbol,
.hljs-bullet,
.hljs-addition {
color: #abe338;
}
/* Blue */
.hljs-title,
.hljs-section {
color: #00e0e0;
}
/* Purple */
.hljs-keyword,
.hljs-selector-tag {
color: #dcc6e0;
}
.hljs {
display: block;
overflow-x: auto;
background: #2b2b2b;
color: #f8f8f2;
padding: 0.5em;
border-radius: 0.4rem;
margin-top: 1rem;
margin-bottom: 1rem;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}
@media screen and (-ms-high-contrast: active) {
.hljs-addition,
.hljs-attribute,
.hljs-built_in,
.hljs-builtin-name,
.hljs-bullet,
.hljs-comment,
.hljs-link,
.hljs-literal,
.hljs-meta,
.hljs-number,
.hljs-params,
.hljs-string,
.hljs-symbol,
.hljs-type,
.hljs-quote {
color: highlight;
}
.hljs-keyword,
.hljs-selector-tag {
font-weight: bold;
}
}

View File

@ -3,6 +3,7 @@
@tailwind utilities; @tailwind utilities;
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"); @import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap");
@import "./code-block.css";
:root { :root {
/* Your default theme */ /* Your default theme */

View File

@ -2,11 +2,9 @@
import { ThemeWrapper } from "./_helpers/ThemeWrapper"; import { ThemeWrapper } from "./_helpers/ThemeWrapper";
import JotaiWrapper from "./_helpers/JotaiWrapper"; import JotaiWrapper from "./_helpers/JotaiWrapper";
import RightContainer from "./_components/RightContainer";
import { ModalWrapper } from "./_helpers/ModalWrapper"; import { ModalWrapper } from "./_helpers/ModalWrapper";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import Image from "next/image"; import Image from "next/image";
import { import {
setup, setup,
plugins, plugins,
@ -16,9 +14,9 @@ import {
isCorePluginInstalled, isCorePluginInstalled,
setupBasePlugins, setupBasePlugins,
} from "./_services/pluginService"; } from "./_services/pluginService";
import LeftContainer from "./_components/LeftContainer";
import EventListenerWrapper from "./_helpers/EventListenerWrapper"; import EventListenerWrapper from "./_helpers/EventListenerWrapper";
import { setupCoreServices } from "./_services/coreService"; import { setupCoreServices } from "./_services/coreService";
import MainContainer from "./_components/MainContainer";
const Page: React.FC = () => { const Page: React.FC = () => {
const [activated, setActivated] = useState(false); const [activated, setActivated] = useState(false);
@ -63,13 +61,9 @@ const Page: React.FC = () => {
<EventListenerWrapper> <EventListenerWrapper>
<ThemeWrapper> <ThemeWrapper>
<ModalWrapper> <ModalWrapper>
{activated && ( {activated ? (
<div className="flex"> <MainContainer />
<LeftContainer /> ) : (
<RightContainer />
</div>
)}
{!activated && (
<div className="bg-white w-screen h-screen items-center justify-center flex"> <div className="bg-white w-screen h-screen items-center justify-center flex">
<Image width={50} height={50} src="icons/app_icon.svg" alt="" /> <Image width={50} height={50} src="icons/app_icon.svg" alt="" />
</div> </div>

View File

@ -17,16 +17,19 @@
"@tailwindcss/typography": "^0.5.9", "@tailwindcss/typography": "^0.5.9",
"@types/react": "18.2.15", "@types/react": "18.2.15",
"@types/react-dom": "18.2.7", "@types/react-dom": "18.2.7",
"@types/react-syntax-highlighter": "^15.5.7",
"autoprefixer": "10.4.14", "autoprefixer": "10.4.14",
"classnames": "^2.3.2", "classnames": "^2.3.2",
"embla-carousel": "^8.0.0-rc11", "embla-carousel": "^8.0.0-rc11",
"embla-carousel-react": "^8.0.0-rc11", "embla-carousel-react": "^8.0.0-rc11",
"eslint": "8.45.0", "eslint": "8.45.0",
"eslint-config-next": "13.4.10", "eslint-config-next": "13.4.10",
"framer-motion": "^10.16.4",
"highlight.js": "^11.9.0",
"jotai": "^2.4.0", "jotai": "^2.4.0",
"jotai-optics": "^0.3.1", "jotai-optics": "^0.3.1",
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
"marked": "^9.1.2",
"marked-highlight": "^2.0.6",
"next": "13.4.10", "next": "13.4.10",
"next-auth": "^4.23.1", "next-auth": "^4.23.1",
"next-themes": "^0.2.1", "next-themes": "^0.2.1",
@ -35,9 +38,6 @@
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-hook-form": "^7.45.4", "react-hook-form": "^7.45.4",
"react-syntax-highlighter": "^15.5.0",
"remark": "^14.0.3",
"remark-html": "^15.0.2",
"tailwindcss": "3.3.3", "tailwindcss": "3.3.3",
"typescript": "5.1.6" "typescript": "5.1.6"
}, },

11231
yarn.lock

File diff suppressed because it is too large Load Diff