Refactor Jan into an Electron app (#175)
* hackathon: Refactor Jan into an Electron app * chore: correct NextJS export output path * chore: build electron app for all production targets * fix: correct assetPrefix for production build * chore: preferences shortcut * chore: refactor * chore: refactor into ts * feature/#52-compile-plugin-with-webpack * chore: introduce renderer <=> plugins <=> main invocation * chore: suppress errors - deprecate graphql & next-auth * chore: data plugin functions * add llm support Signed-off-by: James <james@jan.ai> * chore: update plugin * chore: introduce data-plugin * chore: plugin invokes main with args and synchronously * chore: install db plugin should setup db * feature: Data Driver Plugin - Load conversations and messages from data plugin * chore: store text message sent * chore: shared core services * feature: inference service * chore: conversations ordering * adding model management service Signed-off-by: James <james@jan.ai> * chore: strict type * feature: abstract plugin preferences * chore: abstract plugin preference * Revert "chore: strict type" This reverts commit 9be188d827a0b2e081e9e04b192c323799de5bb5. * chore: base-plugin styling * feature: create and delete conversation * chore: fix plugin search & clean messages * chore: typing indicator * chore: refactor useSendChatMessage * chore: persists inserted id to in-memory messages * chore: search conversation history * add delete and download model (#189) Signed-off-by: James <james@jan.ai> Co-authored-by: James <james@jan.ai> * chore: add empty state for conversation list * chore: prompt missing extension function & fix app crashes * chore: prompt user to install required plugins * chore: add launch background * chore: relaunch app on model downloaded * Jan app add installation instruction (#191) Co-authored-by: Hien To <> * Chore: rename folder web-client to app (#192) * Chore: rename folder web-client to app --------- Co-authored-by: Hien To <> * revert: add pre-install package * add progress for downloading model Signed-off-by: James <james@jan.ai> * feature: production bundle * add download progress Signed-off-by: James <james@jan.ai> * chore: add new chat function * fix: electron asar unpack modules & dynamic import * chore: fix unpack * chore: fix dev pack * Add instruction to build dmg file to README.md * init model dynamically Signed-off-by: James <james@jan.ai> --------- Signed-off-by: James <james@jan.ai> Co-authored-by: James <james@jan.ai> Co-authored-by: NamH <NamNh0122@gmail.com> Co-authored-by: hiento09 <136591877+hiento09@users.noreply.github.com> Co-authored-by: Hien To <>
This commit is contained in:
parent
a5c70630f9
commit
20dbc02c03
6
.gitignore
vendored
6
.gitignore
vendored
@ -4,3 +4,9 @@
|
||||
# Jan inference
|
||||
models/**
|
||||
error.log
|
||||
app/electron/core/*/node_modules
|
||||
app/electron/core/*/dist
|
||||
app/electron/core/*/package-lock.json
|
||||
*.tgz
|
||||
app/yarn.lock
|
||||
app/dist
|
||||
|
||||
0
web-client/.gitignore → app/.gitignore
vendored
0
web-client/.gitignore → app/.gitignore
vendored
96
app/README.md
Normal file
96
app/README.md
Normal file
@ -0,0 +1,96 @@
|
||||
# App
|
||||
|
||||
Jan Desktop is an Electron application designed to allow users to interact with the Language Model (LLM) through chat or create art using Stable Diffusion.
|
||||
|
||||
## Features
|
||||
|
||||
- Chat with the Language Model: Engage in interactive conversations with the Language Model. Ask questions, seek information, or simply have a chat.
|
||||
|
||||
- Generate Art with Stable Diffusion: Utilize the power of Stable Diffusion to generate unique and captivating pieces of art. Experiment with various parameters to achieve desired results.
|
||||
|
||||
## Installation and Usage
|
||||
|
||||
### Pre-requisites
|
||||
- node >= 20.0.0
|
||||
- yarn >= 1.22.0
|
||||
|
||||
### Use as complete suite (in progress)
|
||||
|
||||
### For interactive development
|
||||
|
||||
Note: This instruction is tested on MacOS only.
|
||||
|
||||
1. **Clone the Repository:**
|
||||
|
||||
```
|
||||
git clone https://github.com/janhq/jan
|
||||
git checkout feature/hackathon-refactor-jan-into-electron-app
|
||||
cd jan/app
|
||||
```
|
||||
|
||||
2. **Install dependencies:**
|
||||
|
||||
```
|
||||
yarn install
|
||||
```
|
||||
|
||||
3. **Download Model and copy to userdata directory** (this is a hacky step, will be remove in future versions)
|
||||
|
||||
```
|
||||
# Determining the path to save model with /Users/<username>/Library/Application Support/jan-web/
|
||||
mkdir /Users/<username>/Library/Application Support/jan-web
|
||||
|
||||
# Now download the model to correct location by running command
|
||||
wget -O /Users/<username>/Library/Application Support/jan-web/llama-2-7b-chat.gguf.q4_0.bin https://huggingface.co/TheBloke/Llama-2-7b-Chat-GGUF/resolve/main/llama-2-7b-chat.Q4_0.gguf
|
||||
```
|
||||
|
||||
4. **Run development and Using Jan Desktop**
|
||||
|
||||
```
|
||||
yarn electron:start
|
||||
```
|
||||
This will start the development server and open the desktop app.
|
||||
In this step, there are a few notification about installing base plugin, just click `OK` and `Next` to continue.
|
||||

|
||||

|
||||

|
||||
|
||||
After that, you can use Jan Desktop as normal.
|
||||

|
||||

|
||||

|
||||
|
||||
### For production build
|
||||
|
||||
```bash
|
||||
# Do step 1 and 2 in previous section
|
||||
git clone https://github.com/janhq/jan
|
||||
git checkout feature/hackathon-refactor-jan-into-electron-app
|
||||
cd jan/app
|
||||
yarn install
|
||||
|
||||
# Build the app
|
||||
yarn electron:build:all
|
||||
```
|
||||
|
||||
This will build the app MacOS m1/m2 for production (with code signing already done) and put the result in `dist` folder.
|
||||
|
||||
## Configuration
|
||||
|
||||
TO DO
|
||||
|
||||
## Dependencies
|
||||
|
||||
TO DO
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! If you find a bug or have suggestions for improvements, feel free to open an issue or submit a pull request on the [GitHub repository](https://github.com/janhq/jan).
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the Fair-code License - see the [License](https://faircode.io/#licenses) for more details.
|
||||
|
||||
---
|
||||
|
||||
Feel free to reach out [Discord](https://jan.ai/discord) if you have any questions or need further assistance. Happy coding with Jan Web and exploring the capabilities of the Language Model and Stable Diffusion! 🚀🎨🤖
|
||||
@ -6,7 +6,7 @@ const AdvancedPromptGenerationParams = () => {
|
||||
return (
|
||||
<>
|
||||
<TogglableHeader
|
||||
icon={"/icons/unicorn_layers-alt.svg"}
|
||||
icon={"icons/unicorn_layers-alt.svg"}
|
||||
title={"Generation Parameters"}
|
||||
expand={expand}
|
||||
onTitleClick={() => setExpand(!expand)}
|
||||
@ -15,7 +15,7 @@ const AdvancedPromptImageUpload: React.FC<Props> = ({ register }) => {
|
||||
return (
|
||||
<>
|
||||
<TogglableHeader
|
||||
icon={"/icons/ic_image.svg"}
|
||||
icon={"icons/ic_image.svg"}
|
||||
title={"Image"}
|
||||
expand={expand}
|
||||
onTitleClick={() => setExpand(!expand)}
|
||||
@ -10,7 +10,7 @@ const AdvancedPromptResolution = () => {
|
||||
return (
|
||||
<>
|
||||
<TogglableHeader
|
||||
icon={"/icons/unicorn_layers-alt.svg"}
|
||||
icon={"icons/unicorn_layers-alt.svg"}
|
||||
title={"Resolution"}
|
||||
expand={expand}
|
||||
onTitleClick={() => setExpand(!expand)}
|
||||
@ -13,7 +13,7 @@ const AdvancedPromptText: React.FC<Props> = ({ register }) => {
|
||||
return (
|
||||
<>
|
||||
<TogglableHeader
|
||||
icon={"/icons/messicon.svg"}
|
||||
icon={"icons/messicon.svg"}
|
||||
title={"Prompt"}
|
||||
expand={expand}
|
||||
onTitleClick={() => setExpand(!expand)}
|
||||
@ -4,7 +4,7 @@ const Search: React.FC = () => {
|
||||
return (
|
||||
<div className="flex bg-gray-200 w-[343px] h-[36px] items-center px-2 gap-[6px] rounded-md">
|
||||
<Image
|
||||
src={"/icons/magnifyingglass.svg"}
|
||||
src={"icons/magnifyingglass.svg"}
|
||||
width={15.63}
|
||||
height={15.78}
|
||||
alt=""
|
||||
@ -13,8 +13,8 @@ const BasicPromptAccessories: React.FC = () => {
|
||||
const setShowingAdvancedPrompt = useSetAtom(showingAdvancedPromptAtom);
|
||||
const currentConversation = useAtomValue(currentConversationAtom);
|
||||
|
||||
const shouldShowAdvancedPrompt =
|
||||
currentConversation?.product.type === ProductType.ControlNet;
|
||||
const shouldShowAdvancedPrompt = false;
|
||||
// currentConversation?.product.type === ProductType.ControlNet;
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -4,11 +4,16 @@ import React, { useCallback, useRef, useState } from "react";
|
||||
import ChatItem from "../ChatItem";
|
||||
import { ChatMessage } from "@/_models/ChatMessage";
|
||||
import useChatMessages from "@/_hooks/useChatMessages";
|
||||
import { currentChatMessagesAtom } from "@/_helpers/JotaiWrapper";
|
||||
import {
|
||||
currentChatMessagesAtom,
|
||||
showingTyping,
|
||||
} from "@/_helpers/JotaiWrapper";
|
||||
import { useAtomValue } from "jotai";
|
||||
import LoadingIndicator from "../LoadingIndicator";
|
||||
|
||||
const ChatBody: React.FC = () => {
|
||||
const messages = useAtomValue(currentChatMessagesAtom);
|
||||
const isTyping = useAtomValue(showingTyping);
|
||||
const [offset, setOffset] = useState(0);
|
||||
const { loading, hasMore } = useChatMessages(offset);
|
||||
const intersectObs = useRef<any>(null);
|
||||
@ -32,6 +37,7 @@ const ChatBody: React.FC = () => {
|
||||
|
||||
const content = messages.map((message, index) => {
|
||||
if (messages.length === index + 1) {
|
||||
// @ts-ignore
|
||||
return <ChatItem ref={lastPostRef} message={message} key={message.id} />;
|
||||
}
|
||||
return <ChatItem message={message} key={message.id} />;
|
||||
@ -39,6 +45,11 @@ const ChatBody: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col-reverse flex-1 py-4 overflow-y-auto scroll">
|
||||
{isTyping && (
|
||||
<div className="ml-4 mb-2" key="indicator">
|
||||
<LoadingIndicator />
|
||||
</div>
|
||||
)}
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
@ -8,6 +8,7 @@ import {
|
||||
showingProductDetailAtom,
|
||||
} from "@/_helpers/JotaiWrapper";
|
||||
import { ReactNode } from "react";
|
||||
import ModelManagement from "../ModelManagement";
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
@ -15,16 +16,17 @@ type Props = {
|
||||
|
||||
export default function ChatContainer({ children }: Props) {
|
||||
const activeConvoId = useAtomValue(getActiveConvoIdAtom);
|
||||
const showingProductDetail = useAtomValue(showingProductDetailAtom);
|
||||
// const showingProductDetail = useAtomValue(showingProductDetailAtom);
|
||||
|
||||
if (!activeConvoId) {
|
||||
return <ProductOverview />;
|
||||
// return <ProductOverview />;
|
||||
return <ModelManagement />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
{children}
|
||||
{showingProductDetail ? <ModelDetailSideBar /> : null}
|
||||
{/* {showingProductDetail ? <ModelDetailSideBar /> : null} */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable react/display-name */
|
||||
import React, { forwardRef } from "react";
|
||||
import renderChatMessage from "../ChatBody/renderChatMessage";
|
||||
import { ChatMessage } from "@/_models/ChatMessage";
|
||||
@ -7,11 +7,11 @@ const CompactHistoryList: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-1 gap-1 mt-3">
|
||||
{conversations.map(({ id, product }) => (
|
||||
{conversations.map(({ id, image }) => (
|
||||
<CompactHistoryItem
|
||||
key={id}
|
||||
conversationId={id}
|
||||
imageUrl={product.avatarUrl ?? ""}
|
||||
conversationId={id ?? ""}
|
||||
imageUrl={image ?? ""}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@ -8,7 +8,7 @@ const CompactLogo: React.FC = () => {
|
||||
|
||||
return (
|
||||
<button onClick={() => setActiveConvoId(undefined)}>
|
||||
<JanImage imageUrl="/icons/app_icon.svg" width={28} height={28} />
|
||||
<JanImage imageUrl="icons/app_icon.svg" width={28} height={28} />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
89
app/app/_components/ConfirmDeleteModelModal/index.tsx
Normal file
89
app/app/_components/ConfirmDeleteModelModal/index.tsx
Normal file
@ -0,0 +1,89 @@
|
||||
import React, { Fragment } from "react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { QuestionMarkCircleIcon } from "@heroicons/react/24/outline";
|
||||
import { showConfirmDeleteModalAtom } from "@/_helpers/JotaiWrapper";
|
||||
import { useAtom } from "jotai";
|
||||
import useSignOut from "@/_hooks/useSignOut";
|
||||
|
||||
const ConfirmDeleteModelModal: React.FC = () => {
|
||||
const [show, setShow] = useAtom(showConfirmDeleteModalAtom);
|
||||
const { signOut } = useSignOut();
|
||||
|
||||
const onLogOutClick = () => {
|
||||
signOut().then(() => setShow(false));
|
||||
};
|
||||
|
||||
return (
|
||||
<Transition.Root show={show} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-10" onClose={setShow}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6">
|
||||
<div className="sm:flex sm:items-start">
|
||||
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
|
||||
<QuestionMarkCircleIcon
|
||||
className="h-6 w-6 text-green-600"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-base font-semibold leading-6 text-gray-900"
|
||||
>
|
||||
Log out
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-gray-500">
|
||||
Are you sure you want to delete this model?
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex w-full justify-center rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 sm:ml-3 sm:w-auto"
|
||||
onClick={onLogOutClick}
|
||||
>
|
||||
Log out
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="mt-3 inline-flex w-full justify-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 sm:mt-0 sm:w-auto"
|
||||
onClick={() => setShow(false)}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ConfirmDeleteModelModal);
|
||||
@ -15,7 +15,7 @@ const ConversationalCard: React.FC<Props> = ({ product }) => {
|
||||
return (
|
||||
<button
|
||||
onClick={() =>
|
||||
requestCreateConvo(product)
|
||||
requestCreateConvo(1)
|
||||
}
|
||||
className="flex flex-col justify-between flex-shrink-0 gap-3 bg-white p-4 w-52 rounded-lg text-left dark:bg-gray-700 hover:opacity-20"
|
||||
>
|
||||
@ -35,7 +35,7 @@ const ConversationalCard: React.FC<Props> = ({ product }) => {
|
||||
</span>
|
||||
</div>
|
||||
<span className="flex text-xs leading-5 text-gray-500 items-center gap-[2px]">
|
||||
<Image src={"/icons/play.svg"} width={16} height={16} alt="" />
|
||||
<Image src={"icons/play.svg"} width={16} height={16} alt="" />
|
||||
32.2k runs
|
||||
</span>
|
||||
</button>
|
||||
@ -4,25 +4,12 @@ import Image from "next/image";
|
||||
|
||||
const DiscordContainer = () => (
|
||||
<div className="border-t border-gray-200 p-3 gap-3 flex items-center justify-between">
|
||||
<Link
|
||||
className="flex gap-2 items-center rounded-lg text-gray-900 text-xs leading-[18px]"
|
||||
href="/download"
|
||||
target="_blank_"
|
||||
>
|
||||
<Image
|
||||
src={"/icons/ico_mobile-android.svg"}
|
||||
width={16}
|
||||
height={16}
|
||||
alt=""
|
||||
/>
|
||||
Get the app
|
||||
</Link>
|
||||
<Link
|
||||
className="flex items-center rounded-lg text-purple-700 text-xs leading-[18px] font-semibold gap-2"
|
||||
href={process.env.NEXT_PUBLIC_DISCORD_INVITATION_URL ?? "#"}
|
||||
target="_blank_"
|
||||
>
|
||||
<Image src={"/icons/ico_Discord.svg"} width={20} height={20} alt="" />
|
||||
<Image src={"icons/ico_Discord.svg"} width={20} height={20} alt="" />
|
||||
Discord
|
||||
</Link>
|
||||
</div>
|
||||
81
app/app/_components/DownloadModelCard/index.tsx
Normal file
81
app/app/_components/DownloadModelCard/index.tsx
Normal file
@ -0,0 +1,81 @@
|
||||
"use client";
|
||||
|
||||
import DownloadModelContent from "../DownloadModelContent";
|
||||
import ModelDownloadButton from "../ModelDownloadButton";
|
||||
import ModelDownloadingButton from "../ModelDownloadingButton";
|
||||
import ViewModelDetailButton from "../ViewModelDetailButton";
|
||||
|
||||
type Props = {
|
||||
name: string;
|
||||
type: string;
|
||||
author: string;
|
||||
description: string;
|
||||
isRecommend: boolean;
|
||||
storage: number;
|
||||
installed?: boolean;
|
||||
required?: string;
|
||||
downloading?: boolean;
|
||||
total?: number;
|
||||
transferred?: number;
|
||||
onInitClick?: () => void;
|
||||
onDeleteClick?: () => void;
|
||||
onDownloadClick?: () => void;
|
||||
};
|
||||
|
||||
const DownloadModelCard: React.FC<Props> = ({
|
||||
author,
|
||||
description,
|
||||
isRecommend,
|
||||
name,
|
||||
storage,
|
||||
type,
|
||||
installed = false,
|
||||
required,
|
||||
downloading = false,
|
||||
total = 0,
|
||||
transferred = 0,
|
||||
onInitClick,
|
||||
onDeleteClick,
|
||||
onDownloadClick,
|
||||
}) => {
|
||||
const handleViewDetails = () => {};
|
||||
|
||||
let downloadButton = null;
|
||||
if (!installed) {
|
||||
downloadButton = downloading ? (
|
||||
<div className="w-1/5 flex items-center justify-end">
|
||||
<ModelDownloadButton callback={() => onDownloadClick?.()} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-1/5 flex items-start justify-end">
|
||||
<ModelDownloadingButton total={total} value={transferred} />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
downloadButton = (
|
||||
<div className="flex flex-col">
|
||||
<button onClick={onInitClick}>Init</button>
|
||||
<button onClick={onDeleteClick}>Delete</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border rounded-lg border-gray-200">
|
||||
<div className="flex justify-between py-4 px-3 gap-[10px]">
|
||||
<DownloadModelContent
|
||||
required={required}
|
||||
author={author}
|
||||
description={description}
|
||||
isRecommend={isRecommend}
|
||||
name={name}
|
||||
type={type}
|
||||
/>
|
||||
{downloadButton}
|
||||
</div>
|
||||
<ViewModelDetailButton callback={handleViewDetails} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DownloadModelCard;
|
||||
58
app/app/_components/DownloadModelContent/index.tsx
Normal file
58
app/app/_components/DownloadModelContent/index.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import DownloadModelTitle from "../DownloadModelTitle";
|
||||
|
||||
type Props = {
|
||||
author: string;
|
||||
description: string;
|
||||
isRecommend: boolean;
|
||||
name: string;
|
||||
type: string;
|
||||
required?: string;
|
||||
};
|
||||
|
||||
const DownloadModelContent: React.FC<Props> = ({
|
||||
author,
|
||||
description,
|
||||
isRecommend,
|
||||
name,
|
||||
required,
|
||||
type,
|
||||
}) => {
|
||||
return (
|
||||
<div className="w-4/5 flex flex-col gap-[10px]">
|
||||
<div className="flex items-center gap-1">
|
||||
<h2 className="font-medium text-xl leading-[25px] tracking-[-0.4px] text-gray-900">
|
||||
{name}
|
||||
</h2>
|
||||
<DownloadModelTitle title={type} />
|
||||
<div className="py-[2px] px-[10px] bg-purple-100 rounded-md text-center">
|
||||
<span className="text-xs leading-[18px] font-semibold text-purple-800">
|
||||
{author}
|
||||
</span>
|
||||
</div>
|
||||
{required && (
|
||||
<div className="py-[2px] px-[10px] bg-purple-100 rounded-md text-center">
|
||||
<span className="text-xs leading-[18px] text-[#11192899]">
|
||||
Required{" "}
|
||||
</span>
|
||||
<span className="text-xs leading-[18px] font-semibold text-gray-900">
|
||||
{required}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs leading-[18px] text-gray-500">{description}</p>
|
||||
<div
|
||||
className={`${
|
||||
isRecommend ? "flex" : "hidden"
|
||||
} w-fit justify-center items-center bg-green-50 rounded-full px-[10px] py-[2px] gap-2`}
|
||||
>
|
||||
<div className="w-3 h-3 rounded-full bg-green-400"></div>
|
||||
<span className="text-green-600 font-medium text-xs leading-18px">
|
||||
Recommend
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DownloadModelContent;
|
||||
13
app/app/_components/DownloadModelTitle/index.tsx
Normal file
13
app/app/_components/DownloadModelTitle/index.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
type Props = {
|
||||
title: string;
|
||||
};
|
||||
|
||||
export const DownloadModelTitle: React.FC<Props> = ({ title }) => (
|
||||
<div className="py-[2px] px-[10px] bg-purple-100 rounded-md text-center">
|
||||
<span className="text-xs leading-[18px] font-medium text-purple-800">
|
||||
{title}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default DownloadModelTitle;
|
||||
@ -21,7 +21,7 @@ export const DropdownsList: React.FC<Props> = ({ data, title }) => {
|
||||
<Menu.Button className="inline-flex w-full items-center justify-between gap-x-1.5 rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50">
|
||||
{checked}
|
||||
<Image
|
||||
src={"/icons/unicorn_angle-down.svg"}
|
||||
src={"icons/unicorn_angle-down.svg"}
|
||||
width={12}
|
||||
height={12}
|
||||
alt=""
|
||||
@ -6,7 +6,7 @@ export default function Footer() {
|
||||
return (
|
||||
<div className="flex items-center justify-between container m-auto">
|
||||
<div className="flex items-center gap-3">
|
||||
<Image src={"/icons/app_icon.svg"} width={32} height={32} alt="" />
|
||||
<Image src={"icons/app_icon.svg"} width={32} height={32} alt="" />
|
||||
<span>Jan</span>
|
||||
</div>
|
||||
<div className="flex gap-4 my-6">
|
||||
@ -11,7 +11,7 @@ const GenerateImageCard: React.FC<Props> = ({ product }) => {
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => requestCreateConvo(product)}
|
||||
onClick={() => requestCreateConvo(1)}
|
||||
className="relative active:opacity-50 text-left"
|
||||
>
|
||||
<img
|
||||
23
app/app/_components/Header/index.tsx
Normal file
23
app/app/_components/Header/index.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import React from "react";
|
||||
import UserProfileDropDown from "../UserProfileDropDown";
|
||||
import LoginButton from "../LoginButton";
|
||||
import HamburgerButton from "../HamburgerButton";
|
||||
import { CogIcon } from "@heroicons/react/24/outline";
|
||||
import Link from "next/link";
|
||||
|
||||
const Header: React.FC = () => {
|
||||
return (
|
||||
<header className="flex border-b-[1px] border-gray-200 p-3 dark:bg-gray-800">
|
||||
<nav className="flex-1 justify-center">
|
||||
<HamburgerButton />
|
||||
</nav>
|
||||
<Link href="/settings">
|
||||
<CogIcon width={30} height={30} />
|
||||
</Link>
|
||||
<LoginButton />
|
||||
<UserProfileDropDown />
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
13
app/app/_components/HeaderBackButton/index.tsx
Normal file
13
app/app/_components/HeaderBackButton/index.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import React from "react";
|
||||
import { ArrowLeftIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
const HeaderBackButton: React.FC = () => {
|
||||
return (
|
||||
<button className="flex items-center gap-1">
|
||||
<ArrowLeftIcon width={24} height={24} />
|
||||
<span className="text-sm">Back</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(HeaderBackButton);
|
||||
13
app/app/_components/HeaderTitle/index.tsx
Normal file
13
app/app/_components/HeaderTitle/index.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
};
|
||||
|
||||
const HeaderTitle: React.FC<Props> = ({ title }) => (
|
||||
<h2 className="my-5 font-semibold text-[34px] tracking-[-0.4px] leading-[41px]">
|
||||
{title}
|
||||
</h2>
|
||||
);
|
||||
|
||||
export default React.memo(HeaderTitle);
|
||||
42
app/app/_components/HistoryEmpty/index.tsx
Normal file
42
app/app/_components/HistoryEmpty/index.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import useCreateConversation from "@/_hooks/useCreateConversation";
|
||||
import { executeSerial } from "@/_services/pluginService";
|
||||
import Image from "next/image";
|
||||
import React from "react";
|
||||
import { ModelManagementService } from "../../../shared/coreService";
|
||||
|
||||
const HistoryEmpty: React.FC = () => {
|
||||
const { requestCreateConvo } = useCreateConversation();
|
||||
const startChat = async () => {
|
||||
const downloadedModels = await executeSerial(
|
||||
ModelManagementService.GET_DOWNLOADED_MODELS
|
||||
);
|
||||
if (!downloadedModels || downloadedModels.length === 0) {
|
||||
alert(
|
||||
"Seems like there is no model downloaded yet. Please download a model first."
|
||||
);
|
||||
} else {
|
||||
requestCreateConvo(1);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="mt-5 flex flex-col w-full h-full items-center justify-center gap-4">
|
||||
<Image
|
||||
src={"icons/chats-circle-light.svg"}
|
||||
width={50}
|
||||
height={50}
|
||||
alt=""
|
||||
/>
|
||||
<p className="text-sm leading-5 text-center text-[#9CA3AF]">
|
||||
Its empty here
|
||||
</p>
|
||||
<button
|
||||
onClick={startChat}
|
||||
className="bg-[#1F2A37] py-[10px] px-5 gap-2 rounded-[8px] text-[14px] font-medium leading-[21px] text-white"
|
||||
>
|
||||
Let's chat
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(HistoryEmpty);
|
||||
@ -7,15 +7,14 @@ import {
|
||||
setActiveConvoIdAtom,
|
||||
} from "@/_helpers/JotaiWrapper";
|
||||
import { useAtomValue, useSetAtom } from "jotai";
|
||||
import { ProductType } from "@/_models/Product";
|
||||
import Image from "next/image";
|
||||
import { Conversation } from "@/_models/Conversation";
|
||||
|
||||
type Props = {
|
||||
conversation: Conversation;
|
||||
avatarUrl: string;
|
||||
avatarUrl?: string;
|
||||
name: string;
|
||||
updatedAt?: number;
|
||||
updatedAt?: string;
|
||||
};
|
||||
|
||||
const HistoryItem: React.FC<Props> = ({
|
||||
@ -40,15 +39,8 @@ const HistoryItem: React.FC<Props> = ({
|
||||
: "bg-white dark:bg-gray-500";
|
||||
|
||||
let rightImageUrl: string | undefined;
|
||||
if (conversationStates[conversation.id]?.waitingForResponse === true) {
|
||||
rightImageUrl = "/icons/loading.svg";
|
||||
} else if (
|
||||
conversation &&
|
||||
conversation.product.type === ProductType.GenerativeArt &&
|
||||
conversation.lastImageUrl &&
|
||||
conversation.lastImageUrl.trim().startsWith("https://")
|
||||
) {
|
||||
rightImageUrl = conversation.lastImageUrl;
|
||||
if (conversationStates[conversation.id ?? ""]?.waitingForResponse === true) {
|
||||
rightImageUrl = "icons/loading.svg";
|
||||
}
|
||||
|
||||
return (
|
||||
@ -59,7 +51,7 @@ const HistoryItem: React.FC<Props> = ({
|
||||
<Image
|
||||
width={36}
|
||||
height={36}
|
||||
src={avatarUrl}
|
||||
src={avatarUrl ?? "icons/app_icon.svg"}
|
||||
className="w-9 aspect-square rounded-full"
|
||||
alt=""
|
||||
/>
|
||||
@ -67,13 +59,18 @@ const HistoryItem: React.FC<Props> = ({
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<span className="text-gray-900 text-left">{name}</span>
|
||||
<span className="text-[11px] leading-[13px] tracking-[-0.4px] text-gray-400">
|
||||
{updatedAt && displayDate(updatedAt)}
|
||||
{updatedAt && updatedAt}
|
||||
</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?.lastTextMessage || <br className="h-5 block" />}
|
||||
{conversation?.message ?? (
|
||||
<span>
|
||||
No new message
|
||||
<br className="h-5 block" />
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<>
|
||||
@ -2,11 +2,13 @@ import HistoryItem from "../HistoryItem";
|
||||
import { useEffect, useState } from "react";
|
||||
import ExpandableHeader from "../ExpandableHeader";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { userConversationsAtom } from "@/_helpers/JotaiWrapper";
|
||||
import { searchAtom, userConversationsAtom } from "@/_helpers/JotaiWrapper";
|
||||
import useGetUserConversations from "@/_hooks/useGetUserConversations";
|
||||
import HistoryEmpty from "../HistoryEmpty";
|
||||
|
||||
const HistoryList: React.FC = () => {
|
||||
const conversations = useAtomValue(userConversationsAtom);
|
||||
const searchText = useAtomValue(searchAtom);
|
||||
const [expand, setExpand] = useState<boolean>(true);
|
||||
const { getUserConversations } = useGetUserConversations();
|
||||
|
||||
@ -24,15 +26,25 @@ const HistoryList: React.FC = () => {
|
||||
<div
|
||||
className={`flex flex-col gap-1 mt-1 ${!expand ? "hidden " : "block"}`}
|
||||
>
|
||||
{conversations.map((convo) => (
|
||||
<HistoryItem
|
||||
key={convo.id}
|
||||
conversation={convo}
|
||||
avatarUrl={convo.product.avatarUrl}
|
||||
name={convo.product.name}
|
||||
updatedAt={convo.updatedAt}
|
||||
/>
|
||||
))}
|
||||
{conversations.length > 0 ? (
|
||||
conversations
|
||||
.filter(
|
||||
(e) =>
|
||||
searchText.trim() === "" ||
|
||||
e.name?.toLowerCase().includes(searchText.toLowerCase().trim())
|
||||
)
|
||||
.map((convo) => (
|
||||
<HistoryItem
|
||||
key={convo.id}
|
||||
conversation={convo}
|
||||
avatarUrl={convo.image}
|
||||
name={convo.name || "Jan"}
|
||||
updatedAt={convo.updated_at ?? ""}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<HistoryEmpty/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -10,8 +10,8 @@ const JanLogo: React.FC = () => {
|
||||
className="p-3 flex gap-[2px] items-center"
|
||||
onClick={() => setActiveConvoId(undefined)}
|
||||
>
|
||||
<Image src={"/icons/app_icon.svg"} width={28} height={28} alt="" />
|
||||
<Image src={"/icons/Jan.svg"} width={27} height={12} alt="" />
|
||||
<Image src={"icons/app_icon.svg"} width={28} height={28} alt="" />
|
||||
<Image src={"icons/Jan.svg"} width={27} height={12} alt="" />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@ -11,7 +11,7 @@ const JanWelcomeTitle: React.FC<Props> = ({ title, description }) => (
|
||||
<h2 className="text-[22px] leading-7 font-bold">{title}</h2>
|
||||
<span className="flex items-center text-xs leading-[18px]">
|
||||
Operated by
|
||||
<Image src={"/icons/ico_logo.svg"} width={42} height={22} alt="" />
|
||||
<Image src={"icons/ico_logo.svg"} width={42} height={22} alt="" />
|
||||
</span>
|
||||
<span className="text-sm text-center font-normal">{description}</span>
|
||||
</div>
|
||||
16
app/app/_components/LeftContainer2/index.tsx
Normal file
16
app/app/_components/LeftContainer2/index.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import React from "react";
|
||||
import SidebarFooter from "../SidebarFooter";
|
||||
import SidebarHeader from "../SidebarHeader";
|
||||
import SidebarMenu from "../SidebarMenu";
|
||||
import SidebarEmptyHistory from "../SidebarEmptyHistory";
|
||||
|
||||
const LeftContainer2: React.FC = () => (
|
||||
<div className="w-[323px] flex-shrink-0 p-3 h-screen border-r border-gray-200 flex flex-col">
|
||||
<SidebarHeader />
|
||||
<SidebarEmptyHistory />
|
||||
<SidebarMenu />
|
||||
<SidebarFooter />
|
||||
</div>
|
||||
);
|
||||
|
||||
export default React.memo(LeftContainer2);
|
||||
@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import SearchBar from "../SearchBar";
|
||||
import ShortcutList from "../ShortcutList";
|
||||
// import ShortcutList from "../ShortcutList";
|
||||
import HistoryList from "../HistoryList";
|
||||
import DiscordContainer from "../DiscordContainer";
|
||||
import JanLogo from "../JanLogo";
|
||||
@ -10,7 +10,7 @@ const LeftSidebar: React.FC = () => (
|
||||
<JanLogo />
|
||||
<div className="flex flex-col flex-1 gap-3 overflow-x-hidden">
|
||||
<SearchBar />
|
||||
<ShortcutList />
|
||||
{/* <ShortcutList /> */}
|
||||
<HistoryList />
|
||||
</div>
|
||||
<DiscordContainer />
|
||||
20
app/app/_components/LoadingIndicator.tsx
Normal file
20
app/app/_components/LoadingIndicator.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
const LoadingIndicator = () => {
|
||||
let circleCommonClasses = "h-1.5 w-1.5 bg-current rounded-full";
|
||||
|
||||
return (
|
||||
// <div className="flex">
|
||||
// <div className={`${circleCommonClasses} mr-1 animate-bounce`}></div>
|
||||
// <div className={`${circleCommonClasses} mr-1 animate-bounce200`}></div>
|
||||
// <div className={`${circleCommonClasses} animate-bounce400`}></div>
|
||||
// </div>
|
||||
<div className="typingIndicatorContainer">
|
||||
<div className="typingIndicatorBubble">
|
||||
<div className="typingIndicatorBubbleDot"></div>
|
||||
<div className="typingIndicatorBubbleDot"></div>
|
||||
<div className="typingIndicatorBubbleDot"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoadingIndicator;
|
||||
24
app/app/_components/LoginButton/index.tsx
Normal file
24
app/app/_components/LoginButton/index.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
"use client";
|
||||
|
||||
const LoginButton: React.FC = () => {
|
||||
// const { signInWithKeyCloak } = useSignIn();
|
||||
// const { user, loading } = useGetCurrentUser();
|
||||
|
||||
// if (loading || user) {
|
||||
// return <div />;
|
||||
// }
|
||||
// return (
|
||||
// <div className="hidden lg:block">
|
||||
// <button
|
||||
// onClick={signInWithKeyCloak}
|
||||
// type="button"
|
||||
// className="rounded-md bg-indigo-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
|
||||
// >
|
||||
// Login
|
||||
// </button>
|
||||
// </div>
|
||||
// );
|
||||
return <div />;
|
||||
};
|
||||
|
||||
export default LoginButton;
|
||||
53
app/app/_components/MenuHeader/index.tsx
Normal file
53
app/app/_components/MenuHeader/index.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import Link from "next/link";
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
import { Fragment } from "react";
|
||||
// import useGetCurrentUser from "@/_hooks/useGetCurrentUser";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { showConfirmSignOutModalAtom } from "@/_helpers/JotaiWrapper";
|
||||
|
||||
export const MenuHeader: React.FC = () => {
|
||||
const setShowConfirmSignOutModal = useSetAtom(showConfirmSignOutModalAtom);
|
||||
// const { user } = useGetCurrentUser();
|
||||
|
||||
return <div></div>;
|
||||
|
||||
// return (
|
||||
// <Transition
|
||||
// as={Fragment}
|
||||
// enter="transition ease-out duration-200"
|
||||
// enterFrom="opacity-0 translate-y-1"
|
||||
// enterTo="opacity-100 translate-y-0"
|
||||
// leave="transition ease-in duration-150"
|
||||
// leaveFrom="opacity-100 translate-y-0"
|
||||
// leaveTo="opacity-0 translate-y-1"
|
||||
// >
|
||||
// <Popover.Panel className="absolute shadow-profile -right-2 top-full z-10 mt-3 w-[224px] overflow-hidden rounded-[6px] bg-white shadow-lg ring-1 ring-gray-200">
|
||||
// <div className="py-3 px-4 gap-2 flex flex-col">
|
||||
// <h2 className="text-[20px] leading-[25px] tracking-[-0.4px] font-bold text-[#111928]">
|
||||
// {user.displayName}
|
||||
// </h2>
|
||||
// <span className="text-[#6B7280] leading-[17.5px] text-sm">
|
||||
// {user.email}
|
||||
// </span>
|
||||
// </div>
|
||||
// <hr />
|
||||
// <button
|
||||
// onClick={() => setShowConfirmSignOutModal(true)}
|
||||
// className="px-4 py-3 text-sm w-full text-left text-gray-700"
|
||||
// >
|
||||
// Sign Out
|
||||
// </button>
|
||||
// <hr />
|
||||
// <div className="flex gap-2 px-4 py-2 justify-center items-center">
|
||||
// <Link href="/privacy">
|
||||
// <span className="text-[#6B7280] text-xs">Privacy</span>
|
||||
// </Link>
|
||||
// <div className="w-1 h-1 bg-[#D9D9D9] rounded-lg" />
|
||||
// <Link href="/support">
|
||||
// <span className="text-[#6B7280] text-xs">Support</span>
|
||||
// </Link>
|
||||
// </div>
|
||||
// </Popover.Panel>
|
||||
// </Transition>
|
||||
// );
|
||||
};
|
||||
@ -6,7 +6,7 @@ const MobileDownload = () => {
|
||||
<div className="flex items-center flex-col box-border rounded-lg border border-gray-200 p-4 bg-[#F9FAFB] mb-3">
|
||||
{/** Jan logo */}
|
||||
<Image
|
||||
src="/icons/janai_logo.svg"
|
||||
src="icons/janai_logo.svg"
|
||||
alt={""}
|
||||
width={32}
|
||||
height={32}
|
||||
@ -28,7 +28,7 @@ const MobileDownload = () => {
|
||||
>
|
||||
<div className="flex box-border h-11 rounded-md bg-gray-300 p-2 items-center hover:bg-gray-200 focus:bg-gray-600">
|
||||
<Image
|
||||
src="/icons/social_icon_apple.svg"
|
||||
src="icons/social_icon_apple.svg"
|
||||
alt={""}
|
||||
width={26}
|
||||
height={26}
|
||||
@ -49,7 +49,7 @@ const MobileDownload = () => {
|
||||
>
|
||||
<div className="flex box-border h-11 rounded-md bg-gray-300 p-2 items-center hover:bg-gray-200 focus:bg-gray-600">
|
||||
<Image
|
||||
src="/icons/google_play_logo.svg"
|
||||
src="icons/google_play_logo.svg"
|
||||
alt={""}
|
||||
width={26}
|
||||
height={26}
|
||||
@ -5,7 +5,7 @@ const MobileInstallPane: React.FC = () => {
|
||||
<div className="p-4 rounded-[8px] border-[1px] border-[#E5E7EB] bg-[#F9FAFB]">
|
||||
<div className="flex flex-col gap-5 items-center">
|
||||
<div className="flex flex-col items-center text-[12px]">
|
||||
<Image src={"/icons/app_icon.svg"} width={32} height={32} alt="" />
|
||||
<Image src={"icons/app_icon.svg"} width={32} height={32} alt="" />
|
||||
<h2 className="font-bold leading-[12px] text-center">Jan Mobie</h2>
|
||||
<p className="leading-[18px] text-center">
|
||||
Stay up to date and move work forward with Jan on iOS & Android.
|
||||
@ -15,7 +15,7 @@ const MobileInstallPane: React.FC = () => {
|
||||
</div>
|
||||
<div className="flex justify-between items-center gap-3">
|
||||
<div className="bg-[#E5E7EB] rounded-[8px] gap-3 p-2 flex items-center">
|
||||
<Image src={"/icons/apple.svg"} width={26} height={26} alt="" />
|
||||
<Image src={"icons/apple.svg"} width={26} height={26} alt="" />
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[8px] leading-[12px]">Download on the</span>
|
||||
<h2 className="font-bold text-[12px] leading-[15px]">AppStore</h2>
|
||||
@ -23,7 +23,7 @@ const MobileInstallPane: React.FC = () => {
|
||||
</div>
|
||||
<div className="bg-[#E5E7EB] rounded-[8px] gap-3 p-2 flex items-center">
|
||||
<Image
|
||||
src={"/icons/googleplay.svg"}
|
||||
src={"icons/googleplay.svg"}
|
||||
width={26}
|
||||
height={26}
|
||||
alt=""
|
||||
@ -25,7 +25,7 @@ const MobileMenuPane: React.FC = () => {
|
||||
className="h-8 w-auto"
|
||||
width={32}
|
||||
height={32}
|
||||
src="/icons/app_icon.svg"
|
||||
src="icons/app_icon.svg"
|
||||
alt=""
|
||||
/>
|
||||
</a>
|
||||
@ -6,7 +6,7 @@ const MobileShowcase = () => {
|
||||
return (
|
||||
<div className="md:hidden flex flex-col px-5 mt-10 items-center justify-center w-full gap-10">
|
||||
<Image
|
||||
src="/images/mobile.jpg"
|
||||
src="images/mobile.jpg"
|
||||
width={638}
|
||||
height={892}
|
||||
alt="mobile"
|
||||
@ -15,7 +15,7 @@ const MobileShowcase = () => {
|
||||
/>
|
||||
<div className="flex flex-col items-center justify-center mb-20">
|
||||
<Image
|
||||
src="/icons/app_icon.svg"
|
||||
src="icons/app_icon.svg"
|
||||
width={200}
|
||||
height={200}
|
||||
className="w-[10%]"
|
||||
@ -35,7 +35,7 @@ const MobileShowcase = () => {
|
||||
>
|
||||
<div className="flex box-border h-11 rounded-md bg-gray-300 p-2 items-center hover:bg-gray-200 focus:bg-gray-600">
|
||||
<Image
|
||||
src="/icons/social_icon_apple.svg"
|
||||
src="icons/social_icon_apple.svg"
|
||||
alt={""}
|
||||
width={26}
|
||||
height={26}
|
||||
@ -55,7 +55,7 @@ const MobileShowcase = () => {
|
||||
>
|
||||
<div className="flex box-border h-11 rounded-md bg-gray-300 p-2 items-center hover:bg-gray-200 focus:bg-gray-600">
|
||||
<Image
|
||||
src="/icons/google_play_logo.svg"
|
||||
src="icons/google_play_logo.svg"
|
||||
alt={""}
|
||||
width={26}
|
||||
height={26}
|
||||
21
app/app/_components/ModelDownloadButton/index.tsx
Normal file
21
app/app/_components/ModelDownloadButton/index.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { ArrowDownTrayIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
type Props = {
|
||||
callback: () => void;
|
||||
};
|
||||
|
||||
const ModelDownloadButton: React.FC<Props> = ({ callback }) => {
|
||||
return (
|
||||
<button
|
||||
className="bg-[#1A56DB] rounded-lg py-2 px-3 flex items-center gap-2"
|
||||
onClick={callback}
|
||||
>
|
||||
<ArrowDownTrayIcon width={16} height={16} color="#FFFFFF" />
|
||||
<span className="text-xs leading-[18px] text-[#fff] font-medium">
|
||||
Download
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelDownloadButton;
|
||||
33
app/app/_components/ModelDownloadingButton/index.tsx
Normal file
33
app/app/_components/ModelDownloadingButton/index.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
type Props = {
|
||||
total: number;
|
||||
value: number;
|
||||
};
|
||||
|
||||
const ModelDownloadingButton: React.FC<Props> = ({ total, value }) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<button className="py-2 px-3 flex gap-2 border text-xs leading-[18px] border-gray-200 rounded-lg">
|
||||
Downloading...
|
||||
</button>
|
||||
<div className="py-[2px] px-[10px] bg-gray-200 rounded">
|
||||
<span className="text-xs font-medium text-gray-800">
|
||||
{toGigabytes(value)} / {toGigabytes(total)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const toGigabytes = (input: number) => {
|
||||
if (input > 1024 ** 3) {
|
||||
return (input / 1000 ** 3).toFixed(2) + "GB";
|
||||
} else if (input > 1024 ** 2) {
|
||||
return (input / 1000 ** 2).toFixed(2) + "MB";
|
||||
} else if (input > 1024) {
|
||||
return (input / 1000).toFixed(2) + "KB";
|
||||
} else {
|
||||
return input + "B";
|
||||
}
|
||||
};
|
||||
|
||||
export default ModelDownloadingButton;
|
||||
@ -32,7 +32,7 @@ const ModelInfo: React.FC<Props> = ({
|
||||
</span>
|
||||
</div>
|
||||
<button className="px-3 py-2 bg-[#1F2A37] flex gap-2 items-center rounded-lg">
|
||||
<Image src={"/icons/code.svg"} width={16} height={17} alt="" />
|
||||
<Image src={"icons/code.svg"} width={16} height={17} alt="" />
|
||||
<span className="text-white text-sm font-medium">Get API Key</span>
|
||||
</button>
|
||||
</div>
|
||||
136
app/app/_components/ModelListContainer/index.tsx
Normal file
136
app/app/_components/ModelListContainer/index.tsx
Normal file
@ -0,0 +1,136 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import DownloadModelCard from "../DownloadModelCard";
|
||||
import { executeSerial } from "@/_services/pluginService";
|
||||
import { ModelManagementService } from "../../../shared/coreService";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { modelDownloadStateAtom } from "@/_helpers/JotaiWrapper";
|
||||
|
||||
const ModelListContainer: React.FC = () => {
|
||||
const [downloadedModels, setDownloadedModels] = useState<string[]>([]);
|
||||
const downloadState = useAtomValue(modelDownloadStateAtom);
|
||||
const DownloadedModel = {
|
||||
title: "Downloaded Model",
|
||||
data: [
|
||||
{
|
||||
name: "Llama 2 7B Chat - GGML",
|
||||
type: "7B",
|
||||
author: "The Bloke",
|
||||
description:
|
||||
"Primary intended uses The primary use of LLaMA is research on large language models, including: exploring potential applications such as question answering, natural language understanding or reading comprehension, understanding capabilities and limitations of current language models, and developing techniques to improve those, evaluating and mitigating biases, risks, toxic and harmful content generations, hallucinations.",
|
||||
isRecommend: true,
|
||||
storage: 3780,
|
||||
default: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const BrowseAvailableModels = {
|
||||
title: "Browse Available Models",
|
||||
data: [
|
||||
{
|
||||
name: "Llama 2 7B Chat - GGML",
|
||||
type: "7B",
|
||||
author: "The Bloke",
|
||||
description:
|
||||
"Primary intended uses The primary use of LLaMA is research on large language models, including: exploring potential applications such as question answering, natural language understanding or reading comprehension, understanding capabilities and limitations of current language models, and developing techniques to improve those, evaluating and mitigating biases, risks, toxic and harmful content generations, hallucinations.",
|
||||
isRecommend: true,
|
||||
storage: 3780,
|
||||
default: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const getDownloadedModels = async () => {
|
||||
const modelNames = await executeSerial(
|
||||
ModelManagementService.GET_DOWNLOADED_MODELS,
|
||||
);
|
||||
setDownloadedModels(modelNames);
|
||||
};
|
||||
getDownloadedModels();
|
||||
}, []);
|
||||
|
||||
const onDeleteClick = async () => {
|
||||
// TODO: for now we only support 1 model
|
||||
if (downloadedModels?.length < 1) {
|
||||
return;
|
||||
}
|
||||
console.log(downloadedModels[0]);
|
||||
const pathArray = downloadedModels[0].split("/");
|
||||
const modelName = pathArray[pathArray.length - 1];
|
||||
console.log(`Prepare to delete ${modelName}`);
|
||||
// setShow(true); // TODO: later
|
||||
await executeSerial(ModelManagementService.DELETE_MODEL, modelName);
|
||||
|
||||
setDownloadedModels([]);
|
||||
};
|
||||
|
||||
const initModel = async () => {
|
||||
const product = {
|
||||
name: "LLama 2 7B Chat",
|
||||
fileName: "llama-2-7b-chat.gguf.q4_0.bin",
|
||||
downloadUrl:
|
||||
"https://huggingface.co/TheBloke/Llama-2-7b-Chat-GGUF/resolve/main/llama-2-7b-chat.Q4_0.gguf",
|
||||
};
|
||||
await executeSerial(ModelManagementService.INIT_MODEL, product);
|
||||
};
|
||||
|
||||
const onDownloadClick = async () => {
|
||||
const url =
|
||||
"https://huggingface.co/TheBloke/Llama-2-7b-Chat-GGUF/resolve/main/llama-2-7b-chat.Q4_0.gguf";
|
||||
await executeSerial(ModelManagementService.DOWNLOAD_MODEL, url);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="pb-5 flex flex-col gap-2">
|
||||
<Title title={DownloadedModel.title} />
|
||||
{downloadedModels?.length > 0 &&
|
||||
DownloadedModel.data.map((item, index) => (
|
||||
<DownloadModelCard
|
||||
key={index}
|
||||
{...item}
|
||||
installed={true}
|
||||
onInitClick={initModel}
|
||||
onDeleteClick={onDeleteClick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="pb-5 flex flex-col gap-2">
|
||||
{downloadedModels.length === 0 && (
|
||||
<>
|
||||
<Title title={BrowseAvailableModels.title} />
|
||||
{BrowseAvailableModels.data.map((item, index) => (
|
||||
<DownloadModelCard
|
||||
key={index}
|
||||
{...item}
|
||||
downloading={downloadState == null}
|
||||
total={downloadState?.size.total ?? 0}
|
||||
transferred={downloadState?.size.transferred ?? 0}
|
||||
onDownloadClick={onDownloadClick}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
};
|
||||
|
||||
const Title: React.FC<Props> = ({ title }) => {
|
||||
return (
|
||||
<div className="flex gap-[10px]">
|
||||
<span className="font-semibold text-xl leading-[25px] tracking-[-0.4px]">
|
||||
{title}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelListContainer;
|
||||
15
app/app/_components/ModelManagement/index.tsx
Normal file
15
app/app/_components/ModelManagement/index.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import HeaderBackButton from "../HeaderBackButton";
|
||||
import HeaderTitle from "../HeaderTitle";
|
||||
import ModelListContainer from "../ModelListContainer";
|
||||
import ModelSearchBar from "../ModelSearchBar";
|
||||
|
||||
export default function ModelManagement() {
|
||||
return (
|
||||
<main className="pt-[30px] pr-[89px] pl-[60px] pb-[70px] flex-1">
|
||||
{/* <HeaderBackButton /> */}
|
||||
<HeaderTitle title="Explore Models" />
|
||||
<ModelSearchBar />
|
||||
<ModelListContainer />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@ -11,16 +11,15 @@ import { PlusIcon, TrashIcon } from "@heroicons/react/24/outline";
|
||||
import useCreateConversation from "@/_hooks/useCreateConversation";
|
||||
|
||||
const ModelMenu: React.FC = () => {
|
||||
const currentProduct = useAtomValue(currentProductAtom);
|
||||
const [active, setActive] = useAtom(showingProductDetailAtom);
|
||||
// const currentProduct = useAtomValue(currentProductAtom);
|
||||
// const [active, setActive] = useAtom(showingProductDetailAtom);
|
||||
const { requestCreateConvo } = useCreateConversation();
|
||||
const setShowConfirmDeleteConversationModal = useSetAtom(
|
||||
showConfirmDeleteConversationModalAtom
|
||||
);
|
||||
|
||||
const onCreateConvoClick = () => {
|
||||
if (!currentProduct) return;
|
||||
requestCreateConvo(currentProduct, true);
|
||||
requestCreateConvo(1);
|
||||
};
|
||||
|
||||
return (
|
||||
@ -31,14 +30,14 @@ const ModelMenu: React.FC = () => {
|
||||
<button onClick={() => setShowConfirmDeleteConversationModal(true)}>
|
||||
<TrashIcon width={24} height={24} color="#9CA3AF" />
|
||||
</button>
|
||||
<button onClick={() => setActive(!active)}>
|
||||
{/* <button onClick={() => setActive(!active)}>
|
||||
<Image
|
||||
src={active ? "/icons/ic_sidebar_fill.svg" : "/icons/ic_sidebar.svg"}
|
||||
src={active ? "icons/ic_sidebar_fill.svg" : "icons/ic_sidebar.svg"}
|
||||
width={24}
|
||||
height={24}
|
||||
alt=""
|
||||
/>
|
||||
</button>
|
||||
</button> */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
25
app/app/_components/ModelSearchBar/index.tsx
Normal file
25
app/app/_components/ModelSearchBar/index.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
||||
import { useState } from "react";
|
||||
|
||||
const ModelSearchBar: React.FC = () => {
|
||||
const [text, setText] = useState("");
|
||||
return (
|
||||
<div className="py-[27px] flex items-center justify-center">
|
||||
<div className="w-[520px] h-[42px] flex items-center">
|
||||
<input
|
||||
className="outline-none bg-gray-300 text-sm h-full rounded-tl-lg rounded-bl-lg leading-[17.5px] border border-gray-300 py-3 px-4 flex-1"
|
||||
placeholder="Search model"
|
||||
value={text}
|
||||
onChange={(text) => setText(text.currentTarget.value)}
|
||||
/>
|
||||
<button className="flex items-center justify-center bg-gray-800 border border-gray-800 p-2 w-[42px] rounded-tr-lg rounded-br-lg h-[42px]">
|
||||
<MagnifyingGlassIcon width={20} height={20} color="#FFFFFF" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelSearchBar;
|
||||
@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useAtomValue } from "jotai";
|
||||
import TryItYourself from "./TryItYourself";
|
||||
import React from "react";
|
||||
import { currentProductAtom } from "@/_helpers/JotaiWrapper";
|
||||
|
||||
@ -26,7 +25,6 @@ const OverviewPane: React.FC = () => {
|
||||
{product?.modelUrl}
|
||||
</a>
|
||||
</div>
|
||||
<TryItYourself />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
286
app/app/_components/Preferences.tsx
Normal file
286
app/app/_components/Preferences.tsx
Normal file
@ -0,0 +1,286 @@
|
||||
"use client";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
setup,
|
||||
plugins,
|
||||
extensionPoints,
|
||||
activationPoints,
|
||||
} from "../../electron/core/plugin-manager/execution/index";
|
||||
|
||||
import {
|
||||
ChartPieIcon,
|
||||
CommandLineIcon,
|
||||
HomeIcon,
|
||||
PlayIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
|
||||
import { MagnifyingGlassIcon } from "@heroicons/react/20/solid";
|
||||
import classNames from "classnames";
|
||||
import Link from "next/link";
|
||||
const navigation = [
|
||||
{ name: "Plugin Manager", href: "#", icon: ChartPieIcon, current: true },
|
||||
];
|
||||
|
||||
/* eslint-disable @next/next/no-sync-scripts */
|
||||
export const Preferences = () => {
|
||||
const [search, setSearch] = useState<string>("");
|
||||
const [activePlugins, setActivePlugins] = useState<any[]>([]);
|
||||
|
||||
const preferenceRef = useRef(null);
|
||||
useEffect(() => {
|
||||
async function setupPE() {
|
||||
// Enable activation point management
|
||||
setup({
|
||||
//@ts-ignore
|
||||
importer: (plugin) =>
|
||||
import(/* webpackIgnore: true */ plugin).catch((err) => {
|
||||
console.log(err);
|
||||
}),
|
||||
});
|
||||
|
||||
// Register all active plugins with their activation points
|
||||
await plugins.registerActive();
|
||||
}
|
||||
|
||||
const activePlugins = async () => {
|
||||
const plgs = await plugins.getActive();
|
||||
setActivePlugins(plgs);
|
||||
// Activate alls
|
||||
setTimeout(async () => {
|
||||
await activationPoints.trigger("init");
|
||||
if (extensionPoints.get("experimentComponent")) {
|
||||
const components = await Promise.all(
|
||||
extensionPoints.execute("experimentComponent")
|
||||
);
|
||||
components.forEach((e) => {
|
||||
if (preferenceRef.current) {
|
||||
// @ts-ignore
|
||||
preferenceRef.current.appendChild(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 500);
|
||||
};
|
||||
setupPE().then(() => activePlugins());
|
||||
}, []);
|
||||
|
||||
// Install a new plugin on clicking the install button
|
||||
const install = async (e: any) => {
|
||||
e.preventDefault();
|
||||
//@ts-ignore
|
||||
const pluginFile = new FormData(e.target).get("plugin-file").path;
|
||||
|
||||
// Send the filename of the to be installed plugin
|
||||
// to the main process for installation
|
||||
const installed = await plugins.install([pluginFile]);
|
||||
if (typeof window !== "undefined") {
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
|
||||
// Uninstall a plugin on clicking uninstall
|
||||
const uninstall = async (name: string) => {
|
||||
//@ts-ignore
|
||||
|
||||
// Send the filename of the to be uninstalled plugin
|
||||
// to the main process for removal
|
||||
//@ts-ignore
|
||||
const res = await plugins.uninstall([name]);
|
||||
console.log(
|
||||
res
|
||||
? "Plugin successfully uninstalled"
|
||||
: "Plugin could not be uninstalled"
|
||||
);
|
||||
};
|
||||
|
||||
// Update all plugins on clicking update plugins
|
||||
const update = async (plugin: string) => {
|
||||
if (typeof window !== "undefined") {
|
||||
// @ts-ignore
|
||||
await window.pluggableElectronIpc.update([plugin], true);
|
||||
}
|
||||
// plugins.update(active.map((plg) => plg.name));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full h-screen overflow-scroll">
|
||||
{/* Static sidebar for desktop */}
|
||||
<div className="fixed inset-y-0 z-50 flex w-72 flex-col">
|
||||
{/* Sidebar component, swap this element with another sidebar if you like */}
|
||||
<div className="flex grow flex-col gap-y-5 overflow-y-auto bg-gray-900 px-6 pb-4">
|
||||
<div className="flex h-16 shrink-0 items-center">
|
||||
<Link href="/">
|
||||
<img
|
||||
className="h-8 w-auto"
|
||||
src="icons/app_icon.svg"
|
||||
alt="Your Company"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<nav className="flex flex-1 flex-col">
|
||||
<ul role="list" className="flex flex-1 flex-col gap-y-7">
|
||||
<li>
|
||||
<ul role="list" className="-mx-2 space-y-1">
|
||||
{navigation.map((item) => (
|
||||
<li key={item.name}>
|
||||
<a
|
||||
href={item.href}
|
||||
className={classNames(
|
||||
item.current
|
||||
? "bg-gray-800 text-white"
|
||||
: "text-gray-400 hover:text-white hover:bg-gray-800",
|
||||
"group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold"
|
||||
)}
|
||||
>
|
||||
<item.icon
|
||||
className="h-6 w-6 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{item.name}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li className="mt-auto">
|
||||
<a
|
||||
href="/"
|
||||
className="group -mx-2 flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 text-gray-400 hover:bg-gray-800 hover:text-white"
|
||||
>
|
||||
<HomeIcon className="h-6 w-6 shrink-0" aria-hidden="true" />
|
||||
Home
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pl-72 w-full">
|
||||
<div className="sticky top-0 z-40 flex h-16 shrink-0 items-center gap-x-4 border-b border-gray-200 bg-white shadow-sm sm:gap-x-6 sm:px-6 px-8">
|
||||
{/* Separator */}
|
||||
<div className="h-6 w-px bg-gray-900/10 hidden" aria-hidden="true" />
|
||||
|
||||
<div className="flex flex-1 self-stretch gap-x-6">
|
||||
<form className="relative flex flex-1" action="#" method="GET">
|
||||
<label htmlFor="search-field" className="sr-only">
|
||||
Search
|
||||
</label>
|
||||
<MagnifyingGlassIcon
|
||||
className="pointer-events-none absolute inset-y-0 left-0 h-full w-5 text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<input
|
||||
defaultValue={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
id="search-field"
|
||||
className="block h-full w-full border-0 py-0 pl-8 pr-0 text-gray-900 placeholder:text-gray-400 focus:ring-0 sm:text-sm"
|
||||
placeholder="Search..."
|
||||
type="search"
|
||||
name="search"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main className="py-5">
|
||||
<div className="sm:px-6 px-8">
|
||||
{/* Content */}
|
||||
<div className="flex flex-row items-center my-4">
|
||||
<ChartPieIcon width={30} />
|
||||
Install Plugin
|
||||
</div>
|
||||
<form id="plugin-file" onSubmit={install}>
|
||||
<div className="flex flex-row items-center space-x-10">
|
||||
<div className="flex items-center justify-center w-[300px]">
|
||||
<label className="flex flex-col items-center justify-center w-full border-2 border-gray-300 border-dashed rounded-lg cursor-pointer bg-gray-50 dark:hover:bg-bray-800 dark:bg-gray-700 hover:bg-gray-100 dark:border-gray-600 dark:hover:border-gray-500 dark:hover:bg-gray-600">
|
||||
<div className="flex flex-col items-center justify-center pt-5 pb-6">
|
||||
<p className="mb-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
<span className="font-semibold">Click to upload</span>{" "}
|
||||
or drag and drop
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
TGZ (MAX 50MB)
|
||||
</p>
|
||||
</div>
|
||||
<input
|
||||
id="dropzone-file"
|
||||
name="plugin-file"
|
||||
type="file"
|
||||
className="hidden"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className="rounded-md bg-indigo-600 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
|
||||
>
|
||||
Install Plugin
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="flex flex-row items-center my-4">
|
||||
<CommandLineIcon width={30} />
|
||||
Installed Plugins
|
||||
</div>
|
||||
<div className="flex flex-wrap">
|
||||
{activePlugins
|
||||
.filter(
|
||||
(e) =>
|
||||
search.trim() === "" ||
|
||||
e.name.toLowerCase().includes(search.toLowerCase())
|
||||
)
|
||||
.map((e) => (
|
||||
<div key={e.name} className="m-2">
|
||||
<a
|
||||
href="#"
|
||||
className="block max-w-sm p-6 bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700"
|
||||
>
|
||||
<h5 className="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white">
|
||||
{e.name}
|
||||
</h5>
|
||||
<p className="font-normal text-gray-700 dark:text-gray-400">
|
||||
Activation: {e.activationPoints}
|
||||
</p>
|
||||
<p className="font-normal text-gray-700 dark:text-gray-400">
|
||||
Url: {e.url}
|
||||
</p>
|
||||
<div className="flex flex-row space-x-5">
|
||||
<button
|
||||
type="submit"
|
||||
onClick={() => {
|
||||
uninstall(e.name);
|
||||
}}
|
||||
className="mt-5 rounded-md bg-red-600 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600"
|
||||
>
|
||||
Uninstall
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
onClick={() => {
|
||||
update(e.name);
|
||||
}}
|
||||
className="mt-5 rounded-md bg-indigo-600 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
|
||||
>
|
||||
Update
|
||||
</button>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-row items-center my-4">
|
||||
<PlayIcon width={30} />
|
||||
Test Plugins
|
||||
</div>
|
||||
<div className="h-full w-full" ref={preferenceRef}></div>
|
||||
{/* Content */}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
10
app/app/_components/ProductOverview/index.tsx
Normal file
10
app/app/_components/ProductOverview/index.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import React from "react";
|
||||
import useGetModels from "@/_hooks/useGetModels";
|
||||
|
||||
const ProductOverview: React.FC = () => {
|
||||
const { models } = useGetModels();
|
||||
|
||||
return <div className="bg-gray-100 overflow-y-auto flex-grow scroll"></div>;
|
||||
};
|
||||
|
||||
export default ProductOverview;
|
||||
@ -1,6 +1,4 @@
|
||||
import JanWelcomeTitle from "../JanWelcomeTitle";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { GetProductPromptsDocument, GetProductPromptsQuery } from "@/graphql";
|
||||
import { Product } from "@/_models/Product";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { currentPromptAtom } from "@/_helpers/JotaiWrapper";
|
||||
@ -11,9 +9,7 @@ type Props = {
|
||||
|
||||
const SampleLlmContainer: React.FC<Props> = ({ product }) => {
|
||||
const setCurrentPrompt = useSetAtom(currentPromptAtom);
|
||||
const { data } = useQuery<GetProductPromptsQuery>(GetProductPromptsDocument, {
|
||||
variables: { productSlug: product.slug },
|
||||
});
|
||||
const { data } = { data: { prompts: [] } };
|
||||
|
||||
return (
|
||||
<div className="flex flex-col max-w-sm flex-shrink-0 gap-9 items-center pt-6 mx-auto">
|
||||
@ -25,7 +21,7 @@ const SampleLlmContainer: React.FC<Props> = ({ product }) => {
|
||||
<h2 className="font-semibold text-xl leading-6 tracking-[-0.4px] mb-5">
|
||||
Try now
|
||||
</h2>
|
||||
<div className="flex flex-col">
|
||||
{/* <div className="flex flex-col">
|
||||
{data?.prompts.map((item) => (
|
||||
<button
|
||||
onClick={() => setCurrentPrompt(item.content ?? "")}
|
||||
@ -35,7 +31,7 @@ const SampleLlmContainer: React.FC<Props> = ({ product }) => {
|
||||
<span className="line-clamp-3">{item.content}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -3,14 +3,14 @@ import {
|
||||
currentPromptAtom,
|
||||
} from "@/_helpers/JotaiWrapper";
|
||||
import useSendChatMessage from "@/_hooks/useSendChatMessage";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { useAtom, useAtomValue } from "jotai";
|
||||
import Image from "next/image";
|
||||
|
||||
const SendButton: React.FC = () => {
|
||||
const currentPrompt = useAtomValue(currentPromptAtom);
|
||||
const [currentPrompt] = useAtom(currentPromptAtom);
|
||||
const currentConvoState = useAtomValue(currentConvoStateAtom);
|
||||
const { sendChatMessage } = useSendChatMessage();
|
||||
|
||||
const { sendChatMessage } = useSendChatMessage();
|
||||
const isWaitingForResponse = currentConvoState?.waitingForResponse ?? false;
|
||||
const disabled = currentPrompt.trim().length === 0 || isWaitingForResponse;
|
||||
|
||||
@ -29,7 +29,7 @@ const SendButton: React.FC = () => {
|
||||
type="submit"
|
||||
className="p-2 gap-[10px] inline-flex items-center rounded-[12px] text-sm font-semibold shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
|
||||
>
|
||||
<Image src={"/icons/ic_arrowright.svg"} width={24} height={24} alt="" />
|
||||
<Image src={"icons/ic_arrowright.svg"} width={24} height={24} alt="" />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@ -11,7 +11,7 @@ const ShortcutItem: React.FC<Props> = ({ product }) => {
|
||||
const { requestCreateConvo } = useCreateConversation();
|
||||
|
||||
const onClickHandler = () => {
|
||||
requestCreateConvo(product);
|
||||
requestCreateConvo(1);
|
||||
};
|
||||
|
||||
return (
|
||||
@ -12,7 +12,7 @@ const ShowMoreButton: React.FC<Props> = ({ onClick }) => (
|
||||
>
|
||||
Show more
|
||||
<Image
|
||||
src={"/icons/unicorn_angle-down.svg"}
|
||||
src={"icons/unicorn_angle-down.svg"}
|
||||
width={16}
|
||||
height={16}
|
||||
alt=""
|
||||
24
app/app/_components/SidebarButton/index.tsx
Normal file
24
app/app/_components/SidebarButton/index.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import Image from "next/image";
|
||||
|
||||
type Props = {
|
||||
callback?: () => void;
|
||||
className?: string;
|
||||
icon: string;
|
||||
width: number;
|
||||
height: number;
|
||||
title: string;
|
||||
};
|
||||
|
||||
export const SidebarButton: React.FC<Props> = ({
|
||||
callback,
|
||||
height,
|
||||
icon,
|
||||
className,
|
||||
width,
|
||||
title,
|
||||
}) => (
|
||||
<button onClick={callback} className={className}>
|
||||
<Image src={icon} width={width} height={height} alt="" />
|
||||
<span>{title}</span>
|
||||
</button>
|
||||
);
|
||||
51
app/app/_components/SidebarEmptyHistory/index.tsx
Normal file
51
app/app/_components/SidebarEmptyHistory/index.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import Image from "next/image";
|
||||
import { SidebarButton } from "../SidebarButton";
|
||||
import { executeSerial } from "../../../electron/core/plugin-manager/execution/extension-manager";
|
||||
import { ModelManagementService } from "../../../shared/coreService";
|
||||
import useCreateConversation from "@/_hooks/useCreateConversation";
|
||||
|
||||
const SidebarEmptyHistory: React.FC = () => {
|
||||
const { requestCreateConvo } = useCreateConversation();
|
||||
const startChat = async () => {
|
||||
const downloadedModels = await executeSerial(
|
||||
ModelManagementService.GET_DOWNLOADED_MODELS
|
||||
);
|
||||
if (!downloadedModels || downloadedModels.length === 0) {
|
||||
alert(
|
||||
"Seems like there is no model downloaded yet. Please download a model first."
|
||||
);
|
||||
} else {
|
||||
requestCreateConvo(1);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="flex flex-col items-center py-10 gap-3">
|
||||
<Image
|
||||
src={"icons/chat-bubble-oval-left.svg"}
|
||||
width={32}
|
||||
height={32}
|
||||
alt=""
|
||||
/>
|
||||
<div className="flex flex-col items-center gap-6">
|
||||
<div>
|
||||
<div className="text-center text-gray-900 text-sm">
|
||||
No Chat History
|
||||
</div>
|
||||
<div className="text-center text-gray-500 text-sm">
|
||||
Get started by creating a new chat.
|
||||
</div>
|
||||
</div>
|
||||
<SidebarButton
|
||||
callback={startChat}
|
||||
className="flex items-center border bg-blue-600 rounded-lg py-[9px] pl-[15px] pr-[17px] gap-2 text-white font-medium text-sm"
|
||||
height={14}
|
||||
icon="icons/Icon_plus.svg"
|
||||
title="New chat"
|
||||
width={14}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SidebarEmptyHistory;
|
||||
23
app/app/_components/SidebarFooter/index.tsx
Normal file
23
app/app/_components/SidebarFooter/index.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import React from "react";
|
||||
import { SidebarButton } from "../SidebarButton";
|
||||
|
||||
const SidebarFooter: React.FC = () => (
|
||||
<div className="flex justify-between items-center gap-2">
|
||||
<SidebarButton
|
||||
className="flex items-center border border-gray-200 rounded-lg p-2 gap-3 flex-1 justify-center text-gray-900 font-medium text-sm"
|
||||
height={24}
|
||||
icon="icons/discord.svg"
|
||||
title="Discord"
|
||||
width={24}
|
||||
/>
|
||||
<SidebarButton
|
||||
className="flex items-center border border-gray-200 rounded-lg p-2 gap-3 flex-1 justify-center text-gray-900 font-medium text-sm"
|
||||
height={24}
|
||||
icon="icons/unicorn_twitter.svg"
|
||||
title="Twitter"
|
||||
width={24}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default React.memo(SidebarFooter);
|
||||
13
app/app/_components/SidebarHeader/index.tsx
Normal file
13
app/app/_components/SidebarHeader/index.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import Image from "next/image";
|
||||
|
||||
const SidebarHeader: React.FC = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-[10px]">
|
||||
<div className="flex items-center justify-between">
|
||||
<Image src={"icons/Jan_AppIcon.svg"} width={68} height={28} alt="" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SidebarHeader;
|
||||
53
app/app/_components/SidebarMenu/index.tsx
Normal file
53
app/app/_components/SidebarMenu/index.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
|
||||
const SidebarMenu: React.FC = () => {
|
||||
const menu = [
|
||||
{
|
||||
name: "Chat History",
|
||||
icon: "ClipboardList",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
name: "Explore Models",
|
||||
icon: "Search_gray",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
name: "My Models",
|
||||
icon: "ViewGrid",
|
||||
url: "#",
|
||||
},
|
||||
{
|
||||
name: "Settings",
|
||||
icon: "Cog",
|
||||
url: "/settings",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col justify-end">
|
||||
<div className="text-gray-500 text-xs font-semibold py-2 pl-2 pr-3">
|
||||
Your Configurations
|
||||
</div>
|
||||
{menu.map((item, index) => (
|
||||
<div key={index} className="py-2 pl-2 pr-3">
|
||||
<Link
|
||||
href={item.url}
|
||||
className="flex items-center gap-3 text-base text-gray-600"
|
||||
>
|
||||
<Image
|
||||
src={`icons/${item.icon}.svg`}
|
||||
width={24}
|
||||
height={24}
|
||||
alt=""
|
||||
/>
|
||||
{item.name}
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SidebarMenu;
|
||||
@ -51,7 +51,7 @@ const SimpleControlNetMessage: React.FC<Props> = ({
|
||||
target="_blank_"
|
||||
className="flex gap-1 items-center px-2 py-1 bg-[#F3F4F6] rounded-[12px]"
|
||||
>
|
||||
<Image src="/icons/download.svg" width={16} height={16} alt="" />
|
||||
<Image src="icons/download.svg" width={16} height={16} alt="" />
|
||||
<span className="leading-[20px] text-[14px] text-[#111928]">
|
||||
Download
|
||||
</span>
|
||||
@ -48,7 +48,7 @@ const SimpleImageMessage: React.FC<Props> = ({
|
||||
target="_blank_"
|
||||
className="flex gap-1 items-center px-2 py-1 bg-[#F3F4F6] rounded-[12px]"
|
||||
>
|
||||
<Image src="/icons/download.svg" width={16} height={16} alt="" />
|
||||
<Image src="icons/download.svg" width={16} height={16} alt="" />
|
||||
<span className="leading-[20px] text-[14px] text-[#111928]">
|
||||
Download
|
||||
</span>
|
||||
@ -57,7 +57,7 @@ const SimpleImageMessage: React.FC<Props> = ({
|
||||
className="flex gap-1 items-center px-2 py-1 bg-[#F3F4F6] rounded-[12px]"
|
||||
// onClick={() => sendChatMessage()}
|
||||
>
|
||||
<Image src="/icons/refresh.svg" width={16} height={16} alt="" />
|
||||
<Image src="icons/refresh.svg" width={16} height={16} alt="" />
|
||||
<span className="leading-[20px] text-[14px] text-[#111928]">
|
||||
Re-generate
|
||||
</span>
|
||||
@ -11,7 +11,7 @@ const Slide: React.FC<Props> = ({ product }) => {
|
||||
const { requestCreateConvo } = useCreateConversation();
|
||||
|
||||
const onClick = () => {
|
||||
requestCreateConvo(product);
|
||||
requestCreateConvo(1);
|
||||
};
|
||||
|
||||
return (
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user