Chore/disable submodule (#56)

* Chore disable git submodule for web-client and app-backend

* Chore add newest source code of app-backend and web-client

---------

Co-authored-by: Hien To <tominhhien97@gmail.com>
This commit is contained in:
hiento09 2023-09-05 16:29:07 +07:00 committed by GitHub
parent 3826e30663
commit 86f0ffc7d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
282 changed files with 17895 additions and 8 deletions

6
.gitmodules vendored
View File

@ -1,9 +1,3 @@
[submodule "web-client"]
path = web-client
url = ../jan-web.git
[submodule "app-backend"]
path = app-backend
url = ../app-backend.git
[submodule "jan-inference/sd/sd_cpp"] [submodule "jan-inference/sd/sd_cpp"]
path = jan-inference/sd/sd_cpp path = jan-inference/sd/sd_cpp
url = https://github.com/leejet/stable-diffusion.cpp url = https://github.com/leejet/stable-diffusion.cpp

@ -1 +0,0 @@
Subproject commit 4c44efe7187b15cd2b65f3c8f6a089982941dbd7

4
app-backend/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
.DS_Store
.env
.env_postgresql
worker/node_modules/.mf

59
app-backend/README.md Normal file
View File

@ -0,0 +1,59 @@
## Jan Backend
A Hasura Data API Platform designed to provide APIs for client interaction with the Language Model (LLM) through chat or the generation of art using Stable Diffusion. It is encapsulated within a Docker container for easy local deployment
## Quickstart
1. Run docker up
```bash
docker compose up
```
2. Install [HasuraCLI](https://hasura.io/docs/latest/hasura-cli/overview/)
3. Open Hasura Console
```bash
cd hasura && hasura console
```
4. Apply Migration
```bash
hasura migrate apply
```
5. Apply Metadata
```bash
hasura metadata apply
```
6. Apply seeds
```bash
hasura seed apply
```
## Hasura One Click Deploy
Use this URL to deploy this app to Hasura Cloud
[![Hasura Deploy](https://hasura.io/deploy-button.svg)](https://cloud.hasura.io/deploy?github_repo=https://github.com/janhq/app-backend/&hasura_dir=/hasura)
[One-click deploy docs](https://hasura.io/docs/latest/getting-started/getting-started-cloud/)
## Modify schema & model
[Hasura Tutorials](https://hasura.io/docs/latest/resources/tutorials/index/)
## Events & Workers
Serverless function (Cloudflare worker) to stream llm message & update
Readmore about Hasura Events here:
> https://hasura.io/docs/latest/event-triggers/serverless/
## Deploy Worker
```bash
npx wrangler deploy
```
[Cloudflare Worker Guide](https://developers.cloudflare.com/workers/get-started/guide/)

View File

@ -0,0 +1,52 @@
version: "3.6"
services:
postgres:
image: postgres:13
restart: always
volumes:
- db_data:/var/lib/postgresql/data
env_file:
- .env_postgresql
graphql-engine:
image: hasura/graphql-engine:v2.31.0.cli-migrations-v3
ports:
- "8080:8080"
restart: always
env_file:
- .env
volumes:
- ./hasura/migrations:/migrations
- ./hasura/metadata:/metadata
depends_on:
data-connector-agent:
condition: service_healthy
data-connector-agent:
image: hasura/graphql-data-connector:v2.31.0
restart: always
ports:
- 8081:8081
environment:
QUARKUS_LOG_LEVEL: ERROR # FATAL, ERROR, WARN, INFO, DEBUG, TRACE
## https://quarkus.io/guides/opentelemetry#configuration-reference
QUARKUS_OPENTELEMETRY_ENABLED: "false"
## QUARKUS_OPENTELEMETRY_TRACER_EXPORTER_OTLP_ENDPOINT: http://jaeger:4317
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8081/api/v1/athena/health"]
interval: 5s
timeout: 10s
retries: 5
start_period: 5s
worker:
build:
context: ./worker
dockerfile: ./Dockerfile
restart: always
environment:
- "NODE_ENV=development"
volumes:
- ./worker:/worker
ports:
- "8787:8787"
volumes:
db_data:

View File

@ -0,0 +1,7 @@
version: 3
endpoint: http://localhost:8080
admin_secret: myadminsecretkey
metadata_directory: metadata
actions:
kind: synchronous
handler_webhook_baseurl: http://localhost:3000

View File

@ -0,0 +1,20 @@
type Mutation {
imageGeneration(
input: ImageGenerationInput!
): ImageGenerationOutput
}
input ImageGenerationInput {
prompt: String!
neg_prompt: String!
model: String!
seed: Int!
steps: Int!
width: Int!
height: Int!
}
type ImageGenerationOutput {
url: String!
}

View File

@ -0,0 +1,32 @@
actions:
- name: imageGeneration
definition:
kind: synchronous
handler: '{{HASURA_ACTION_STABLE_DIFFUSION_URL}}'
request_transform:
body:
action: transform
template: |-
{
"prompt": {{$body.input.input.prompt}},
"neg_prompt": {{$body.input.input.neg_prompt}},
"unet_model": {{$body.input.input.model}},
"seed": {{$body.input.input.seed}},
"steps": {{$body.input.input.steps}},
"width": {{$body.input.input.width}},
"height": {{$body.input.input.height}}
}
method: POST
query_params: {}
template_engine: Kriti
url: '{{$base_url}}/inferences/txt2img'
version: 2
permissions:
- role: user
custom_types:
enums: []
input_objects:
- name: ImageGenerationInput
objects:
- name: ImageGenerationOutput
scalars: []

View File

@ -0,0 +1 @@
[]

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1,11 @@
dataconnector:
athena:
uri: http://data-connector-agent:8081/api/v1/athena
mariadb:
uri: http://data-connector-agent:8081/api/v1/mariadb
mysql8:
uri: http://data-connector-agent:8081/api/v1/mysql
oracle:
uri: http://data-connector-agent:8081/api/v1/oracle
snowflake:
uri: http://data-connector-agent:8081/api/v1/snowflake

View File

@ -0,0 +1 @@
[]

View File

@ -0,0 +1,9 @@
- name: jandb
kind: postgres
configuration:
connection_info:
database_url:
from_env: PG_DATABASE_URL
isolation_level: read-committed
use_prepared_statements: false
tables: "!include jandb/tables/tables.yaml"

View File

@ -0,0 +1,43 @@
table:
name: collection_products
schema: public
array_relationships:
- name: collections
using:
manual_configuration:
column_mapping:
collection_id: id
insertion_order: null
remote_table:
name: collections
schema: public
- name: products
using:
manual_configuration:
column_mapping:
product_id: id
insertion_order: null
remote_table:
name: products
schema: public
select_permissions:
- role: public
permission:
columns:
- created_at
- updated_at
- collection_id
- id
- product_id
filter: {}
comment: ""
- role: user
permission:
columns:
- created_at
- updated_at
- collection_id
- id
- product_id
filter: {}
comment: ""

View File

@ -0,0 +1,36 @@
table:
name: collections
schema: public
array_relationships:
- name: collection_products
using:
manual_configuration:
column_mapping:
id: collection_id
insertion_order: null
remote_table:
name: collection_products
schema: public
select_permissions:
- role: public
permission:
columns:
- slug
- description
- name
- created_at
- updated_at
- id
filter: {}
comment: ""
- role: user
permission:
columns:
- slug
- description
- name
- created_at
- updated_at
- id
filter: {}
comment: ""

View File

@ -0,0 +1,68 @@
table:
name: conversations
schema: public
object_relationships:
- name: conversation_product
using:
manual_configuration:
column_mapping:
product_id: id
insertion_order: null
remote_table:
name: products
schema: public
array_relationships:
- name: conversation_messages
using:
manual_configuration:
column_mapping:
id: conversation_id
insertion_order: null
remote_table:
name: messages
schema: public
insert_permissions:
- role: user
permission:
check:
user_id:
_eq: X-Hasura-User-Id
columns:
- last_image_url
- last_text_message
- product_id
- user_id
comment: ""
select_permissions:
- role: user
permission:
columns:
- last_image_url
- last_text_message
- user_id
- created_at
- updated_at
- id
- product_id
filter:
user_id:
_eq: X-Hasura-User-Id
comment: ""
update_permissions:
- role: user
permission:
columns:
- last_image_url
- last_text_message
filter:
user_id:
_eq: X-Hasura-User-Id
check: null
comment: ""
delete_permissions:
- role: user
permission:
filter:
user_id:
_eq: X-Hasura-User-Id
comment: ""

View File

@ -0,0 +1,68 @@
table:
name: message_medias
schema: public
object_relationships:
- name: media_message
using:
manual_configuration:
column_mapping:
message_id: id
insertion_order: null
remote_table:
name: messages
schema: public
insert_permissions:
- role: user
permission:
check:
media_message:
message_conversation:
user_id:
_eq: X-Hasura-User-Id
columns:
- media_url
- message_id
- mime_type
comment: ""
select_permissions:
- role: user
permission:
columns:
- mime_type
- media_url
- created_at
- updated_at
- id
- message_id
filter:
media_message:
message_conversation:
user_id:
_eq: X-Hasura-User-Id
comment: ""
update_permissions:
- role: user
permission:
columns:
- mime_type
- media_url
- created_at
- updated_at
- id
- message_id
filter:
media_message:
message_conversation:
user_id:
_eq: X-Hasura-User-Id
check: null
comment: ""
delete_permissions:
- role: user
permission:
filter:
media_message:
message_conversation:
user_id:
_eq: X-Hasura-User-Id
comment: ""

View File

@ -0,0 +1,97 @@
table:
name: messages
schema: public
object_relationships:
- name: message_conversation
using:
manual_configuration:
column_mapping:
conversation_id: id
insertion_order: null
remote_table:
name: conversations
schema: public
array_relationships:
- name: message_medias
using:
manual_configuration:
column_mapping:
id: message_id
insertion_order: null
remote_table:
name: message_medias
schema: public
insert_permissions:
- role: user
permission:
check:
message_conversation:
user_id:
_eq: X-Hasura-User-Id
columns:
- content
- conversation_id
- message_sender_type
- message_type
- prompt_cache
- sender
- sender_avatar_url
- sender_name
- status
comment: ""
select_permissions:
- role: user
permission:
columns:
- content
- conversation_id
- created_at
- id
- message_sender_type
- message_type
- sender
- sender_avatar_url
- sender_name
- status
- updated_at
filter:
message_conversation:
user_id:
_eq: X-Hasura-User-Id
comment: ""
update_permissions:
- role: user
permission:
columns:
- content
- message_sender_type
- message_type
- sender
- sender_avatar_url
- sender_name
- status
filter:
message_conversation:
user_id:
_eq: X-Hasura-User-Id
check: null
comment: ""
delete_permissions:
- role: user
permission:
filter:
message_conversation:
user_id:
_eq: X-Hasura-User-Id
comment: ""
event_triggers:
- name: new_llm_message
definition:
enable_manual: false
insert:
columns: '*'
retry_conf:
interval_sec: 10
num_retries: 0
timeout_sec: 60
webhook: '{{HASURA_EVENTS_HOOK_URL}}'

View File

@ -0,0 +1,43 @@
table:
name: product_prompts
schema: public
array_relationships:
- name: products
using:
manual_configuration:
column_mapping:
product_id: id
insertion_order: null
remote_table:
name: products
schema: public
- name: prompts
using:
manual_configuration:
column_mapping:
prompt_id: id
insertion_order: null
remote_table:
name: prompts
schema: public
select_permissions:
- role: public
permission:
columns:
- created_at
- updated_at
- id
- product_id
- prompt_id
filter: {}
comment: ""
- role: user
permission:
columns:
- created_at
- updated_at
- id
- product_id
- prompt_id
filter: {}
comment: ""

View File

@ -0,0 +1,65 @@
table:
name: products
schema: public
array_relationships:
- name: product_collections
using:
manual_configuration:
column_mapping:
id: product_id
insertion_order: null
remote_table:
name: collection_products
schema: public
- name: product_prompts
using:
manual_configuration:
column_mapping:
id: product_id
insertion_order: null
remote_table:
name: product_prompts
schema: public
select_permissions:
- role: public
permission:
columns:
- nsfw
- slug
- inputs
- outputs
- author
- description
- greeting
- image_url
- long_description
- name
- source_url
- technical_description
- version
- created_at
- updated_at
- id
filter: {}
comment: ""
- role: user
permission:
columns:
- nsfw
- slug
- inputs
- outputs
- author
- description
- greeting
- image_url
- long_description
- name
- source_url
- technical_description
- version
- created_at
- updated_at
- id
filter: {}
comment: ""

View File

@ -0,0 +1,36 @@
table:
name: prompts
schema: public
array_relationships:
- name: prompt_products
using:
manual_configuration:
column_mapping:
id: prompt_id
insertion_order: null
remote_table:
name: product_prompts
schema: public
select_permissions:
- role: public
permission:
columns:
- slug
- content
- image_url
- created_at
- updated_at
- id
filter: {}
comment: ""
- role: user
permission:
columns:
- slug
- content
- image_url
- created_at
- updated_at
- id
filter: {}
comment: ""

View File

@ -0,0 +1,8 @@
- "!include public_collection_products.yaml"
- "!include public_collections.yaml"
- "!include public_conversations.yaml"
- "!include public_message_medias.yaml"
- "!include public_messages.yaml"
- "!include public_product_prompts.yaml"
- "!include public_products.yaml"
- "!include public_prompts.yaml"

View File

@ -0,0 +1 @@
disabled_for_roles: []

View File

@ -0,0 +1 @@
[]

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
[]

View File

@ -0,0 +1 @@
[]

View File

@ -0,0 +1 @@
[]

View File

@ -0,0 +1 @@
version: 3

View File

@ -0,0 +1 @@
DROP TABLE "public"."collections";

View File

@ -0,0 +1,18 @@
CREATE TABLE "public"."collections" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "slug" varchar NOT NULL, "name" text NOT NULL, "description" text NOT NULL, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id") , UNIQUE ("slug"));
CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"()
RETURNS TRIGGER AS $$
DECLARE
_new record;
BEGIN
_new := NEW;
_new."updated_at" = NOW();
RETURN _new;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER "set_public_collections_updated_at"
BEFORE UPDATE ON "public"."collections"
FOR EACH ROW
EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"();
COMMENT ON TRIGGER "set_public_collections_updated_at" ON "public"."collections"
IS 'trigger to set value of column "updated_at" to current timestamp on row update';
CREATE EXTENSION IF NOT EXISTS pgcrypto;

View File

@ -0,0 +1 @@
DROP TABLE "public"."products";

View File

@ -0,0 +1,18 @@
CREATE TABLE "public"."products" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "slug" varchar NOT NULL, "name" text NOT NULL, "description" text, "image_url" text, "long_description" text, "technical_description" text, "author" text, "version" text, "source_url" text, "nsfw" boolean NOT NULL DEFAULT true, "greeting" text, "inputs" jsonb, "outputs" jsonb, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id") , UNIQUE ("slug"));
CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"()
RETURNS TRIGGER AS $$
DECLARE
_new record;
BEGIN
_new := NEW;
_new."updated_at" = NOW();
RETURN _new;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER "set_public_products_updated_at"
BEFORE UPDATE ON "public"."products"
FOR EACH ROW
EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"();
COMMENT ON TRIGGER "set_public_products_updated_at" ON "public"."products"
IS 'trigger to set value of column "updated_at" to current timestamp on row update';
CREATE EXTENSION IF NOT EXISTS pgcrypto;

View File

@ -0,0 +1 @@
DROP TABLE "public"."prompts";

View File

@ -0,0 +1,18 @@
CREATE TABLE "public"."prompts" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "slug" varchar NOT NULL, "content" text, "image_url" text, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id") , UNIQUE ("slug"));
CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"()
RETURNS TRIGGER AS $$
DECLARE
_new record;
BEGIN
_new := NEW;
_new."updated_at" = NOW();
RETURN _new;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER "set_public_prompts_updated_at"
BEFORE UPDATE ON "public"."prompts"
FOR EACH ROW
EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"();
COMMENT ON TRIGGER "set_public_prompts_updated_at" ON "public"."prompts"
IS 'trigger to set value of column "updated_at" to current timestamp on row update';
CREATE EXTENSION IF NOT EXISTS pgcrypto;

View File

@ -0,0 +1 @@
DROP TABLE "public"."conversations";

View File

@ -0,0 +1,18 @@
CREATE TABLE "public"."conversations" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "product_id" uuid NOT NULL, "user_id" Text NOT NULL, "last_image_url" text, "last_text_message" text, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id") );
CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"()
RETURNS TRIGGER AS $$
DECLARE
_new record;
BEGIN
_new := NEW;
_new."updated_at" = NOW();
RETURN _new;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER "set_public_conversations_updated_at"
BEFORE UPDATE ON "public"."conversations"
FOR EACH ROW
EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"();
COMMENT ON TRIGGER "set_public_conversations_updated_at" ON "public"."conversations"
IS 'trigger to set value of column "updated_at" to current timestamp on row update';
CREATE EXTENSION IF NOT EXISTS pgcrypto;

View File

@ -0,0 +1 @@
DROP TABLE "public"."messages";

View File

@ -0,0 +1,18 @@
CREATE TABLE "public"."messages" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "conversation_id" uuid NOT NULL, "message_type" varchar, "message_sender_type" varchar, "sender" text NOT NULL, "sender_name" text, "sender_avatar_url" text, "content" text, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id") );
CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"()
RETURNS TRIGGER AS $$
DECLARE
_new record;
BEGIN
_new := NEW;
_new."updated_at" = NOW();
RETURN _new;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER "set_public_messages_updated_at"
BEFORE UPDATE ON "public"."messages"
FOR EACH ROW
EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"();
COMMENT ON TRIGGER "set_public_messages_updated_at" ON "public"."messages"
IS 'trigger to set value of column "updated_at" to current timestamp on row update';
CREATE EXTENSION IF NOT EXISTS pgcrypto;

View File

@ -0,0 +1 @@
DROP TABLE "public"."message_medias";

View File

@ -0,0 +1,18 @@
CREATE TABLE "public"."message_medias" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "message_id" uuid NOT NULL, "media_url" text, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), "mime_type" varchar, PRIMARY KEY ("id") );
CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"()
RETURNS TRIGGER AS $$
DECLARE
_new record;
BEGIN
_new := NEW;
_new."updated_at" = NOW();
RETURN _new;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER "set_public_message_medias_updated_at"
BEFORE UPDATE ON "public"."message_medias"
FOR EACH ROW
EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"();
COMMENT ON TRIGGER "set_public_message_medias_updated_at" ON "public"."message_medias"
IS 'trigger to set value of column "updated_at" to current timestamp on row update';
CREATE EXTENSION IF NOT EXISTS pgcrypto;

View File

@ -0,0 +1 @@
DROP TABLE "public"."collection_products";

View File

@ -0,0 +1,18 @@
CREATE TABLE "public"."collection_products" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "collection_id" uuid NOT NULL, "product_id" uuid NOT NULL, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id") , UNIQUE ("collection_id", "product_id"));
CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"()
RETURNS TRIGGER AS $$
DECLARE
_new record;
BEGIN
_new := NEW;
_new."updated_at" = NOW();
RETURN _new;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER "set_public_collection_products_updated_at"
BEFORE UPDATE ON "public"."collection_products"
FOR EACH ROW
EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"();
COMMENT ON TRIGGER "set_public_collection_products_updated_at" ON "public"."collection_products"
IS 'trigger to set value of column "updated_at" to current timestamp on row update';
CREATE EXTENSION IF NOT EXISTS pgcrypto;

View File

@ -0,0 +1 @@
DROP TABLE "public"."product_prompts";

View File

@ -0,0 +1,18 @@
CREATE TABLE "public"."product_prompts" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "product_id" uuid NOT NULL, "prompt_id" uuid NOT NULL, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id") , UNIQUE ("product_id", "prompt_id"));
CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"()
RETURNS TRIGGER AS $$
DECLARE
_new record;
BEGIN
_new := NEW;
_new."updated_at" = NOW();
RETURN _new;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER "set_public_product_prompts_updated_at"
BEFORE UPDATE ON "public"."product_prompts"
FOR EACH ROW
EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"();
COMMENT ON TRIGGER "set_public_product_prompts_updated_at" ON "public"."product_prompts"
IS 'trigger to set value of column "updated_at" to current timestamp on row update';
CREATE EXTENSION IF NOT EXISTS pgcrypto;

View File

@ -0,0 +1 @@
alter table "public"."collection_products" drop constraint "collection_products_collection_id_fkey";

View File

@ -0,0 +1,5 @@
alter table "public"."collection_products"
add constraint "collection_products_collection_id_fkey"
foreign key ("collection_id")
references "public"."collections"
("id") on update cascade on delete cascade;

View File

@ -0,0 +1 @@
alter table "public"."collection_products" drop constraint "collection_products_product_id_fkey";

View File

@ -0,0 +1,5 @@
alter table "public"."collection_products"
add constraint "collection_products_product_id_fkey"
foreign key ("product_id")
references "public"."products"
("id") on update cascade on delete cascade;

View File

@ -0,0 +1 @@
alter table "public"."messages" drop column "status";

View File

@ -0,0 +1,2 @@
alter table "public"."messages" add column "status" varchar
null default 'ready';

View File

@ -0,0 +1 @@
alter table "public"."messages" drop column "prompt_cache";

View File

@ -0,0 +1,2 @@
alter table "public"."messages" add column "prompt_cache" jsonb
null;

View File

@ -0,0 +1,4 @@
SET check_function_bodies = false;
INSERT INTO public.products ("slug", "name", "nsfw", "image_url", "description", "long_description", "technical_description", "author", "version", "source_url", "inputs", "outputs", "greeting") VALUES
('llama2', 'Llama-2-7B-Chat', 't', 'https://static-assets.jan.ai/llama2.jpg','Llama 2 is Meta`s open source large language model (LLM)', 'Llama 2 is a collection of pretrained and fine-tuned generative text models ranging in scale from 7 billion to 70 billion parameters. This is the repository for the 7B pretrained model. Links to other models can be found in the index at the bottom.', 'Meta developed and publicly released the Llama 2 family of large language models (LLMs), a collection of pretrained and fine-tuned generative text models ranging in scale from 7 billion to 70 billion parameters. Our fine-tuned LLMs, called Llama-2-Chat, are optimized for dialogue use cases. Llama-2-Chat models outperform open-source chat models on most benchmarks we tested, and in our human evaluations for helpfulness and safety, are on par with some popular closed-source models like ChatGPT and PaLM.', 'Meta', 'Llama2-7B-GGML', 'https://huggingface.co/TheBloke/airoboros-13B-gpt4-1.4-GGML', '{"body": [{"name": "messages", "type": "array", "items": [{"type": "object", "properties": [{"name": "role", "type": "string", "example": "system", "description": "Defines the role of the message."}, {"name": "content", "type": "string", "example": "Hello, world!", "description": "Contains the content of the message."}]}], "description": "An array of messages, each containing a role and content. The latest message is always at the end of the array."}, {"name": "stream", "type": "boolean", "example": true, "description": "Indicates whether the client wants to keep the connection open for streaming."}, {"name": "max_tokens", "type": "integer", "example": 500, "description": "Defines the maximum number of tokens that the client wants to receive."}], "slug": "llm", "headers": {"accept": "text/event-stream", "content-type": "application/json"}}', '{"slug": "llm", "type": "object", "properties": [{"name": "id", "type": "string", "example": "chatcmpl-4c4e5eb5-bf53-4dbc-9136-1cf69fc5fd7c", "description": "The unique identifier of the chat completion chunk."}, {"name": "model", "type": "string", "example": "gpt-3.5-turbo", "description": "The name of the GPT model used to generate the completion."}, {"name": "created", "type": "integer", "example": 1692169988, "description": "The Unix timestamp representing the time when the completion was generated."}, {"name": "object", "type": "string", "example": "chat.completion.chunk", "description": "A string indicating the type of the chat completion chunk."}, {"name": "choices", "type": "array", "items": [{"type": "object", "properties": [{"name": "index", "type": "integer", "example": 0, "description": "The index of the choice made by the GPT model."}, {"name": "delta", "type": "object", "properties": [{"name": "content", "type": "string", "example": "What", "description": "The content generated by the GPT model."}], "description": "A JSON object containing the content generated by the GPT model."}, {"name": "finish_reason", "type": "string", "example": null, "description": "A string indicating why the GPT model stopped generating content."}]}], "description": "An array containing the choices made by the GPT model to generate the completion."}], "description": "A JSON object representing a chat completion chunk."}', '👋Im a versatile AI trained on a wide range of topics, here to answer your questions about the universe. What are you curious about today?'),
('stablediffusion', 'Stable-Diffusion-v1.5', 't', 'https://static-assets.jan.ai/stablediffusion.jpg', 'Stable Diffusion is a latent text-to-image diffusion model capable of generating photo-realistic images given any text input.', 'The Stable-Diffusion-v1-5 checkpoint was initialized with the weights of the Stable-Diffusion-v1-2 checkpoint and subsequently fine-tuned on 595k steps at resolution 512x512 on "laion-aesthetics v2 5+" and 10% dropping of the text-conditioning to improve classifier-free guidance sampling.', 'This is a model that can be used to generate and modify images based on text prompts. It is a Latent Diffusion Model that uses a fixed, pretrained text encoder (CLIP ViT-L/14) as suggested in the Imagen paper.', 'runwayml', '4.0.0', 'https://huggingface.co/runwayml/stable-diffusion-v1-5/resolve/main/v1-5-pruned-emaonly.safetensors', '{"body": [{"name": "prompt", "type": "string", "description": "Input prompt"}, {"name": "negative_prompt", "type": "string", "description": "Specify things to not see in the output"}, {"name": "width", "type": "integer", "description": "Width of output image. Maximum size is 1024x768 or 768x1024 because of memory limits"}, {"name": "height", "type": "integer", "description": "Height of output image. Maximum size is 1024x768 or 768x1024 because of memory limits"}, {"name": "num_outputs", "type": "integer", "description": "Number of images to output. (minimum: 1; maximum: 4)"}, {"name": "num_inference_steps", "type": "integer", "description": "Number of denoising steps (minimum: 1; maximum: 500)"}, {"name": "guidance_scale", "type": "integer", "description": "Scale for classifier-free guidance (minimum: 1; maximum: 20)"}], "slug": "sd", "headers": {"accept": "application/json", "content-type": "multipart/form-data"}}', '{"slug": "sd", "type": "object", "properties": [{"name": "url", "type": "string", "description": "The unique identifier of the chat completion chunk."}], "description": "URL result image."}', 'Hello there 👋')

View File

@ -0,0 +1,4 @@
SET check_function_bodies = false;
INSERT INTO public.collections ("slug", "name", "description") VALUES
('conversational', 'Conversational', 'Chatbot alternatives to ChatGPT. Converse with these models and get answers.'),
('text-to-image', 'Text to image', 'Converse with these models to generate images.');

View File

@ -0,0 +1,11 @@
SET check_function_bodies = false;
INSERT INTO public.collection_products (collection_id, product_id)
SELECT (SELECT id FROM public.collections WHERE slug = 'conversational') AS collection_id, id AS product_id
FROM public.products
WHERE slug IN ('llama2');
INSERT INTO public.collection_products (collection_id, product_id)
SELECT (SELECT id FROM public.collections WHERE slug = 'text-to-image') AS collection_id, id AS product_id
FROM public.products
WHERE slug IN ('stablediffusion');

View File

@ -0,0 +1,16 @@
SET check_function_bodies = false;
INSERT INTO public.prompts ("slug", "content", "image_url") VALUES
('conversational-ai-future', 'What are possible developments for AI technology in the next decade?', ''),
('conversational-managing-stress', 'What are some tips for managing stress?', ''),
('conversational-postapoc-robot', 'Let''s role play. You are a robot in a post-apocalyptic world.', ''),
('conversational-python-pytorch', 'What is the difference between Python and Pytorch?', ''),
('conversational-quadratic-equation', 'Can you explain how to solve a quadratic equation?', ''),
('conversational-roman-history', 'What is the history of the Roman Empire?', ''),
('openjourney-1girl-gothic-lolita', '(masterpiece, top quality, best, official art, beautiful and aesthetic:1. 2), 1girl, (pop art:1. 4), (zentangle, flower effects:1. 2), (art nouveau:1. 1), (Gothic Lolita:1. 3)', 'https://static-assets.jan.ai/openjourney-2.jpeg'),
('openjourney-female-robot-rust', 'old, female robot, metal, rust, wisible wires, destroyed, sad, dark, dirty, looking at viewer, portrait, photography, detailed skin, realistic, photo-realistic, 8k, highly detailed, full length frame, High detail RAW color art, piercing, diffused soft lighting, shallow depth of field, sharp focus, hyperrealism, cinematic lighting', 'https://static-assets.jan.ai/openjourney-3.jpeg'),
('openjourney-ginger-cat', 'full body fluffy ginger cat with blue eyes by studio ghibli, makoto shinkai, by artgerm, by wlop, by greg rutkowski, volumetric lighting, octane render, 4 k resolution, trending on artstation, masterpiece', 'https://static-assets.jan.ai/openjourney-0.jpg'),
('openjourney-human-face-paint', 'FluidArt, human face covered in paint, photoshoot pose, portrait, dramatic, tri-color, long sleeved frilly victorian dress made of thick dripping paint, rich thick cords of paint, medusa paint hair, appendages and legs transform into thick dripping paint, wide-zoom shot, hair metamorphosis into thick paint', 'https://static-assets.jan.ai/openjourney-4.jpeg'),
('openjourney-pocahontas', 'mdjrny-v4 style portrait photograph of Madison Beer as Pocahontas, young beautiful native american woman, perfect symmetrical face, feather jewelry, traditional handmade dress, armed female hunter warrior, (((wild west))) environment, Utah landscape, ultra realistic, concept art, elegant, ((intricate)), ((highly detailed)), depth of field, ((professionally color graded)), 8k, art by artgerm and greg rutkowski and alphonse mucha', 'https://static-assets.jan.ai/openjourney-1.jpeg'),
('text2image-gray-dog-eyes', 'realistic portrait of an gray dog, bright eyes, radiant and ethereal intricately detailed photography, cinematic lighting, 50mm lens with bokeh', 'https://static-assets.jan.ai/openjourney-7.jpeg'),
('text2image-ogre-exoskeleton', 'mdjrny-v4 style OGRE is wearing a powered exoskeleton , long horn, , cute face, tsutomu nihei style, Claude Monet, banksy art, 8K, Highly Detailed, Dramatic Lighting, high quality, ray of god, explosion, lens flare, beautiful detailed sky, cinematic lighting, overexposure, quality, colorful, hdr, concept design, photorealistic, hyper real, Alphonse Mucha, Pixar, cyberpunk 2077, masterpiece, the best quality, super fine illustrations, beatiful detailed cyberpunk city, extremely detailed eyes and face, beatiful detailed hair, wavy hair,beatiful detailed steet,mecha clothes, robot girl, bodysuit, very delicate light, fine lighting, very fine 8KCG wallpapers, plateau, sunrise, overexposure, randomly distributed clouds, cliff, rotating star sky, lake in mountain stream, luminous particles , Unreal Engine5, 8K', 'https://static-assets.jan.ai/openjourney-6.jpeg'),
('text2image-pablo-picasso', 'a young caucasian man holding his chin.pablo picasso style, acrylic painting, trending on pixiv fanbox, palette knife and brush. strokes,', 'https://static-assets.jan.ai/openjourney-5.jpeg');

View File

@ -0,0 +1,21 @@
SET check_function_bodies = false;
INSERT INTO public.product_prompts (product_id, prompt_id)
SELECT p.id AS product_id, r.id AS prompt_id
FROM public.products p
JOIN public.prompts r
ON (p.id
IN (SELECT x.id FROM public.products x INNER JOIN public.collection_products y ON x.id = y.product_id
INNER JOIN public.collections z ON y.collection_id = z.id
WHERE z.slug = 'text-to-image'))
WHERE r.image_url IS NOT NULL AND r.image_url != '';
INSERT INTO public.product_prompts (product_id, prompt_id)
SELECT p.id AS product_id, r.id AS prompt_id
FROM public.products p
JOIN public.prompts r
ON (p.id
IN (SELECT x.id FROM public.products x INNER JOIN public.collection_products y ON x.id = y.product_id
INNER JOIN public.collections z ON y.collection_id = z.id
WHERE z.slug = 'conversational'))
WHERE r.image_url IS NULL OR r.image_url = '';

23
app-backend/sample.env Normal file
View File

@ -0,0 +1,23 @@
## postgres database to store Hasura metadata
HASURA_GRAPHQL_METADATA_DATABASE_URL=postgres://postgres:postgrespassword@postgres:5432/postgres
## this env var can be used to add the above postgres database to Hasura as a data source. this can be removed/updated based on your needs
PG_DATABASE_URL=postgres://postgres:postgrespassword@postgres:5432/postgres
## enable the console served by server
HASURA_GRAPHQL_ENABLE_CONSOLE="true" # set to "false" to disable console
## enable debugging mode. It is recommended to disable this in production
HASURA_GRAPHQL_DEV_MODE="true"
HASURA_GRAPHQL_ENABLED_LOG_TYPES=startup, http-log, webhook-log, websocket-log, query-log
## uncomment next line to run console offline (i.e load console assets from server instead of CDN)
# HASURA_GRAPHQL_CONSOLE_ASSETS_DIR: /srv/console-assets
## uncomment next line to set an admin secret
HASURA_GRAPHQL_ADMIN_SECRET=myadminsecretkey
HASURA_GRAPHQL_UNAUTHORIZED_ROLE="public"
HASURA_GRAPHQL_METADATA_DEFAULTS='{"backend_configs":{"dataconnector":{"athena":{"uri":"http://data-connector-agent:8081/api/v1/athena"},"mariadb":{"uri":"http://data-connector-agent:8081/api/v1/mariadb"},"mysql8":{"uri":"http://data-connector-agent:8081/api/v1/mysql"},"oracle":{"uri":"http://data-connector-agent:8081/api/v1/oracle"},"snowflake":{"uri":"http://data-connector-agent:8081/api/v1/snowflake"}}}}'
HASURA_GRAPHQL_JWT_SECRET={"type": "RS256", "key": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"}
# Environment variable for auto migrate
HASURA_GRAPHQL_MIGRATIONS_DIR=/migrations
HASURA_GRAPHQL_METADATA_DIR=/metadata
HASURA_GRAPHQL_ENABLE_CONSOLE='true'
HASURA_ACTION_STABLE_DIFFUSION_URL=http://sd:8000
HASURA_EVENTS_HOOK_URL="http://worker:8787"

View File

@ -0,0 +1 @@
POSTGRES_PASSWORD=postgrespassword

View File

@ -0,0 +1,16 @@
# Dockerfile
# alpine does not work with wrangler
FROM node
RUN mkdir -p /worker
WORKDIR /worker
RUN npm install -g wrangler
COPY . /worker
EXPOSE 8787
CMD ["wrangler", "dev"]

View File

@ -0,0 +1,197 @@
export interface Env {
HASURA_ADMIN_API_KEY: string;
LLM_INFERENCE_ENDPOINT: string;
INFERENCE_API_KEY: string;
HASURA_GRAPHQL_ENGINE_ENDPOINT: string;
}
export default {
async fetch(request: Request, env: Env) {
return handleRequest(env, request);
},
};
async function handleRequest(env: Env, request: Request) {
const apiurl = env.LLM_INFERENCE_ENDPOINT;
const requestBody = await request.json();
let lastCallTime = 0;
let timeoutId: any;
let done = true;
function throttle(fn: () => void, delay: number) {
return async function () {
const now = new Date().getTime();
const timeSinceLastCall = now - lastCallTime;
if (timeSinceLastCall >= delay && done) {
lastCallTime = now;
done = false;
await fn();
done = true;
} else {
clearTimeout(timeoutId);
timeoutId = setTimeout(async () => {
lastCallTime = now;
done = false;
await fn();
done = true;
}, delay - timeSinceLastCall);
}
};
}
const messageBody = {
id: requestBody.event.data.new.id,
content: requestBody.event.data.new.content,
messages: requestBody.event.data.new.prompt_cache,
status: requestBody.event.data.new.status,
};
if (messageBody.status !== "pending") {
return new Response(JSON.stringify({ status: "success" }), {
status: 200,
statusText: "success",
});
}
const llmRequestBody = {
messages: messageBody.messages,
stream: true,
model: "gpt-3.5-turbo",
max_tokens: 500,
};
const init = {
body: JSON.stringify(llmRequestBody),
headers: {
"Content-Type": "application/json",
Accept: "text/event-stream",
Authorization: "Access-Control-Allow-Origin: *",
"api-key": env.INFERENCE_API_KEY,
},
method: "POST",
};
return fetch(apiurl, init)
.then((res) => res.body?.getReader())
.then(async (reader) => {
if (!reader) {
console.error("Error: fail to read data from response");
return;
}
let answer = "";
let cachedChunk = "";
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
const textDecoder = new TextDecoder("utf-8");
const chunk = textDecoder.decode(value);
cachedChunk += chunk;
const matched = cachedChunk.match(/data: {(.*)}/g);
if (!matched) {
continue;
}
let deltaText = "";
for (const line of cachedChunk.split("\n")) {
const trimmedLine = line.trim();
if (!trimmedLine || trimmedLine === "data: [DONE]") {
continue;
}
const json = trimmedLine.replace("data: ", "");
try {
const obj = JSON.parse(json);
const content = obj.choices[0].delta.content;
if (content) deltaText = deltaText.concat(content);
} catch (e) {
console.log(e);
}
}
cachedChunk = "";
answer = answer + deltaText;
const variables = {
id: messageBody.id,
data: {
content: answer,
},
};
throttle(async () => {
await fetch(env.HASURA_GRAPHQL_ENGINE_ENDPOINT + "/v1/graphql", {
method: "POST",
body: JSON.stringify({ query: updateMessageQuery, variables }),
headers: {
"Content-Type": "application/json",
"x-hasura-admin-secret": env.HASURA_ADMIN_API_KEY,
},
})
.catch((error) => {
console.error(error);
})
.finally(() => console.log("++-- request sent"));
}, 300)();
}
const variables = {
id: messageBody.id,
data: {
status: "ready",
prompt_cache: null,
},
};
await fetch(env.HASURA_GRAPHQL_ENGINE_ENDPOINT + "/v1/graphql", {
method: "POST",
body: JSON.stringify({ query: updateMessageQuery, variables }),
headers: {
"Content-Type": "application/json",
"x-hasura-admin-secret": env.HASURA_ADMIN_API_KEY,
},
}).catch((error) => {
console.error(error);
});
const convUpdateVars = {
id: requestBody.event.data.new.conversation_id,
content: answer
}
await fetch(env.HASURA_GRAPHQL_ENGINE_ENDPOINT + "/v1/graphql", {
method: "POST",
body: JSON.stringify({ query: updateConversationquery, variables: convUpdateVars }),
headers: {
"Content-Type": "application/json",
"x-hasura-admin-secret": env.HASURA_ADMIN_API_KEY,
},
}).catch((error) => {
console.error(error);
});
return new Response(JSON.stringify({ status: "success" }), {
status: 200,
statusText: "success",
});
});
}
const updateMessageQuery = `
mutation chatCompletions($id: uuid = "", $data: messages_set_input) {
update_messages_by_pk(pk_columns: {id: $id}, _set: $data) {
id
content
}
}
`;
const updateConversationquery = `
mutation updateConversation($id: uuid = "", $content: String = "") {
update_conversations_by_pk(pk_columns: {id: $id}, _set: {last_text_message: $content}) {
id
}
}
`

View File

@ -0,0 +1,11 @@
name = "cloudlfare_worker"
main = "worker.ts"
compatibility_date = "2023-06-08"
workers_dev = true
[vars]
HASURA_GRAPHQL_ENGINE_ENDPOINT = "http://graphql-engine:8080"
HASURA_ADMIN_API_KEY = "myadminsecretkey"
LLM_INFERENCE_ENDPOINT="http://llm:8000/v1/chat/completions"
INFERENCE_API_KEY=""
PROJECT_ID = ""

@ -1 +0,0 @@
Subproject commit ace5d4cd50311799e4dfa34b37e31c8f4e0a53d9

View File

@ -0,0 +1,6 @@
{
"extends": "next/core-web-vitals",
"rules": {
"@next/next/no-img-element": "off"
}
}

45
web-client/.gitignore vendored Normal file
View File

@ -0,0 +1,45 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# apollo graphql
/graphql/generated
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
.env
# Firebase config file
app/_services/firebase/firebase_configs.json
# Sentry Auth Token
.sentryclirc

48
web-client/Dockerfile Normal file
View File

@ -0,0 +1,48 @@
FROM node:20-alpine AS base
# 1. Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Install dependencies based on the preferred package manager
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN npm install && npm i -g @nestjs/cli typescript ts-node
# 2. Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# This will do the trick, use the corresponding env file for each environment.
RUN npm run build
# 3. Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN \
addgroup -g 1001 -S nodejs; \
adduser -S nextjs -u 1001
COPY --from=builder /app/public ./public
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
USER nextjs
EXPOSE 3000
ENV PORT 3000
CMD ["npm", "start"]

102
web-client/README.md Normal file
View File

@ -0,0 +1,102 @@
# Jan Web
Jan Web is a Next.js application designed to provide users with the ability to interact with the Language Model (LLM) through chat or generate art using Stable Diffusion. This application runs as a single-page application (SPA) and is encapsulated within a Docker container for easy local deployment.
## 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
### Use as complete suite
For using our complete solution, check [this](https://github.com/janhq/jan)
### For interactive development
1. **Clone the Repository:**
```
git clone https://github.com/your-username/jan-web.git
cd jan-web
```
2. **Install dependencies:**
```
yarn
```
3. **Run development:**
```
yarn dev
```
4. **Access Jan Web:**
Open your web browser and navigate to `http://localhost:3000` to access the Jan Web application.
## Configuration
You can customize the endpoint of the Jan Web application through environment file. These options can be found in the `.env` file located in the project's root directory.
```env
// .env
KEYCLOAK_CLIENT_ID=hasura
KEYCLOAK_CLIENT_SECRET=**********
AUTH_ISSUER=http://localhost:8088/realms/hasura
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=my-secret
END_SESSION_URL=http://localhost:8088/realms/hasura/protocol/openid-connect/logout
REFRESH_TOKEN_URL=http://localhost:8088/realms/hasura/protocol/openid-connect/token
HASURA_ADMIN_TOKEN=myadminsecretkey
NEXT_PUBLIC_GRAPHQL_ENGINE_URL=localhost:8080
```
Replace above configuration with your actual infrastructure.
## Dependencies
|Library| Category | Version | Description |
|--|--|--|--|
| [next](https://nextjs.org/) | Framework | 13.4.10 |
| [typescript](https://www.typescriptlang.org/) | Language | 5.1.6 |
| [tailwindcss](https://tailwindcss.com/) | UI | 3.3.3 |
| [Tailwind UI](https://tailwindui.com/) | UI | |
| [react-hook-form](https://www.react-hook-form.com/) | UI | ^7.45.4 |
| [@headlessui/react](https://headlessui.com/) | UI | ^1.7.15 |
| [@heroicons/react](https://heroicons.com/) | UI | ^2.0.18 |
| [@tailwindcss/typography](https://tailwindcss.com/docs/typography-plugin) | UI | ^0.5.9 |
| [embla-carousel](https://www.embla-carousel.com/) | UI | ^8.0.0-rc11 |
| [@apollo/client](https://www.apollographql.com/docs/react/) | State management | ^3.8.1 |
| [mobx](https://mobx.js.org/README.html) | State management | ^6.10.0 |
| [mobx-react-lite](https://www.npmjs.com/package/mobx-react-lite) | State management | ^4.0.3 |
| [mobx-state-tree](https://mobx-state-tree.js.org/) | State management | ^5.1.8 |
## Deploy to Netlify
Clone this repository on own GitHub account and deploy to Netlify:
[![Netlify Deploy button](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/janhq/jan-web)
## Deploy to Vercel
Deploy Jan Web on Vercel in one click:
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/janhq/jan-web)
## 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-web/tree/6337306c54e735a4a5c2132dcd1377f21fd76a33).
## 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

@ -0,0 +1,41 @@
import classNames from "classnames";
import Image from "next/image";
type Props = {
title: string;
icon: string;
isLoading?: boolean;
onClick: () => void;
};
const ActionButton: React.FC<Props> = (props) => {
return (
<>
{!props.isLoading && (
<button
type="button"
className={classNames(
"rounded-xl flex items-center h-[40px] gap-1 bg-[#F3F4F6] px-2 text-xs font-normal text-gray-900 shadow-sm",
!props.isLoading && "hover:bg-indigo-100"
)}
onClick={props.onClick}
>
<Image src={props.icon} width={16} height={16} alt="" />
<span>{props.title}</span>
</button>
)}
{props.isLoading && (
<div className="w-[80px] flex flex-row justify-center items-center">
<Image
src="/icons/loading.svg"
width={32}
height={32}
alt="loading"
/>
</div>
)}
</>
);
};
export default ActionButton;

View File

@ -0,0 +1,69 @@
"use client";
import { useCallback } from "react";
import Image from "next/image";
import { useStore } from "@/_models/RootStore";
import { observer } from "mobx-react-lite";
import { MenuAdvancedPrompt } from "../MenuAdvancedPrompt";
import { useForm } from "react-hook-form";
import { useMutation } from "@apollo/client";
import { CreateMessageDocument, CreateMessageMutation } from "@/graphql";
export const AdvancedPrompt: React.FC = observer(() => {
const { register, handleSubmit } = useForm();
const { historyStore } = useStore();
const onAdvancedPrompt = useCallback(() => {
historyStore.toggleAdvancedPrompt();
}, []);
const [createMessageMutation] = useMutation<CreateMessageMutation>(
CreateMessageDocument
);
const onSubmit = (data: any) => {
historyStore.sendControlNetPrompt(
createMessageMutation,
data.prompt,
data.negativePrompt,
data.fileInput[0]
);
};
return (
<form
className={`${
historyStore.showAdvancedPrompt ? "w-[288px]" : "hidden"
} h-screen flex flex-col border-r border-gray-200`}
onSubmit={handleSubmit(onSubmit)}
>
<button
onClick={onAdvancedPrompt}
className="flex items-center mx-2 mt-3 mb-[10px] flex-none gap-1 text-xs leading-[18px] text-[#6B7280]"
>
<Image src={"/icons/chevron-left.svg"} width={20} height={20} alt="" />
<span className="font-semibold text-gray-500 text-xs">
BASIC PROMPT
</span>
</button>
<div className="flex flex-col justify-start flex-1 p-3 gap-[10px] overflow-x-hidden scroll">
<MenuAdvancedPrompt register={register} />
</div>
<div className="py-3 px-2 flex flex-none gap-3 items-center justify-between border-t border-gray-200">
<button className="w-1/2 flex items-center text-gray-900 py-2 px-3 rounded-lg gap-1 justify-center bg-gray-100 text-sm leading-5">
<Image
src={"/icons/unicorn_arrow-random.svg"}
width={16}
height={16}
alt=""
/>
Random
</button>
<button
className="w-1/2 flex items-center text-gray-900 justify-center py-2 px-3 rounded-lg gap-1 bg-yellow-300 text-sm leading-5"
onClick={(e) => handleSubmit(onSubmit)(e)}
>
Generate
</button>
</div>
</form>
);
});

View File

@ -0,0 +1,18 @@
import React, { useState } from "react";
import TogglableHeader from "../TogglableHeader";
const AdvancedPromptGenerationParams = () => {
const [expand, setExpand] = useState(true);
return (
<>
<TogglableHeader
icon={"/icons/unicorn_layers-alt.svg"}
title={"Generation Parameters"}
expand={expand}
onTitleClick={() => setExpand(!expand)}
/>
</>
);
};
export default AdvancedPromptGenerationParams;

View File

@ -0,0 +1,31 @@
import React, { useState } from "react";
import { DropdownsList } from "../DropdownList";
import TogglableHeader from "../TogglableHeader";
import { UploadFileImage } from "../UploadFileImage";
import { FieldValues, UseFormRegister } from "react-hook-form";
type Props = {
register: UseFormRegister<FieldValues>;
};
const AdvancedPromptImageUpload: React.FC<Props> = ({ register }) => {
const [expand, setExpand] = useState(true);
const data = ["test1", "test2", "test3", "test4"];
return (
<>
<TogglableHeader
icon={"/icons/ic_image.svg"}
title={"Image"}
expand={expand}
onTitleClick={() => setExpand(!expand)}
/>
<div className={`${expand ? "flex" : "hidden"} flex-col gap-[5px]`}>
<UploadFileImage register={register} />
<DropdownsList title="Control image with ControlNet:" data={data} />
</div>
</>
);
};
export default AdvancedPromptImageUpload;

View File

@ -0,0 +1,29 @@
import React, { useState } from "react";
import { DropdownsList } from "../DropdownList";
import TogglableHeader from "../TogglableHeader";
const AdvancedPromptResolution = () => {
const [expand, setExpand] = useState(true);
const data = ["512", "524", "536"];
const ratioData = ["1:1", "2:2", "3:3"];
return (
<>
<TogglableHeader
icon={"/icons/unicorn_layers-alt.svg"}
title={"Resolution"}
expand={expand}
onTitleClick={() => setExpand(!expand)}
/>
<div className={`${expand ? "flex" : "hidden"} flex-col gap-[5px]`}>
<div className="flex gap-3 py-3">
<DropdownsList data={data} title="Width" />
<DropdownsList data={data} title="Height" />
</div>
<DropdownsList title="Select ratio" data={ratioData} />
</div>
</>
);
};
export default AdvancedPromptResolution;

View File

@ -0,0 +1,41 @@
import React, { useState } from "react";
import TogglableHeader from "../TogglableHeader";
import { AdvancedTextArea } from "../AdvancedTextArea";
import { FieldValues, UseFormRegister } from "react-hook-form";
type Props = {
register: UseFormRegister<FieldValues>;
};
const AdvancedPromptText: React.FC<Props> = ({ register }) => {
const [expand, setExpand] = useState(true);
return (
<>
<TogglableHeader
icon={"/icons/messicon.svg"}
title={"Prompt"}
expand={expand}
onTitleClick={() => setExpand(!expand)}
/>
<div className={`${expand ? "flex" : "hidden"} flex-col gap-[5px]`}>
<AdvancedTextArea
formId="prompt"
height={80}
placeholder="Prompt"
title="Prompt"
register={register}
/>
<AdvancedTextArea
formId="negativePrompt"
height={80}
placeholder="Describe what you don't want in your image"
title="Negative Prompt"
register={register}
/>
</div>
</>
);
};
export default AdvancedPromptText;

View File

@ -0,0 +1,27 @@
import { FieldValues, UseFormRegister } from "react-hook-form";
type Props = {
formId?: string;
height: number;
title: string;
placeholder: string;
register: UseFormRegister<FieldValues>;
};
export const AdvancedTextArea: React.FC<Props> = ({
formId = "",
height,
placeholder,
title,
register,
}) => (
<div className="w-full flex flex-col pt-3 gap-1">
<label className="text-sm leading-5 text-gray-800">{title}</label>
<textarea
style={{ height: `${height}px` }}
className="rounded-lg py-[13px] px-5 border outline-none resize-none border-gray-300 bg-gray-50 placeholder:gray-400 text-sm font-normal"
placeholder={placeholder}
{...register(formId, { required: formId === "prompt" ? true : false })}
/>
</div>
);

View File

@ -0,0 +1,20 @@
import Image from "next/image";
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"}
width={15.63}
height={15.78}
alt=""
/>
<input
className="bg-inherit outline-0 w-full border-0 p-0 focus:ring-0"
placeholder="Search"
/>
</div>
);
};
export default Search;

View File

@ -0,0 +1,20 @@
import Image from "next/image";
import Link from "next/link";
type Props = {
name: string;
imageUrl: string;
};
const AiTypeCard: React.FC<Props> = ({ imageUrl, name }) => {
return (
<Link href={`/ai/${name}`} className='flex-1'>
<div className="flex-1 h-full bg-[#F3F4F6] flex items-center justify-center gap-[10px] py-[13px] rounded-[8px] px-4 active:opacity-50 hover:opacity-20">
<Image src={imageUrl} width={82} height={82} alt="" />
<span className="font-bold">{name}</span>
</div>
</Link>
);
};
export default AiTypeCard;

View File

@ -0,0 +1,79 @@
import Image from "next/image";
import { useState } from "react";
import { Light as SyntaxHighlighter } from "react-syntax-highlighter";
import js from "react-syntax-highlighter/dist/esm/languages/hljs/javascript";
import { atomOneDark } from "react-syntax-highlighter/dist/esm/styles/hljs";
import useGetModelApiInfo from "@/_hooks/useGetModelApiInfo";
SyntaxHighlighter.registerLanguage("javascript", js);
const ApiPane: React.FC = () => {
const [expend, setExpend] = useState(true);
const { data } = useGetModelApiInfo();
const [highlightCode, setHighlightCode] = useState(data[0]);
return (
<div className="h-full flex flex-col relative">
<div className="absolute top-0 left-0 h-full w-full overflow-x-hidden scroll">
<button
onClick={() => setExpend(!expend)}
className="flex items-center flex-none"
>
<Image
src={"/icons/unicorn_angle-down.svg"}
width={24}
height={24}
alt=""
/>
<span>Request</span>
</button>
<div
className={`${
expend ? "block" : "hidden"
} bg-[#1F2A37] rounded-lg w-full flex-1`}
>
<div className="p-2 flex justify-between flex-1">
<div className="flex">
{data.map((item, index) => (
<button
className={`py-1 text-xs text-[#9CA3AF] px-2 flex gap-[10px] rounded ${
highlightCode?.type === item.type
? "bg-[#374151] text-white"
: ""
}`}
key={index}
onClick={() => setHighlightCode(item)}
>
{item.type}
</button>
))}
</div>
<button
onClick={() =>
navigator.clipboard.writeText(highlightCode?.stringCode)
}
>
<Image
src={"/icons/unicorn_clipboard-alt.svg"}
width={24}
height={24}
alt=""
/>
</button>
</div>
<SyntaxHighlighter
className="w-full bg-transparent overflow-x-hidden scroll resize-none"
language="jsx"
style={atomOneDark}
customStyle={{ padding: "12px", background: "transparent" }}
wrapLongLines={true}
>
{highlightCode?.stringCode}
</SyntaxHighlighter>
</div>
</div>
</div>
);
};
export default ApiPane;

View File

@ -0,0 +1,15 @@
type Props = {
title: string;
description: string;
};
export const ApiStep: React.FC<Props> = ({ description, title }) => {
return (
<div className="gap-2 flex flex-col">
<span className="text-[#8A8A8A]">{title}</span>
<div className="flex flex-col gap-[10px] p-[18px] bg-[#F9F9F9] overflow-y-hidden">
<pre className="text-sm leading-5 text-black">{description}</pre>
</div>
</div>
);
};

View File

@ -0,0 +1,45 @@
import Image from "next/image";
import React, { PropsWithChildren } from "react";
type PropType = PropsWithChildren<
React.DetailedHTMLProps<
React.ButtonHTMLAttributes<HTMLButtonElement>,
HTMLButtonElement
>
>;
export const PrevButton: React.FC<PropType> = (props) => {
const { children, ...restProps } = props;
return (
<button
className="embla__button embla__button--prev"
type="button"
{...restProps}
>
<Image
className="rotate-180"
src={"/icons/chevron-right.svg"}
width={20}
height={20}
alt=""
/>
{children}
</button>
);
};
export const NextButton: React.FC<PropType> = (props) => {
const { children, ...restProps } = props;
return (
<button
className="embla__button embla__button--next"
type="button"
{...restProps}
>
<Image src={"/icons/chevron-right.svg"} width={20} height={20} alt="" />
{children}
</button>
);
};

View File

@ -0,0 +1,25 @@
import { useTheme } from "next-themes";
import { SunIcon, MoonIcon } from "@heroicons/react/24/outline";
export const ThemeChanger: React.FC = () => {
const { theme, setTheme, systemTheme } = useTheme();
const currentTheme = theme === "system" ? systemTheme : theme;
if (currentTheme === "dark") {
return (
<SunIcon
className="h-6 w-6"
aria-hidden="true"
onClick={() => setTheme("light")}
/>
);
}
return (
<MoonIcon
className="h-6 w-6"
aria-hidden="true"
onClick={() => setTheme("dark")}
/>
);
};

View File

@ -0,0 +1,186 @@
import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
import { useStore } from "@/_models/RootStore";
import { observer } from "mobx-react-lite";
import { ChatMessage, MessageStatus, MessageType } from "@/_models/ChatMessage";
import SimpleImageMessage from "../SimpleImageMessage";
import SimpleTextMessage from "../SimpleTextMessage";
import { Instance } from "mobx-state-tree";
import { GenerativeSampleContainer } from "../GenerativeSampleContainer";
import { AiModelType } from "@/_models/Product";
import SampleLlmContainer from "@/_components/SampleLlmContainer";
import SimpleControlNetMessage from "../SimpleControlNetMessage";
import {
GetConversationMessagesQuery,
GetConversationMessagesDocument,
} from "@/graphql";
import { useLazyQuery } from "@apollo/client";
import LoadingIndicator from "../LoadingIndicator";
import StreamTextMessage from "../StreamTextMessage";
type Props = {
onPromptSelected: (prompt: string) => void;
};
export const ChatBody: React.FC<Props> = observer(({ onPromptSelected }) => {
const ref = useRef<HTMLDivElement>(null);
const scrollRef = useRef<HTMLDivElement>(null);
const [height, setHeight] = useState(0);
const { historyStore } = useStore();
const refSmooth = useRef<HTMLDivElement>(null);
const [heightContent, setHeightContent] = useState(0);
const refContent = useRef<HTMLDivElement>(null);
const convo = historyStore.getActiveConversation();
const [getConversationMessages] = useLazyQuery<GetConversationMessagesQuery>(
GetConversationMessagesDocument
);
useEffect(() => {
refSmooth.current?.scrollIntoView({ behavior: "instant" });
}, [heightContent]);
useLayoutEffect(() => {
if (refContent.current) {
setHeightContent(refContent.current?.offsetHeight);
}
});
useLayoutEffect(() => {
if (!ref.current) return;
setHeight(ref.current?.offsetHeight);
}, []);
const loadFunc = () => {
historyStore.fetchMoreMessages(getConversationMessages);
};
const messages = historyStore.getActiveMessages();
const shouldShowSampleContainer = messages.length === 0;
const shouldShowImageSampleContainer =
shouldShowSampleContainer &&
convo &&
convo.product.type === AiModelType.GenerativeArt;
const model = convo?.product;
const handleScroll = () => {
if (!scrollRef.current) return;
if (
scrollRef.current?.clientHeight - scrollRef.current?.scrollTop + 1 >=
scrollRef.current?.scrollHeight
) {
loadFunc();
}
};
useEffect(() => {
loadFunc();
scrollRef.current?.addEventListener("scroll", handleScroll);
return () => {
scrollRef.current?.removeEventListener("scroll", handleScroll);
};
}, [scrollRef.current]);
return (
<div className="flex-grow flex flex-col h-fit" ref={ref}>
{shouldShowSampleContainer && model ? (
shouldShowImageSampleContainer ? (
<GenerativeSampleContainer
model={convo?.product}
onPromptSelected={onPromptSelected}
/>
) : (
<SampleLlmContainer
model={convo?.product}
onPromptSelected={onPromptSelected}
/>
)
) : (
<div
className="flex flex-col-reverse scroll"
style={{
height: height + "px",
overflowX: "hidden",
}}
ref={scrollRef}
>
<div
className="flex flex-col justify-end gap-8 py-2"
ref={refContent}
>
{messages.map((message, index) => renderItem(index, message))}
<div ref={refSmooth}>
{convo?.isWaitingForModelResponse && (
<div className="w-[50px] h-[50px] px-2 flex flex-row items-start justify-start">
<LoadingIndicator />
</div>
)}
</div>
</div>
</div>
)}
</div>
);
});
const renderItem = (
index: number,
{
id,
messageType,
senderAvatarUrl,
senderName,
createdAt,
imageUrls,
text,
status,
}: Instance<typeof ChatMessage>
) => {
switch (messageType) {
case MessageType.ImageWithText:
return (
<SimpleControlNetMessage
key={index}
avatarUrl={senderAvatarUrl ?? "/icons/app_icon.svg"}
senderName={senderName}
createdAt={createdAt}
imageUrls={imageUrls ?? []}
text={text ?? ""}
/>
);
case MessageType.Image:
return (
<SimpleImageMessage
key={index}
avatarUrl={senderAvatarUrl ?? "/icons/app_icon.svg"}
senderName={senderName}
createdAt={createdAt}
imageUrls={imageUrls ?? []}
text={text}
/>
);
case MessageType.Text:
return status === MessageStatus.Ready ? (
<SimpleTextMessage
key={index}
avatarUrl={senderAvatarUrl ?? "/icons/app_icon.svg"}
senderName={senderName}
createdAt={createdAt}
text={text}
/>
) : (
<StreamTextMessage
key={index}
id={id}
avatarUrl={senderAvatarUrl ?? "/icons/app_icon.svg"}
senderName={senderName}
createdAt={createdAt}
text={text}
/>
);
default:
return null;
}
};

View File

@ -0,0 +1,91 @@
"use client";
import { useState, useEffect } from "react";
import { ChatBody } from "../ChatBody";
import { InputToolbar } from "../InputToolbar";
import { UserToolbar } from "../UserToolbar";
import ModelMenu from "../ModelMenu";
import { useStore } from "@/_models/RootStore";
import { observer } from "mobx-react-lite";
import ConfirmDeleteConversationModal from "../ConfirmDeleteConversationModal";
import { ModelDetailSideBar } from "../ModelDetailSideBar";
import NewChatBlankState from "../NewChatBlankState";
import useGetCurrentUser from "@/_hooks/useGetCurrentUser";
import {
DeleteConversationMutation,
DeleteConversationDocument,
} from "@/graphql";
import { useMutation } from "@apollo/client";
const ChatContainer: React.FC = observer(() => {
const [prefillPrompt, setPrefillPrompt] = useState("");
const { historyStore } = useStore();
const { user } = useGetCurrentUser();
const showBodyChat = historyStore.activeConversationId != null;
const conversation = historyStore.getActiveConversation();
const [deleteConversation] = useMutation<DeleteConversationMutation>(
DeleteConversationDocument
);
useEffect(() => {
if (!user) {
historyStore.clearAllConversations();
}
}, [user]);
const [open, setOpen] = useState(false);
const onConfirmDelete = () => {
setPrefillPrompt("");
historyStore.closeModelDetail();
if (conversation?.id) {
deleteConversation({ variables: { id: conversation.id } }).then(() =>
historyStore.deleteConversationById(conversation.id)
);
}
setOpen(false);
};
const onSuggestPromptClick = (prompt: string) => {
if (prompt !== prefillPrompt) {
setPrefillPrompt(prompt);
}
};
return (
<div className="flex flex-1 h-full overflow-y-hidden">
<ConfirmDeleteConversationModal
open={open}
setOpen={setOpen}
onConfirmDelete={onConfirmDelete}
/>
{showBodyChat ? (
<div className="flex-1 flex flex-col w-full">
<div className="flex w-full overflow-hidden flex-shrink-0 px-3 py-1 border-b dark:bg-gray-950 border-gray-200 bg-white shadow-sm sm:px-3 lg:px-3">
{/* Separator */}
<div
className="h-full w-px bg-gray-200 lg:hidden"
aria-hidden="true"
/>
<div className="flex justify-between self-stretch flex-1">
<UserToolbar />
<ModelMenu
onDeleteClick={() => setOpen(true)}
onCreateConvClick={() => {}}
/>
</div>
</div>
<div className="flex flex-col h-full px-1 sm:px-2 lg:px-3 overflow-hidden">
<ChatBody onPromptSelected={onSuggestPromptClick} />
<InputToolbar prefillPrompt={prefillPrompt} />
</div>
</div>
) : (
<NewChatBlankState />
)}
<ModelDetailSideBar onPromptClick={onSuggestPromptClick} />
</div>
);
});
export default ChatContainer;

View File

@ -0,0 +1,39 @@
import { useStore } from "@/_models/RootStore";
import Image from "next/image";
import React from "react";
type Props = {
imageUrl: string;
isSelected: boolean;
conversationId: string;
};
const CompactHistoryItem: React.FC<Props> = ({
imageUrl,
isSelected,
conversationId,
}) => {
const { historyStore } = useStore();
const onClick = () => {
historyStore.setActiveConversationId(conversationId);
};
return (
<button
onClick={onClick}
className={`${
isSelected ? "bg-gray-100" : "bg-transparent"
} p-2 rounded-lg`}
>
<Image
className="rounded-full"
src={imageUrl}
width={36}
height={36}
alt=""
/>
</button>
);
};
export default React.memo(CompactHistoryItem);

View File

@ -0,0 +1,16 @@
import React from "react";
import JanImage from "../JanImage";
type Props = {
onClick: () => void;
};
const CompactLogo: React.FC<Props> = ({ onClick }) => {
return (
<button onClick={onClick}>
<JanImage imageUrl="/icons/app_icon.svg" width={28} height={28} />
</button>
);
};
export default React.memo(CompactLogo);

View File

@ -0,0 +1,33 @@
"use client"
import { observer } from "mobx-react-lite";
import CompactLogo from "../CompactLogo";
import CompactHistoryItem from "../CompactHistoryItem";
import { useStore } from "@/_models/RootStore";
export const CompactSideBar: React.FC = observer(() => {
const { historyStore } = useStore();
const onLogoClick = () => {
historyStore.clearActiveConversationId();
};
return (
<div
className={`${
!historyStore.showAdvancedPrompt ? "hidden" : "block"
} h-screen border-r border-gray-300 flex flex-col items-center pt-3 gap-3`}
>
<CompactLogo onClick={onLogoClick} />
<div className="flex flex-col gap-1 mx-1 mt-3 overflow-x-hidden">
{historyStore.conversations.map(({ id, product: aiModel }) => (
<CompactHistoryItem
key={id}
conversationId={id}
imageUrl={aiModel.avatarUrl ?? ""}
isSelected={historyStore.activeConversationId === id}
/>
))}
</div>
</div>
);
});

View File

@ -0,0 +1,99 @@
import { Dialog, Transition } from "@headlessui/react";
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
import React, { Fragment, useRef, useState } from "react";
type Props = {
open: boolean;
setOpen: (open: boolean) => void;
onConfirmDelete: () => void;
};
const ConfirmDeleteConversationModal: React.FC<Props> = ({
open,
setOpen,
onConfirmDelete,
}) => {
const cancelButtonRef = useRef(null);
return (
<Transition.Root show={open} as={Fragment}>
<Dialog
as="div"
className="relative z-10"
initialFocus={cancelButtonRef}
onClose={setOpen}
>
<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">
<ExclamationTriangleIcon
className="h-6 w-6 text-red-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"
>
Delete Conversation
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-gray-500">
Are you sure you want to delete this conversation? All
of messages will be permanently removed from our servers
forever. This action cannot be undone.
</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={() => onConfirmDelete()}
>
Delete
</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={() => setOpen(false)}
ref={cancelButtonRef}
>
Cancel
</button>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
};
export default ConfirmDeleteConversationModal;

View File

@ -0,0 +1,90 @@
import React, { Fragment } from "react";
import { Dialog, Transition } from "@headlessui/react";
import { QuestionMarkCircleIcon } from "@heroicons/react/24/outline";
type Props = {
open: boolean;
setOpen: (open: boolean) => void;
onConfirm: () => void;
};
const ConfirmSignOutModal: React.FC<Props> = ({ open, setOpen, onConfirm }) => {
const onLogOutClick = () => {
onConfirm();
setOpen(false);
};
return (
<Transition.Root show={open} as={Fragment}>
<Dialog as="div" className="relative z-10" onClose={setOpen}>
<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 want to logout?
</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={() => setOpen(false)}
>
Cancel
</button>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
};
export default React.memo(ConfirmSignOutModal);

View File

@ -0,0 +1,47 @@
import React from "react";
import Image from "next/image";
import {
ProductDetailFragment,
} from "@/graphql";
import useCreateConversation from "@/_hooks/useCreateConversation";
type Props = {
product: ProductDetailFragment;
};
const ConversationalCard: React.FC<Props> = ({ product }) => {
const { requestCreateConvo } = useCreateConversation();
const { name, image_url, description } = product;
return (
<button
onClick={() =>
requestCreateConvo(product)
}
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"
>
<div className="flex flex-col gap-2 box-border">
<Image
width={32}
height={32}
src={image_url ?? ""}
className="rounded-full"
alt=""
/>
<h2 className="text-gray-900 font-semibold dark:text-white line-clamp-1 mt-2">
{name}
</h2>
<span className="text-gray-600 mt-1 font-normal line-clamp-2">
{description}
</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="" />
32.2k runs
</span>
</button>
);
};
export default React.memo(ConversationalCard);

View File

@ -0,0 +1,25 @@
import ConversationalCard from "../ConversationalCard";
import Image from "next/image";
import { ProductDetailFragment } from "@/graphql";
type Props = {
products: ProductDetailFragment[];
};
const ConversationalList: React.FC<Props> = ({ products }) => (
<>
<div className="flex items-center gap-3 mt-8 mb-2">
<Image src={"/icons/messicon.svg"} width={24} height={24} alt="" />
<span className="font-semibold text-gray-900 dark:text-white">
Conversational
</span>
</div>
<div className="mt-2 flex w-full gap-2 overflow-x-scroll scroll overflow-hidden">
{products.map((item) => (
<ConversationalCard key={item.name} product={item} />
))}
</div>
</>
);
export default ConversationalList;

View File

@ -0,0 +1,33 @@
import { ApiStep } from "../ApiStep";
const DescriptionPane: React.FC = () => {
const data = [
{
title: "Install the Node.js client:",
description: "npm install replicate",
},
{
title:
"Next, copy your API token and authenticate by setting it as an environment variable:",
description:
"export REPLICATE_API_TOKEN=r8_*************************************",
},
{
title: "lorem ipsum dolor asimet",
description: "come codes here",
},
];
return (
<div className="flex flex-col gap-4 w-[full]">
<h2 className="text-[20px] tracking-[-0.4px] leading-[25px]">
Run the model
</h2>
{data.map((item, index) => (
<ApiStep key={index} {...item} />
))}
</div>
);
};
export default DescriptionPane;

View File

@ -0,0 +1,31 @@
import React from "react";
import Link from "next/link";
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="" />
Discord
</Link>
</div>
);
export default React.memo(DiscordContainer);

View File

@ -0,0 +1,37 @@
import React, { useState } from "react";
import { useStore } from "@/_models/RootStore";
type Props = {
targetRef: React.RefObject<HTMLDivElement>;
};
export const Draggable: React.FC<Props> = ({ targetRef }) => {
const { historyStore } = useStore();
const [initialPos, setInitialPos] = useState<number | null>(null);
const [initialSize, setInitialSize] = useState<number | null>(null);
const [width, setWidth] = useState<number>(0);
const initial = (e: React.DragEvent<HTMLDivElement>) => {
setInitialPos(e.clientX);
setInitialSize(targetRef.current?.offsetWidth ?? 0);
};
const resize = (e: React.DragEvent<HTMLDivElement>) => {
if (initialPos !== null && initialSize !== null) {
setWidth(initialSize - (e.clientX - initialPos));
targetRef.current!.style.width = `${width}px`;
}
if (width <= 270) {
historyStore.closeModelDetail();
}
};
return (
<div
className="absolute left-0 top-0 w-1 h-full cursor-ew-resize"
draggable={true}
onDrag={resize}
onDragStart={initial}
></div>
);
};

View File

@ -0,0 +1,64 @@
import { Fragment, useState } from "react";
import { Menu, Transition } from "@headlessui/react";
import Image from "next/image";
function classNames(...classes: any) {
return classes.filter(Boolean).join(" ");
}
type Props = {
title: string;
data: string[];
};
export const DropdownsList: React.FC<Props> = ({ data, title }) => {
const [checked, setChecked] = useState(data[0]);
return (
<Menu as="div" className="relative w-full text-left">
<div className="pt-2 gap-2 flex flex-col">
<h2 className="text-[#111928] text-sm">{title}</h2>
<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"}
width={12}
height={12}
alt=""
/>
</Menu.Button>
</div>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute right-0 z-10 mt-2 w-full origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
<div className="py-1">
{data.map((item, index) => (
<Menu.Item key={index}>
{({ active }) => (
<a
onClick={() => setChecked(item)}
href="#"
className={classNames(
active ? "bg-gray-100 text-gray-900" : "text-gray-700",
"block px-4 py-2 text-sm"
)}
>
{item}
</a>
)}
</Menu.Item>
))}
</div>
</Menu.Items>
</Transition>
</Menu>
);
};

View File

@ -0,0 +1,29 @@
import Image from "next/image";
import Link from "next/link";
// DEPRECATED
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="" />
<span>Jan</span>
</div>
<div className="flex gap-4 my-6">
<Link
href="/privacy"
className="cursor-pointer"
>
Privacy
</Link>
<span>&#8226;</span>
<Link
href="/support"
className="cursor-pointer"
>
Support
</Link>
</div>
</div>
);
}

View File

@ -0,0 +1,31 @@
import useCreateConversation from "@/_hooks/useCreateConversation";
import { ProductDetailFragment } from "@/graphql";
import { useCallback } from "react";
type Props = {
product: ProductDetailFragment;
};
const GenerateImageCard: React.FC<Props> = ({ product }) => {
const { name, image_url } = product;
const { requestCreateConvo } = useCreateConversation();
const onClick = useCallback(() => {
requestCreateConvo(product);
}, [product]);
return (
<button onClick={onClick} className="relative active:opacity-50 text-left">
<img
src={image_url ?? ""}
alt=""
className="w-full h-full rounded-[8px] bg-gray-200 group-hover:opacity-75 object-cover object-center"
/>
<div className="absolute bottom-0 rounded-br-[8px] rounded-bl-[8px] bg-[rgba(0,0,0,0.5)] w-full p-3">
<span className="text-white font-semibold">{name}</span>
</div>
</button>
);
};
export default GenerateImageCard;

View File

@ -0,0 +1,27 @@
import Image from "next/image";
import GenerateImageCard from "../GenerateImageCard";
import { ProductDetailFragment } from "@/graphql";
type Props = {
products: ProductDetailFragment[];
};
const GenerateImageList: React.FC<Props> = ({ products }) => (
<div className="pb-4">
<div className="flex mt-4 justify-between">
<div className="gap-4 flex items-center">
<Image src={"icons/ic_image.svg"} width={20} height={20} alt="" />
<h2 className="text-gray-900 font-bold dark:text-white">
Generate Images
</h2>
</div>
</div>
<div className="mt-2 grid grid-cols-2 gap-6 sm:gap-x-6 md:grid-cols-4 md:gap-8">
{products.map((item) => (
<GenerateImageCard key={item.name} product={item} />
))}
</div>
</div>
);
export default GenerateImageList;

View File

@ -0,0 +1,52 @@
import JanWelcomeTitle from "../JanWelcomeTitle";
import { Product } from "@/_models/Product";
import { Instance } from "mobx-state-tree";
import { GetProductPromptsQuery, GetProductPromptsDocument } from "@/graphql";
import { useQuery } from "@apollo/client";
type Props = {
model: Instance<typeof Product>;
onPromptSelected: (prompt: string) => void;
};
export const GenerativeSampleContainer: React.FC<Props> = ({
model,
onPromptSelected,
}) => {
const { loading, error, data } = useQuery<GetProductPromptsQuery>(
GetProductPromptsDocument,
{
variables: { productSlug: model.id },
}
);
return (
<div className="flex flex-col max-w-2xl flex-shrink-0 mx-auto mt-6">
<JanWelcomeTitle
title={model.name}
description={model.modelDescription ?? ""}
/>
<div className="flex flex-col">
<h2 className="font-semibold text-xl leading-6 tracking-[-0.4px] mb-5">
Create now
</h2>
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3">
{data?.prompts.map((item) => (
<button
key={item.slug}
onClick={() => onPromptSelected(item.content ?? "")}
className="w-full h-full"
>
<img
style={{ objectFit: "cover" }}
className="w-full h-full rounded col-span-1 flex flex-col"
src={item.image_url ?? ""}
alt=""
/>
</button>
))}
</div>
</div>
</div>
);
};

Some files were not shown because too many files have changed in this diff Show More