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:
parent
3826e30663
commit
86f0ffc7d1
6
.gitmodules
vendored
6
.gitmodules
vendored
@ -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"]
|
||||
path = jan-inference/sd/sd_cpp
|
||||
url = https://github.com/leejet/stable-diffusion.cpp
|
||||
|
||||
@ -1 +0,0 @@
|
||||
Subproject commit 4c44efe7187b15cd2b65f3c8f6a089982941dbd7
|
||||
4
app-backend/.gitignore
vendored
Normal file
4
app-backend/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
.DS_Store
|
||||
.env
|
||||
.env_postgresql
|
||||
worker/node_modules/.mf
|
||||
59
app-backend/README.md
Normal file
59
app-backend/README.md
Normal 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
|
||||
|
||||
[](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/)
|
||||
52
app-backend/docker-compose.yml
Normal file
52
app-backend/docker-compose.yml
Normal 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:
|
||||
7
app-backend/hasura/config.yaml
Normal file
7
app-backend/hasura/config.yaml
Normal 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
|
||||
20
app-backend/hasura/metadata/actions.graphql
Normal file
20
app-backend/hasura/metadata/actions.graphql
Normal 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!
|
||||
}
|
||||
|
||||
32
app-backend/hasura/metadata/actions.yaml
Normal file
32
app-backend/hasura/metadata/actions.yaml
Normal 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: []
|
||||
1
app-backend/hasura/metadata/allow_list.yaml
Normal file
1
app-backend/hasura/metadata/allow_list.yaml
Normal file
@ -0,0 +1 @@
|
||||
[]
|
||||
1
app-backend/hasura/metadata/api_limits.yaml
Normal file
1
app-backend/hasura/metadata/api_limits.yaml
Normal file
@ -0,0 +1 @@
|
||||
{}
|
||||
11
app-backend/hasura/metadata/backend_configs.yaml
Normal file
11
app-backend/hasura/metadata/backend_configs.yaml
Normal 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
|
||||
1
app-backend/hasura/metadata/cron_triggers.yaml
Normal file
1
app-backend/hasura/metadata/cron_triggers.yaml
Normal file
@ -0,0 +1 @@
|
||||
[]
|
||||
9
app-backend/hasura/metadata/databases/databases.yaml
Normal file
9
app-backend/hasura/metadata/databases/databases.yaml
Normal 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"
|
||||
@ -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: ""
|
||||
@ -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: ""
|
||||
@ -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: ""
|
||||
@ -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: ""
|
||||
@ -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}}'
|
||||
@ -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: ""
|
||||
@ -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: ""
|
||||
@ -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: ""
|
||||
@ -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"
|
||||
@ -0,0 +1 @@
|
||||
disabled_for_roles: []
|
||||
1
app-backend/hasura/metadata/inherited_roles.yaml
Normal file
1
app-backend/hasura/metadata/inherited_roles.yaml
Normal file
@ -0,0 +1 @@
|
||||
[]
|
||||
1
app-backend/hasura/metadata/metrics_config.yaml
Normal file
1
app-backend/hasura/metadata/metrics_config.yaml
Normal file
@ -0,0 +1 @@
|
||||
{}
|
||||
1
app-backend/hasura/metadata/network.yaml
Normal file
1
app-backend/hasura/metadata/network.yaml
Normal file
@ -0,0 +1 @@
|
||||
{}
|
||||
1
app-backend/hasura/metadata/opentelemetry.yaml
Normal file
1
app-backend/hasura/metadata/opentelemetry.yaml
Normal file
@ -0,0 +1 @@
|
||||
{}
|
||||
1
app-backend/hasura/metadata/query_collections.yaml
Normal file
1
app-backend/hasura/metadata/query_collections.yaml
Normal file
@ -0,0 +1 @@
|
||||
[]
|
||||
1
app-backend/hasura/metadata/remote_schemas.yaml
Normal file
1
app-backend/hasura/metadata/remote_schemas.yaml
Normal file
@ -0,0 +1 @@
|
||||
[]
|
||||
1
app-backend/hasura/metadata/rest_endpoints.yaml
Normal file
1
app-backend/hasura/metadata/rest_endpoints.yaml
Normal file
@ -0,0 +1 @@
|
||||
[]
|
||||
1
app-backend/hasura/metadata/version.yaml
Normal file
1
app-backend/hasura/metadata/version.yaml
Normal file
@ -0,0 +1 @@
|
||||
version: 3
|
||||
@ -0,0 +1 @@
|
||||
DROP TABLE "public"."collections";
|
||||
@ -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;
|
||||
@ -0,0 +1 @@
|
||||
DROP TABLE "public"."products";
|
||||
@ -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;
|
||||
@ -0,0 +1 @@
|
||||
DROP TABLE "public"."prompts";
|
||||
@ -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;
|
||||
@ -0,0 +1 @@
|
||||
DROP TABLE "public"."conversations";
|
||||
@ -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;
|
||||
@ -0,0 +1 @@
|
||||
DROP TABLE "public"."messages";
|
||||
@ -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;
|
||||
@ -0,0 +1 @@
|
||||
DROP TABLE "public"."message_medias";
|
||||
@ -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;
|
||||
@ -0,0 +1 @@
|
||||
DROP TABLE "public"."collection_products";
|
||||
@ -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;
|
||||
@ -0,0 +1 @@
|
||||
DROP TABLE "public"."product_prompts";
|
||||
@ -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;
|
||||
@ -0,0 +1 @@
|
||||
alter table "public"."collection_products" drop constraint "collection_products_collection_id_fkey";
|
||||
@ -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;
|
||||
@ -0,0 +1 @@
|
||||
alter table "public"."collection_products" drop constraint "collection_products_product_id_fkey";
|
||||
@ -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;
|
||||
@ -0,0 +1 @@
|
||||
alter table "public"."messages" drop column "status";
|
||||
@ -0,0 +1,2 @@
|
||||
alter table "public"."messages" add column "status" varchar
|
||||
null default 'ready';
|
||||
@ -0,0 +1 @@
|
||||
alter table "public"."messages" drop column "prompt_cache";
|
||||
@ -0,0 +1,2 @@
|
||||
alter table "public"."messages" add column "prompt_cache" jsonb
|
||||
null;
|
||||
@ -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."}', '👋I’m 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 👋')
|
||||
@ -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.');
|
||||
@ -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');
|
||||
16
app-backend/hasura/seeds/jandb/1692711599434_promptsSeed.sql
Normal file
16
app-backend/hasura/seeds/jandb/1692711599434_promptsSeed.sql
Normal 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');
|
||||
@ -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
23
app-backend/sample.env
Normal 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"
|
||||
1
app-backend/sample.env_postgresql
Normal file
1
app-backend/sample.env_postgresql
Normal file
@ -0,0 +1 @@
|
||||
POSTGRES_PASSWORD=postgrespassword
|
||||
16
app-backend/worker/Dockerfile
Normal file
16
app-backend/worker/Dockerfile
Normal 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"]
|
||||
197
app-backend/worker/worker.ts
Normal file
197
app-backend/worker/worker.ts
Normal 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
|
||||
}
|
||||
}
|
||||
`
|
||||
11
app-backend/worker/wrangler.toml
Normal file
11
app-backend/worker/wrangler.toml
Normal 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
|
||||
6
web-client/.eslintrc.json
Normal file
6
web-client/.eslintrc.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals",
|
||||
"rules": {
|
||||
"@next/next/no-img-element": "off"
|
||||
}
|
||||
}
|
||||
45
web-client/.gitignore
vendored
Normal file
45
web-client/.gitignore
vendored
Normal 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
48
web-client/Dockerfile
Normal 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
102
web-client/README.md
Normal 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:
|
||||
|
||||
[](https://app.netlify.com/start/deploy?repository=https://github.com/janhq/jan-web)
|
||||
|
||||
## Deploy to Vercel
|
||||
|
||||
Deploy Jan Web on Vercel in one click:
|
||||
|
||||
[](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! 🚀🎨🤖
|
||||
41
web-client/app/_components/ActionButton/index.tsx
Normal file
41
web-client/app/_components/ActionButton/index.tsx
Normal 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;
|
||||
69
web-client/app/_components/AdvancedPrompt/index.tsx
Normal file
69
web-client/app/_components/AdvancedPrompt/index.tsx
Normal 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>
|
||||
);
|
||||
});
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
41
web-client/app/_components/AdvancedPromptText/index.tsx
Normal file
41
web-client/app/_components/AdvancedPromptText/index.tsx
Normal 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;
|
||||
27
web-client/app/_components/AdvancedTextArea/index.tsx
Normal file
27
web-client/app/_components/AdvancedTextArea/index.tsx
Normal 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>
|
||||
);
|
||||
20
web-client/app/_components/AiSearch/index.tsx
Normal file
20
web-client/app/_components/AiSearch/index.tsx
Normal 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;
|
||||
20
web-client/app/_components/AiTypeCard/index.tsx
Normal file
20
web-client/app/_components/AiTypeCard/index.tsx
Normal 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;
|
||||
79
web-client/app/_components/ApiPane/index.tsx
Normal file
79
web-client/app/_components/ApiPane/index.tsx
Normal 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;
|
||||
15
web-client/app/_components/ApiStep/index.tsx
Normal file
15
web-client/app/_components/ApiStep/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
45
web-client/app/_components/ButtonSlider/index.tsx
Normal file
45
web-client/app/_components/ButtonSlider/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
25
web-client/app/_components/ChangeTheme/index.tsx
Normal file
25
web-client/app/_components/ChangeTheme/index.tsx
Normal 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")}
|
||||
/>
|
||||
);
|
||||
};
|
||||
186
web-client/app/_components/ChatBody/index.tsx
Normal file
186
web-client/app/_components/ChatBody/index.tsx
Normal 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;
|
||||
}
|
||||
};
|
||||
91
web-client/app/_components/ChatContainer/index.tsx
Normal file
91
web-client/app/_components/ChatContainer/index.tsx
Normal 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;
|
||||
39
web-client/app/_components/CompactHistoryItem/index.tsx
Normal file
39
web-client/app/_components/CompactHistoryItem/index.tsx
Normal 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);
|
||||
16
web-client/app/_components/CompactLogo/index.tsx
Normal file
16
web-client/app/_components/CompactLogo/index.tsx
Normal 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);
|
||||
33
web-client/app/_components/CompactSideBar/index.tsx
Normal file
33
web-client/app/_components/CompactSideBar/index.tsx
Normal 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>
|
||||
);
|
||||
});
|
||||
@ -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;
|
||||
90
web-client/app/_components/ConfirmSignOutModal/index.tsx
Normal file
90
web-client/app/_components/ConfirmSignOutModal/index.tsx
Normal 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);
|
||||
47
web-client/app/_components/ConversationalCard/index.tsx
Normal file
47
web-client/app/_components/ConversationalCard/index.tsx
Normal 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);
|
||||
25
web-client/app/_components/ConversationalList/index.tsx
Normal file
25
web-client/app/_components/ConversationalList/index.tsx
Normal 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;
|
||||
33
web-client/app/_components/DescriptionPane/index.tsx
Normal file
33
web-client/app/_components/DescriptionPane/index.tsx
Normal 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;
|
||||
31
web-client/app/_components/DiscordContainer/index.tsx
Normal file
31
web-client/app/_components/DiscordContainer/index.tsx
Normal 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);
|
||||
37
web-client/app/_components/Draggable/index.tsx
Normal file
37
web-client/app/_components/Draggable/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
64
web-client/app/_components/DropdownList/index.tsx
Normal file
64
web-client/app/_components/DropdownList/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
29
web-client/app/_components/Footer/index.tsx
Normal file
29
web-client/app/_components/Footer/index.tsx
Normal 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>•</span>
|
||||
<Link
|
||||
href="/support"
|
||||
className="cursor-pointer"
|
||||
>
|
||||
Support
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
31
web-client/app/_components/GenerateImageCard/index.tsx
Normal file
31
web-client/app/_components/GenerateImageCard/index.tsx
Normal 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;
|
||||
27
web-client/app/_components/GenerateImageList/index.tsx
Normal file
27
web-client/app/_components/GenerateImageList/index.tsx
Normal 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;
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user