Fix/250 (#349)
* 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:
parent
4c03946ef4
commit
84dd54a98c
@ -151,6 +151,7 @@ export function init({ register }: { register: RegisterExtensionPoint }) {
|
||||
|
||||
register(DataService.GetConversations, getConversations.name, getConversations);
|
||||
register(DataService.CreateConversation, createConversation.name, createConversation);
|
||||
register(DataService.UpdateConversation, updateConversation.name, updateConversation);
|
||||
register(DataService.UpdateMessage, updateMessage.name, updateMessage);
|
||||
register(DataService.DeleteConversation, deleteConversation.name, deleteConversation);
|
||||
register(DataService.CreateMessage, createMessage.name, createMessage);
|
||||
@ -160,13 +161,19 @@ export function init({ register }: { register: RegisterExtensionPoint }) {
|
||||
function getConversations(): Promise<any> {
|
||||
return store.findMany("conversations", {}, [{ updatedAt: "desc" }]);
|
||||
}
|
||||
|
||||
function createConversation(conversation: any): Promise<number | undefined> {
|
||||
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> {
|
||||
return store.insertOne("messages", message);
|
||||
}
|
||||
|
||||
function updateMessage(message: any): Promise<void> {
|
||||
return store.updateOne("messages", message._id, message);
|
||||
}
|
||||
|
||||
@ -59,13 +59,18 @@ function insertOne(collectionName: string, value: any): Promise<any> {
|
||||
*
|
||||
*/
|
||||
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].put({
|
||||
_id: key,
|
||||
_rev: doc._rev,
|
||||
force: true,
|
||||
...value,
|
||||
});
|
||||
},
|
||||
{ force: true });
|
||||
}).then((res: any) => {
|
||||
console.info(`updateOne ${collectionName} result: ${JSON.stringify(res)}`);
|
||||
}).catch((err: any) => {
|
||||
console.error(`updateOne ${collectionName} error: ${err}`);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"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.",
|
||||
"icon": "https://raw.githubusercontent.com/tailwindlabs/heroicons/88e98b0c2b458553fbadccddc2d2f878edc0387b/src/20/solid/circle-stack.svg",
|
||||
"main": "dist/esm/index.js",
|
||||
|
||||
109
package-lock.json
generated
109
package-lock.json
generated
@ -326,6 +326,21 @@
|
||||
"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": {
|
||||
"version": "4.4.0",
|
||||
"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==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@janhq/plugin-core": {
|
||||
"resolved": "plugin-core",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
|
||||
@ -5454,6 +5473,29 @@
|
||||
"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": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
|
||||
@ -7532,9 +7574,7 @@
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"node_modules/lodash.castarray": {
|
||||
"version": "4.4.0",
|
||||
@ -7666,6 +7706,25 @@
|
||||
"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": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
|
||||
@ -10204,7 +10263,6 @@
|
||||
"version": "1.29.0",
|
||||
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz",
|
||||
"integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
@ -13819,12 +13877,41 @@
|
||||
"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": {
|
||||
"name": "jan-web",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.7.15",
|
||||
"@heroicons/react": "^2.0.18",
|
||||
"@janhq/plugin-core": "file:../plugin-core",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@types/react": "18.2.15",
|
||||
"@types/react-dom": "18.2.7",
|
||||
@ -13835,14 +13922,20 @@
|
||||
"embla-carousel-react": "^8.0.0-rc11",
|
||||
"eslint": "8.45.0",
|
||||
"eslint-config-next": "13.4.10",
|
||||
"framer-motion": "^10.16.4",
|
||||
"highlight.js": "^11.9.0",
|
||||
"jotai": "^2.4.0",
|
||||
"jotai-optics": "^0.3.1",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"lodash": "^4.17.21",
|
||||
"marked": "^9.1.2",
|
||||
"marked-highlight": "^2.0.6",
|
||||
"next": "13.4.10",
|
||||
"next-auth": "^4.23.1",
|
||||
"next-themes": "^0.2.1",
|
||||
"optics-ts": "^2.4.1",
|
||||
"postcss": "8.4.26",
|
||||
"prismjs": "^1.29.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-hook-form": "^7.45.4",
|
||||
@ -13898,6 +13991,14 @@
|
||||
"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": {
|
||||
"version": "8.4.26",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.26.tgz",
|
||||
|
||||
@ -76,6 +76,11 @@ export enum DataService {
|
||||
*/
|
||||
CreateConversation = "createConversation",
|
||||
|
||||
/**
|
||||
* Updates an existing conversation on the server.
|
||||
*/
|
||||
UpdateConversation = "updateConversation",
|
||||
|
||||
/**
|
||||
* Deletes an existing conversation from the server.
|
||||
*/
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import React from "react";
|
||||
import JanImage from "../JanImage";
|
||||
import { useAtomValue, useSetAtom } from "jotai";
|
||||
import Image from "next/image";
|
||||
import { Conversation } from "@/_models/Conversation";
|
||||
@ -17,11 +16,13 @@ import {
|
||||
MainViewState,
|
||||
} from "@/_helpers/atoms/MainView.atom";
|
||||
import useInitModel from "@/_hooks/useInitModel";
|
||||
import { displayDate } from "@/_utils/datetime";
|
||||
|
||||
type Props = {
|
||||
conversation: Conversation;
|
||||
avatarUrl?: string;
|
||||
name: string;
|
||||
summary?: string;
|
||||
updatedAt?: string;
|
||||
};
|
||||
|
||||
@ -29,6 +30,7 @@ const HistoryItem: React.FC<Props> = ({
|
||||
conversation,
|
||||
avatarUrl,
|
||||
name,
|
||||
summary,
|
||||
updatedAt,
|
||||
}) => {
|
||||
const setMainViewState = useSetAtom(setMainViewStateAtom);
|
||||
@ -73,49 +75,39 @@ const HistoryItem: React.FC<Props> = ({
|
||||
rightImageUrl = "icons/loading.svg";
|
||||
}
|
||||
|
||||
const description = conversation?.lastMessage ?? "No new message";
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`flex flex-row mx-1 items-center gap-2.5 rounded-lg p-2 ${backgroundColor} hover:bg-hover-light`}
|
||||
<li
|
||||
role="button"
|
||||
className={`flex flex-row ml-3 mr-2 rounded p-3 ${backgroundColor} hover:bg-hover-light`}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Image
|
||||
width={36}
|
||||
height={36}
|
||||
src={avatarUrl ?? "icons/app_icon.svg"}
|
||||
className="w-9 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">
|
||||
<span className="text-gray-900 text-left">{name}</span>
|
||||
<span className="text-xs leading-[13px] tracking-[-0.4px] text-gray-400">
|
||||
{updatedAt && new Date(updatedAt).toDateString()}
|
||||
<div className="w-8 h-8">
|
||||
<Image
|
||||
width={32}
|
||||
height={32}
|
||||
src={avatarUrl ?? "icons/app_icon.svg"}
|
||||
className="aspect-square rounded-full"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col ml-2 flex-1">
|
||||
{/* 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>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-1">
|
||||
<div className="flex-1">
|
||||
<span className="text-gray-400 hidden-text text-left">
|
||||
{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>
|
||||
|
||||
{/* description */}
|
||||
<span className="mt-1 text-gray-400 line-clamp-2">{description}</span>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -24,8 +24,10 @@ const HistoryList: React.FC = () => {
|
||||
expanded={expand}
|
||||
onClick={() => setExpand(!expand)}
|
||||
/>
|
||||
<div
|
||||
className={`flex flex-col gap-1 mt-1 overflow-y-auto scroll ${!expand ? "hidden " : "block"}`}
|
||||
<ul
|
||||
className={`flex flex-col gap-1 mt-1 overflow-y-auto scroll ${
|
||||
!expand ? "hidden " : "block"
|
||||
}`}
|
||||
>
|
||||
{conversations.length > 0 ? (
|
||||
conversations
|
||||
@ -38,6 +40,7 @@ const HistoryList: React.FC = () => {
|
||||
<HistoryItem
|
||||
key={convo._id}
|
||||
conversation={convo}
|
||||
summary={convo.summary}
|
||||
avatarUrl={convo.image}
|
||||
name={convo.name || "Jan"}
|
||||
updatedAt={convo.updatedAt ?? ""}
|
||||
@ -46,7 +49,7 @@ const HistoryList: React.FC = () => {
|
||||
) : (
|
||||
<SidebarEmptyHistory />
|
||||
)}
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -6,12 +6,10 @@ import { useAtomValue } from "jotai";
|
||||
import { showingAdvancedPromptAtom } from "@/_helpers/atoms/Modal.atom";
|
||||
import SecondaryButton from "../SecondaryButton";
|
||||
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 { activeAssistantModelAtom } from "@/_helpers/atoms/Model.atom";
|
||||
import LoadingIndicator from "../LoadingIndicator";
|
||||
import { currentConvoStateAtom } from "@/_helpers/atoms/Conversation.atom";
|
||||
import SendButton from "../SendButton";
|
||||
|
||||
const InputToolbar: React.FC = () => {
|
||||
const showingAdvancedPrompt = useAtomValue(showingAdvancedPromptAtom);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React, { Fragment } from "react";
|
||||
import SidebarFooter from "../SidebarFooter";
|
||||
import SidebarHeader from "../SidebarHeader";
|
||||
import SidebarMenu from "../SidebarMenu";
|
||||
@ -6,13 +6,13 @@ import HistoryList from "../HistoryList";
|
||||
import NewChatButton from "../NewChatButton";
|
||||
|
||||
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 />
|
||||
<NewChatButton />
|
||||
<HistoryList />
|
||||
<SidebarMenu />
|
||||
<SidebarFooter />
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
export default React.memo(LeftContainer);
|
||||
|
||||
45
web/app/_components/MainContainer/index.tsx
Normal file
45
web/app/_components/MainContainer/index.tsx
Normal 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;
|
||||
@ -1,11 +1,12 @@
|
||||
import { Fragment } from "react";
|
||||
import MainView from "../MainView";
|
||||
import MonitorBar from "../MonitorBar";
|
||||
|
||||
const RightContainer = () => (
|
||||
<div className="flex flex-col flex-1 h-screen">
|
||||
<Fragment>
|
||||
<MainView />
|
||||
<MonitorBar />
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
export default RightContainer;
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { currentPromptAtom } from "@/_helpers/JotaiWrapper";
|
||||
import { currentConvoStateAtom } from "@/_helpers/atoms/Conversation.atom";
|
||||
import useSendChatMessage from "@/_hooks/useSendChatMessage";
|
||||
import { ArrowRightIcon } from "@heroicons/react/24/outline";
|
||||
import { useAtom, useAtomValue } from "jotai";
|
||||
|
||||
const SendButton: React.FC = () => {
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import React from "react";
|
||||
import { displayDate } from "@/_utils/datetime";
|
||||
import { TextCode } from "../TextCode";
|
||||
import { getMessageCode } from "@/_utils/message";
|
||||
import Image from "next/image";
|
||||
import { MessageSenderType } from "@/_models/ChatMessage";
|
||||
import LoadingIndicator from "../LoadingIndicator";
|
||||
import { Marked } from "marked";
|
||||
import { markedHighlight } from "marked-highlight";
|
||||
import hljs from "highlight.js";
|
||||
|
||||
type Props = {
|
||||
avatarUrl: string;
|
||||
@ -14,16 +15,26 @@ type Props = {
|
||||
text?: string;
|
||||
};
|
||||
|
||||
const renderMessageCode = (text: string) => {
|
||||
return getMessageCode(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>
|
||||
));
|
||||
};
|
||||
const marked = new Marked(
|
||||
markedHighlight({
|
||||
langPrefix: "hljs",
|
||||
highlight(code, lang) {
|
||||
if (lang === undefined || lang === "") {
|
||||
return hljs.highlightAuto(code).value;
|
||||
}
|
||||
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> = ({
|
||||
senderName,
|
||||
@ -35,6 +46,8 @@ const SimpleTextMessage: React.FC<Props> = ({
|
||||
const backgroundColor =
|
||||
senderType === MessageSenderType.User ? "" : "bg-gray-100";
|
||||
|
||||
const parsedText = marked.parse(text);
|
||||
|
||||
return (
|
||||
<div
|
||||
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}
|
||||
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="text-[#1B1B1B] text-sm font-extrabold leading-[15.2px] dark:text-[#d1d5db]">
|
||||
{senderName}
|
||||
@ -57,10 +70,11 @@ const SimpleTextMessage: React.FC<Props> = ({
|
||||
</div>
|
||||
{text === "" ? (
|
||||
<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>
|
||||
|
||||
@ -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);
|
||||
@ -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>
|
||||
);
|
||||
@ -8,7 +8,7 @@ const UserToolbar: React.FC = () => {
|
||||
const currentConvo = useAtomValue(currentConversationAtom);
|
||||
|
||||
const avatarUrl = currentConvo?.image;
|
||||
const title = currentConvo?.name ?? "";
|
||||
const title = currentConvo?.summary ?? currentConvo?.name ?? "";
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3 p-1">
|
||||
|
||||
@ -103,7 +103,3 @@ export const updateLastMessageAsReadyAtom = atom(
|
||||
set(chatMessages, newData);
|
||||
}
|
||||
);
|
||||
|
||||
export const currentStreamingMessageAtom = atom<ChatMessage | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
@ -33,7 +33,7 @@ export const currentConvoStateAtom = atom<ConversationState | undefined>(
|
||||
(get) => {
|
||||
const activeConvoId = get(activeConversationIdAtom);
|
||||
if (!activeConvoId) {
|
||||
console.log("active convo id is undefined");
|
||||
console.debug("Active convo id is 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
|
||||
*/
|
||||
@ -87,30 +110,3 @@ export const userConversationsAtom = atom<Conversation[]>([]);
|
||||
export const currentConversationAtom = atom<Conversation | undefined>((get) =>
|
||||
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);
|
||||
}
|
||||
);
|
||||
|
||||
6
web/app/_helpers/atoms/LeftSideBarExpand.atom.ts
Normal file
6
web/app/_helpers/atoms/LeftSideBarExpand.atom.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { atom } from "jotai";
|
||||
|
||||
/**
|
||||
* Stores expand state of conversation container. Default is true.
|
||||
*/
|
||||
export const leftSideBarExpandStateAtom = atom<boolean>(true);
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
@ -13,12 +13,17 @@ export default function useInitModel() {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentTime = Date.now();
|
||||
console.debug("Init model: ", model._id);
|
||||
|
||||
const res = await executeSerial(InferenceService.InitModel, model._id);
|
||||
if (res?.error) {
|
||||
console.log("error occured: ", res);
|
||||
return res;
|
||||
} else {
|
||||
console.log(`Init model successfully!`);
|
||||
console.debug(
|
||||
`Init model successfully!, take ${Date.now() - currentTime}ms`
|
||||
);
|
||||
setActiveModel(model);
|
||||
return {};
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ import { useAtom, useAtomValue, useSetAtom } from "jotai";
|
||||
import { selectAtom } from "jotai/utils";
|
||||
import { DataService, InferenceService } from "@janhq/plugin-core";
|
||||
import {
|
||||
ChatMessage,
|
||||
MessageSenderType,
|
||||
RawMessage,
|
||||
toChatMessage,
|
||||
@ -13,17 +14,19 @@ import {
|
||||
addNewMessageAtom,
|
||||
updateMessageAtom,
|
||||
chatMessages,
|
||||
currentStreamingMessageAtom,
|
||||
currentChatMessagesAtom,
|
||||
} from "@/_helpers/atoms/ChatMessage.atom";
|
||||
import {
|
||||
currentConversationAtom,
|
||||
getActiveConvoIdAtom,
|
||||
updateConversationAtom,
|
||||
updateConversationWaitingForResponseAtom,
|
||||
} from "@/_helpers/atoms/Conversation.atom";
|
||||
import { Conversation } from "@/_models/Conversation";
|
||||
|
||||
export default function useSendChatMessage() {
|
||||
const currentConvo = useAtomValue(currentConversationAtom);
|
||||
const updateStreamMessage = useSetAtom(currentStreamingMessageAtom);
|
||||
const updateConversation = useSetAtom(updateConversationAtom);
|
||||
const addNewMessage = useSetAtom(addNewMessageAtom);
|
||||
const updateMessage = useSetAtom(updateMessageAtom);
|
||||
const activeConversationId = useAtomValue(getActiveConvoIdAtom) ?? "";
|
||||
@ -100,7 +103,7 @@ export default function useSendChatMessage() {
|
||||
};
|
||||
const respId = await executeSerial(DataService.CreateMessage, newResponse);
|
||||
newResponse._id = respId;
|
||||
const responseChatMessage = await toChatMessage(newResponse);
|
||||
const responseChatMessage = toChatMessage(newResponse);
|
||||
addNewMessage(responseChatMessage);
|
||||
|
||||
while (true && reader) {
|
||||
@ -119,10 +122,6 @@ export default function useSendChatMessage() {
|
||||
if (answer.startsWith("assistant: ")) {
|
||||
answer = answer.replace("assistant: ", "");
|
||||
}
|
||||
updateStreamMessage({
|
||||
...responseChatMessage,
|
||||
text: answer,
|
||||
});
|
||||
updateMessage(
|
||||
responseChatMessage.id,
|
||||
responseChatMessage.conversationId,
|
||||
@ -144,8 +143,103 @@ export default function useSendChatMessage() {
|
||||
.replace("T", " ")
|
||||
.replace(/\.\d+Z$/, ""),
|
||||
});
|
||||
|
||||
const updatedConvo: Conversation = {
|
||||
...currentConvo,
|
||||
lastMessage: answer.trim(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
await executeSerial(DataService.UpdateConversation, updatedConvo);
|
||||
updateConversation(updatedConvo);
|
||||
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 {
|
||||
sendChatMessage,
|
||||
};
|
||||
|
||||
@ -1,6 +1,3 @@
|
||||
import { remark } from "remark";
|
||||
import html from "remark-html";
|
||||
|
||||
export enum MessageType {
|
||||
Text = "Text",
|
||||
Image = "Image",
|
||||
@ -9,8 +6,8 @@ export enum MessageType {
|
||||
}
|
||||
|
||||
export enum MessageSenderType {
|
||||
Ai = "Ai",
|
||||
User = "User",
|
||||
Ai = "assistant",
|
||||
User = "user",
|
||||
}
|
||||
|
||||
export enum MessageStatus {
|
||||
@ -26,7 +23,6 @@ export interface ChatMessage {
|
||||
senderUid: string;
|
||||
senderName: string;
|
||||
senderAvatarUrl: string;
|
||||
htmlText?: string | undefined;
|
||||
text: string | undefined;
|
||||
imageUrls?: string[] | undefined;
|
||||
createdAt: number;
|
||||
@ -42,7 +38,7 @@ export interface RawMessage {
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export const toChatMessage = async (m: RawMessage): Promise<ChatMessage> => {
|
||||
export const toChatMessage = (m: RawMessage): ChatMessage => {
|
||||
const createdAt = new Date(m.createdAt ?? "").getTime();
|
||||
const imageUrls: string[] = [];
|
||||
const imageUrl = undefined;
|
||||
@ -55,8 +51,6 @@ export const toChatMessage = async (m: RawMessage): Promise<ChatMessage> => {
|
||||
m.user === "user" ? MessageSenderType.User : MessageSenderType.Ai;
|
||||
|
||||
const content = m.message ?? "";
|
||||
const processedContent = await remark().use(html).process(content);
|
||||
const contentHtml = processedContent.toString();
|
||||
|
||||
return {
|
||||
id: (m._id ?? 0).toString(),
|
||||
@ -68,7 +62,6 @@ export const toChatMessage = async (m: RawMessage): Promise<ChatMessage> => {
|
||||
senderAvatarUrl:
|
||||
m.user === "user" ? "icons/avatar.svg" : "icons/app_icon.svg",
|
||||
text: content,
|
||||
htmlText: contentHtml,
|
||||
imageUrls: imageUrls,
|
||||
createdAt: createdAt,
|
||||
status: MessageStatus.Ready,
|
||||
|
||||
@ -4,6 +4,8 @@ export interface Conversation {
|
||||
name?: string;
|
||||
image?: string;
|
||||
message?: string;
|
||||
lastMessage?: string;
|
||||
summary?: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
@ -25,18 +25,3 @@ export function mergeAndRemoveDuplicates(
|
||||
|
||||
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
97
web/app/code-block.css
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,7 @@
|
||||
@tailwind utilities;
|
||||
|
||||
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap");
|
||||
@import "./code-block.css";
|
||||
|
||||
:root {
|
||||
/* Your default theme */
|
||||
|
||||
@ -2,11 +2,9 @@
|
||||
|
||||
import { ThemeWrapper } from "./_helpers/ThemeWrapper";
|
||||
import JotaiWrapper from "./_helpers/JotaiWrapper";
|
||||
import RightContainer from "./_components/RightContainer";
|
||||
import { ModalWrapper } from "./_helpers/ModalWrapper";
|
||||
import { useEffect, useState } from "react";
|
||||
import Image from "next/image";
|
||||
|
||||
import {
|
||||
setup,
|
||||
plugins,
|
||||
@ -16,9 +14,9 @@ import {
|
||||
isCorePluginInstalled,
|
||||
setupBasePlugins,
|
||||
} from "./_services/pluginService";
|
||||
import LeftContainer from "./_components/LeftContainer";
|
||||
import EventListenerWrapper from "./_helpers/EventListenerWrapper";
|
||||
import { setupCoreServices } from "./_services/coreService";
|
||||
import MainContainer from "./_components/MainContainer";
|
||||
|
||||
const Page: React.FC = () => {
|
||||
const [activated, setActivated] = useState(false);
|
||||
@ -63,13 +61,9 @@ const Page: React.FC = () => {
|
||||
<EventListenerWrapper>
|
||||
<ThemeWrapper>
|
||||
<ModalWrapper>
|
||||
{activated && (
|
||||
<div className="flex">
|
||||
<LeftContainer />
|
||||
<RightContainer />
|
||||
</div>
|
||||
)}
|
||||
{!activated && (
|
||||
{activated ? (
|
||||
<MainContainer />
|
||||
) : (
|
||||
<div className="bg-white w-screen h-screen items-center justify-center flex">
|
||||
<Image width={50} height={50} src="icons/app_icon.svg" alt="" />
|
||||
</div>
|
||||
|
||||
@ -17,16 +17,19 @@
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@types/react": "18.2.15",
|
||||
"@types/react-dom": "18.2.7",
|
||||
"@types/react-syntax-highlighter": "^15.5.7",
|
||||
"autoprefixer": "10.4.14",
|
||||
"classnames": "^2.3.2",
|
||||
"embla-carousel": "^8.0.0-rc11",
|
||||
"embla-carousel-react": "^8.0.0-rc11",
|
||||
"eslint": "8.45.0",
|
||||
"eslint-config-next": "13.4.10",
|
||||
"framer-motion": "^10.16.4",
|
||||
"highlight.js": "^11.9.0",
|
||||
"jotai": "^2.4.0",
|
||||
"jotai-optics": "^0.3.1",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"marked": "^9.1.2",
|
||||
"marked-highlight": "^2.0.6",
|
||||
"next": "13.4.10",
|
||||
"next-auth": "^4.23.1",
|
||||
"next-themes": "^0.2.1",
|
||||
@ -35,9 +38,6 @@
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"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",
|
||||
"typescript": "5.1.6"
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user