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:
Louis 2023-09-25 10:42:58 +07:00 committed by GitHub
parent a5c70630f9
commit 20dbc02c03
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
339 changed files with 7090 additions and 12098 deletions

6
.gitignore vendored
View File

@ -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

96
app/README.md Normal file
View 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.
![](./images/jan-desktop-dev-instruction-1.png)
![](./images/jan-desktop-dev-instruction-2.png)
![](./images/jan-desktop-dev-instruction-3.png)
After that, you can use Jan Desktop as normal.
![](./images/jan-desktop-dev-instruction-4.png)
![](./images/jan-desktop-dev-instruction-5.png)
![](./images/jan-desktop-dev-instruction-6.png)
### 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! 🚀🎨🤖

View File

@ -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)}

View File

@ -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)}

View File

@ -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)}

View File

@ -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)}

View File

@ -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=""

View File

@ -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

View File

@ -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>
);

View File

@ -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>
);
}

View File

@ -1,3 +1,4 @@
/* eslint-disable react/display-name */
import React, { forwardRef } from "react";
import renderChatMessage from "../ChatBody/renderChatMessage";
import { ChatMessage } from "@/_models/ChatMessage";

View File

@ -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>

View File

@ -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>
);
};

View 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);

View File

@ -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>

View File

@ -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>

View 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;

View 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;

View 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;

View File

@ -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=""

View File

@ -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">

View File

@ -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

View 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;

View 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);

View 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);

View 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&apos;s chat
</button>
</div>
);
};
export default React.memo(HistoryEmpty);

View File

@ -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>
<>

View File

@ -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>
);

View File

@ -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>
);
};

View File

@ -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>

View 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);

View File

@ -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 />

View 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;

View 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;

View 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>
// );
};

View File

@ -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}

View File

@ -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=""

View File

@ -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>

View File

@ -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}

View 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;

View 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;

View File

@ -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>

View 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;

View 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>
);
}

View File

@ -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>
);
};

View 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;

View File

@ -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>
);

View 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>
);
};

View 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;

View File

@ -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>
);

View File

@ -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>
);
};

View File

@ -11,7 +11,7 @@ const ShortcutItem: React.FC<Props> = ({ product }) => {
const { requestCreateConvo } = useCreateConversation();
const onClickHandler = () => {
requestCreateConvo(product);
requestCreateConvo(1);
};
return (

View File

@ -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=""

View 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>
);

View 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;

View 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);

View 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;

View 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;

View File

@ -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>

View File

@ -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>

View File

@ -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