diff --git a/README.md b/README.md
index 5464ff1c1..8405eb74e 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
-# Jan - Personal AI
+# Jan - Own Your AI
-
+
@@ -21,7 +21,7 @@
> ⚠️ **Jan is currently in Development**: Expect breaking changes and bugs!
-Jan is a free, open-source alternative to OpenAI that runs on your personal computer.
+Jan is a free, open-source alternative to OpenAI's platform that runs on a local folder of open-format files.
**Jan runs on any hardware.** From PCs to multi-GPU clusters, Jan supports universal architectures:
diff --git a/core/src/events.ts b/core/src/events.ts
index e962dead4..fa0bef15c 100644
--- a/core/src/events.ts
+++ b/core/src/events.ts
@@ -12,44 +12,16 @@ export enum EventName {
OnDownloadError = "onDownloadError",
}
-export type MessageHistory = {
- role: string;
- content: string;
-};
-/**
- * The `NewMessageRequest` type defines the shape of a new message request object.
- */
-export type NewMessageRequest = {
- id?: string;
- conversationId?: string;
- user?: string;
- avatar?: string;
- message?: string;
- createdAt?: string;
- updatedAt?: string;
- history?: MessageHistory[];
-};
-
-/**
- * The `NewMessageRequest` type defines the shape of a new message request object.
- */
-export type NewMessageResponse = {
- id?: string;
- conversationId?: string;
- user?: string;
- avatar?: string;
- message?: string;
- createdAt?: string;
- updatedAt?: string;
-};
-
/**
* Adds an observer for an event.
*
* @param eventName The name of the event to observe.
* @param handler The handler function to call when the event is observed.
*/
-const on: (eventName: string, handler: Function) => void = (eventName, handler) => {
+const on: (eventName: string, handler: Function) => void = (
+ eventName,
+ handler
+) => {
window.corePlugin?.events?.on(eventName, handler);
};
@@ -59,7 +31,10 @@ const on: (eventName: string, handler: Function) => void = (eventName, handler)
* @param eventName The name of the event to stop observing.
* @param handler The handler function to call when the event is observed.
*/
-const off: (eventName: string, handler: Function) => void = (eventName, handler) => {
+const off: (eventName: string, handler: Function) => void = (
+ eventName,
+ handler
+) => {
window.corePlugin?.events?.off(eventName, handler);
};
diff --git a/core/src/index.ts b/core/src/index.ts
index 5c741c863..76827f6b3 100644
--- a/core/src/index.ts
+++ b/core/src/index.ts
@@ -34,4 +34,4 @@ export { fs } from "./fs";
* Plugin base module export.
* @module
*/
-export { JanPlugin, PluginType } from "./plugin";
+export * from "./plugin";
diff --git a/core/src/plugins/conversational.ts b/core/src/plugins/conversational.ts
index a76c41a51..ebeb77333 100644
--- a/core/src/plugins/conversational.ts
+++ b/core/src/plugins/conversational.ts
@@ -1,5 +1,5 @@
+import { Thread } from "../index";
import { JanPlugin } from "../plugin";
-import { Conversation } from "../types/index";
/**
* Abstract class for conversational plugins.
@@ -17,10 +17,10 @@ export abstract class ConversationalPlugin extends JanPlugin {
/**
* Saves a conversation.
* @abstract
- * @param {Conversation} conversation - The conversation to save.
+ * @param {Thread} conversation - The conversation to save.
* @returns {Promise} A promise that resolves when the conversation is saved.
*/
- abstract saveConversation(conversation: Conversation): Promise;
+ abstract saveConversation(conversation: Thread): Promise;
/**
* Deletes a conversation.
diff --git a/core/src/plugins/inference.ts b/core/src/plugins/inference.ts
index 8da7f5059..663d0b258 100644
--- a/core/src/plugins/inference.ts
+++ b/core/src/plugins/inference.ts
@@ -1,4 +1,4 @@
-import { NewMessageRequest } from "../events";
+import { MessageRequest } from "../index";
import { JanPlugin } from "../plugin";
/**
@@ -21,5 +21,5 @@ export abstract class InferencePlugin extends JanPlugin {
* @param data - The data for the inference request.
* @returns The result of the inference request.
*/
- abstract inferenceRequest(data: NewMessageRequest): Promise;
+ abstract inferenceRequest(data: MessageRequest): Promise;
}
diff --git a/core/src/types/index.ts b/core/src/types/index.ts
index 296bc1a7e..dd227081a 100644
--- a/core/src/types/index.ts
+++ b/core/src/types/index.ts
@@ -1,140 +1,183 @@
-export interface Conversation {
- id: string;
- modelId?: string;
- botId?: string;
- name: string;
- message?: string;
- summary?: string;
- createdAt?: string;
- updatedAt?: string;
- messages: Message[];
- lastMessage?: string;
-}
-
-export interface Message {
- id: string;
- message?: string;
- user?: string;
- createdAt?: string;
- updatedAt?: string;
-}
-
-export interface RawMessage {
- id?: string;
- conversationId?: string;
- user?: string;
- avatar?: string;
- message?: string;
- createdAt?: string;
- updatedAt?: string;
-}
-
-export interface Model {
- /**
- * Combination of owner and model name.
- * Being used as file name. MUST be unique.
- */
- id: string;
- name: string;
- quantMethod: string;
- bits: number;
- size: number;
- maxRamRequired: number;
- usecase: string;
- downloadLink: string;
- modelFile?: string;
- /**
- * For tracking download info
- */
- startDownloadAt?: number;
- finishDownloadAt?: number;
- productId: string;
- productName: string;
- shortDescription: string;
- longDescription: string;
- avatarUrl: string;
- author: string;
- version: string;
- modelUrl: string;
- createdAt: number;
- updatedAt?: number;
- status: string;
- releaseDate: number;
- tags: string[];
-}
-export interface ModelCatalog {
- id: string;
- name: string;
- shortDescription: string;
- avatarUrl: string;
- longDescription: string;
- author: string;
- version: string;
- modelUrl: string;
- createdAt: number;
- updatedAt?: number;
- status: string;
- releaseDate: number;
- tags: string[];
- availableVersions: ModelVersion[];
-}
/**
- * Model type which will be stored in the database
+ * Message Request and Response
+ * ============================
+ * */
+
+/**
+ * The role of the author of this message.
+ * @data_transfer_object
*/
-export type ModelVersion = {
- /**
- * Combination of owner and model name.
- * Being used as file name. Should be unique.
- */
- id: string;
- name: string;
- quantMethod: string;
- bits: number;
- size: number;
- maxRamRequired: number;
- usecase: string;
- downloadLink: string;
- productId: string;
- /**
- * For tracking download state
- */
- startDownloadAt?: number;
- finishDownloadAt?: number;
-};
-
-export interface ChatMessage {
- id: string;
- conversationId: string;
- messageType: MessageType;
- messageSenderType: MessageSenderType;
- senderUid: string;
- senderName: string;
- senderAvatarUrl: string;
- text: string | undefined;
- imageUrls?: string[] | undefined;
- createdAt: number;
- status: MessageStatus;
-}
-
-export enum MessageType {
- Text = "Text",
- Image = "Image",
- ImageWithText = "ImageWithText",
- Error = "Error",
-}
-
-export enum MessageSenderType {
- Ai = "assistant",
+export enum ChatCompletionRole {
+ System = "system",
+ Assistant = "assistant",
User = "user",
}
+/**
+ * The `MessageRequest` type defines the shape of a new message request object.
+ * @data_transfer_object
+ */
+export type ChatCompletionMessage = {
+ /** The contents of the message. **/
+ content?: string;
+ /** The role of the author of this message. **/
+ role: ChatCompletionRole;
+};
+
+/**
+ * The `MessageRequest` type defines the shape of a new message request object.
+ * @data_transfer_object
+ */
+export type MessageRequest = {
+ id?: string;
+ /** The thread id of the message request. **/
+ threadId?: string;
+ /** Messages for constructing a chat completion request **/
+ messages?: ChatCompletionMessage[];
+};
+
+/**
+ * Thread and Message
+ * ========================
+ * */
+
+/**
+ * The status of the message.
+ * @data_transfer_object
+ */
export enum MessageStatus {
+ /** Message is fully loaded. **/
Ready = "ready",
+ /** Message is not fully loaded. **/
Pending = "pending",
}
-
-export type ConversationState = {
- hasMore: boolean;
- waitingForResponse: boolean;
- error?: Error;
+/**
+ * The `ThreadMessage` type defines the shape of a thread's message object.
+ * @stored
+ */
+export type ThreadMessage = {
+ /** Unique identifier for the message, generated by default using the ULID method. **/
+ id?: string;
+ /** Thread id, default is a ulid. **/
+ threadId?: string;
+ /** The role of the author of this message. **/
+ role?: ChatCompletionRole;
+ /** The content of this message. **/
+ content?: string;
+ /** The status of this message. **/
+ status: MessageStatus;
+ /** The timestamp indicating when this message was created, represented in ISO 8601 format. **/
+ createdAt?: string;
+};
+
+/**
+ * The `Thread` type defines the shape of a thread object.
+ * @stored
+ */
+export interface Thread {
+ /** Unique identifier for the thread, generated by default using the ULID method. **/
+ id: string;
+ /** The summary of this thread. **/
+ summary?: string;
+ /** The messages of this thread. **/
+ messages: ThreadMessage[];
+ /** The timestamp indicating when this thread was created, represented in ISO 8601 format. **/
+ createdAt?: string;
+ /** The timestamp indicating when this thread was updated, represented in ISO 8601 format. **/
+ updatedAt?: string;
+
+ /**
+ * @deprecated This field is deprecated and should not be used.
+ * Read from model file instead.
+ */
+ modelId?: string;
+}
+
+/**
+ * Model type defines the shape of a model object.
+ * @stored
+ */
+export interface Model {
+ /** Combination of owner and model name.*/
+ id: string;
+ /** The name of the model.*/
+ name: string;
+ /** Quantization method name.*/
+ quantizationName: string;
+ /** The the number of bits represents a number.*/
+ bits: number;
+ /** The size of the model file in bytes.*/
+ size: number;
+ /** The maximum RAM required to run the model in bytes.*/
+ maxRamRequired: number;
+ /** The use case of the model.*/
+ usecase: string;
+ /** The download link of the model.*/
+ downloadLink: string;
+ /** The short description of the model.*/
+ shortDescription: string;
+ /** The long description of the model.*/
+ longDescription: string;
+ /** The avatar url of the model.*/
+ avatarUrl: string;
+ /** The author name of the model.*/
+ author: string;
+ /** The version of the model.*/
+ version: string;
+ /** The origin url of the model repo.*/
+ modelUrl: string;
+ /** The timestamp indicating when this model was released.*/
+ releaseDate: number;
+ /** The tags attached to the model description */
+ tags: string[];
+}
+
+/**
+ * Model type of the presentation object which will be presented to the user
+ * @data_transfer_object
+ */
+export interface ModelCatalog {
+ /** The unique id of the model.*/
+ id: string;
+ /** The name of the model.*/
+ name: string;
+ /** The avatar url of the model.*/
+ avatarUrl: string;
+ /** The short description of the model.*/
+ shortDescription: string;
+ /** The long description of the model.*/
+ longDescription: string;
+ /** The author name of the model.*/
+ author: string;
+ /** The version of the model.*/
+ version: string;
+ /** The origin url of the model repo.*/
+ modelUrl: string;
+ /** The timestamp indicating when this model was released.*/
+ releaseDate: number;
+ /** The tags attached to the model description **/
+ tags: string[];
+
+ /** The available versions of this model to download. */
+ availableVersions: ModelVersion[];
+}
+/**
+ * Model type which will be present a version of ModelCatalog
+ * @data_transfer_object
+ */
+export type ModelVersion = {
+ /** The name of this model version.*/
+ name: string;
+ /** The quantization method name.*/
+ quantizationName: string;
+ /** The the number of bits represents a number.*/
+ bits: number;
+ /** The size of the model file in bytes.*/
+ size: number;
+ /** The maximum RAM required to run the model in bytes.*/
+ maxRamRequired: number;
+ /** The use case of the model.*/
+ usecase: string;
+ /** The download link of the model.*/
+ downloadLink: string;
};
diff --git a/docs/docs/docs/specs/assistants.md b/docs/docs/docs/specs/assistants.md
index 1d326fd2e..eb93265fe 100644
--- a/docs/docs/docs/specs/assistants.md
+++ b/docs/docs/docs/specs/assistants.md
@@ -2,79 +2,188 @@
title: "Assistants"
---
-Assistants can use models and tools.
+:::warning
-- Jan's `Assistants` are even more powerful than OpenAI due to customizable code in `index.js`
+Draft Specification: functionality has not been implemented yet.
-> OpenAI Equivalent: https://platform.openai.com/docs/api-reference/assistants
+Feedback: [HackMD: Assistants Spec](https://hackmd.io/KKAznzZvS668R6Vmyf8fCg)
-## Assistant Object
+:::
-- `assistant.json`
-- Equivalent to: https://platform.openai.com/docs/api-reference/assistants/object
+
+## User Stories
+
+_Users can chat with an assistant_
+
+- [Wireframes - show asst object properties]
+- See [Threads Spec](https://hackmd.io/BM_8o_OCQ-iLCYhunn2Aug)
+
+_Users can use Jan - the default assistant_
+
+- [Wireframes here - show model picker]
+- See [Default Jan Object](#Default-Jan-Example)
+
+_Users can create an assistant from scratch_
+
+- [Wireframes here - show create asst flow]
+- Users can select any model for an assistant. See Model Spec
+
+_Users can create an assistant from an existing assistant_
+
+- [Wireframes showing asst edit mode]
+
+## Jan Assistant Object
+
+- A `Jan Assistant Object` is a "representation of an assistant"
+- Objects are defined by `assistant-uuid.json` files in `json` format
+- Objects are designed to be compatible with `OpenAI Assistant Objects` with additional properties needed to run on our infrastructure.
+- ALL object properties are optional, i.e. users should be able to use an assistant declared by an empty `json` file.
+
+| Property | Type | Description | Validation |
+| ------------- | ----------------------------------------------- | ---------------------------------------------------------------------------------------------- | ------------------------------- |
+| `object` | enum: `model`, `assistant`, `thread`, `message` | The Jan Object type | Defaults to `assistant` |
+| `name` | string | A vanity name. | Defaults to filename |
+| `description` | string | A vanity description. | Max `n` chars. Defaults to `""` |
+| `models` | array | A list of Model Objects that the assistant can use. | Defaults to ALL models |
+| `metadata` | map | This can be useful for storing additional information about the object in a structured format. | Defaults to `{}` |
+| `tools` | array | TBA. | TBA |
+| `files` | array | TBA. | TBA |
+
+### Generic Example
```json
-{
- // Jan specific properties
- "avatar": "https://lala.png"
- "thread_location": "ROOT/threads" // Default to root (optional field)
- // TODO: add moar
+// janroot/assistants/example/example.json
+"name": "Homework Helper",
- // OpenAI compatible properties: https://platform.openai.com/docs/api-reference/assistants
- "id": "asst_abc123",
- "object": "assistant",
- "created_at": 1698984975,
- "name": "Math Tutor",
- "description": null,
- "model": reference model.json,
- "instructions": reference model.json,
- "tools": [
- {
- "type": "rag"
- }
- ],
- "file_ids": [],
- "metadata": {}
-}
+// Option 1 (default): all models in janroot/models are available via Model Picker
+"models": [],
+
+// Option 2: creator can configure custom parameters on existing models in `janroot/models` &&
+// Option 3: creator can package a custom model with the assistant
+"models": [{ ...modelObject1 }, { ...modelObject2 }],
```
-## Assistants API
+### Default Jan Example
-- _TODO_: What would modifying Assistant do? (doesn't mutate `index.js`?)
+- Every user install has a default "Jan Assistant" declared below.
+ > Q: can we omit most properties in `jan.json`? It's all defaults anyway.
+
+```json
+// janroot/assistants/jan/jan.json
+"description": "Use Jan to chat with all models",
+```
+
+## Filesystem
+
+- Everything needed to represent & run an assistant is packaged into an `Assistant folder`.
+- The folder is standalone and can be easily zipped, imported, and exported, e.g. to Github.
+- The folder always contains an `Assistant Object`, declared in an `assistant-uuid.json`.
+ - The folder and file must share the same name: `assistant-uuid`
+- In the future, the folder will contain all of the resources an assistant needs to run, e.g. custom model binaries, pdf files, custom code, etc.
```sh
-GET https://api.openai.com/v1/assistants # List
-POST https://api.openai.com/v1/assistants # C
-GET https://api.openai.com/v1/assistants/{assistant_id} # R
-POST https://api.openai.com/v1/assistants/{assistant_id} # U
-DELETE https://api.openai.com/v1/assistants/{assistant_id} # D
+janroot/
+ assistants/
+ jan/ # Assistant Folder
+ jan.json # Assistant Object
+ homework-helper/ # Assistant Folder
+ homework-helper.json # Assistant Object
```
-## Assistants Filesystem
+### Custom Code
+
+> Not in scope yet. Sharing as a preview only.
+
+- Assistants can call custom code in the future
+- Custom code extends beyond `function calling` to any features that can be implemented in `/src`
```sh
-/assistants
- /jan
- assistant.json # Assistant configs (see below)
-
- # For any custom code
- package.json # Import npm modules
- # e.g. Langchain, Llamaindex
- /src # Supporting files (needs better name)
- index.js # Entrypoint
- process.js # For electron IPC processes (needs better name)
-
- # `/threads` at root level
- # `/models` at root level
- /shakespeare
- assistant.json
- model.json # Creator chooses model and settings
- package.json
- /src
- index.js
- process.js
-
- /threads # Assistants remember conversations in the future
- /models # Users can upload custom models
- /finetuned-model
+example/ # Assistant Folder
+ example.json # Assistant Object
+ package.json
+ src/
+ index.ts
+ helpers.ts
```
+
+### Knowledge Files
+
+> Not in scope yet. Sharing as a preview only
+
+- Assistants can do `retrieval` in future
+
+```sh
+
+example/ # Assistant Folder
+ example.json # Assistant Object
+ files/
+```
+
+## Jan API
+
+### Assistant API Object
+
+#### `GET /v1/assistants/{assistant_id}`
+
+- The `Jan Assistant Object` maps into the `OpenAI Assistant Object`.
+- Properties marked with `*` are compatible with the [OpenAI `assistant` object](https://platform.openai.com/docs/api-reference/assistants)
+- Note: The `Jan Assistant Object` has additional properties when retrieved via its API endpoint.
+- https://platform.openai.com/docs/api-reference/assistants/getAssistant
+
+| Property | Type | Public Description | Jan Assistant Object (`a`) Property |
+| ---------------- | -------------- | ------------------------------------------------------------------------- | ----------------------------------- |
+| `id`\* | string | Assistant uuid, also the name of the Jan Assistant Object file: `id.json` | `json` filename |
+| `object`\* | string | Always "assistant" | `a.object` |
+| `created_at`\* | integer | Timestamp when assistant was created. | `a.json` creation time |
+| `name`\* | string or null | A display name | `a.name` or `id` |
+| `description`\* | string or null | A description | `a.description` |
+| `model`\* | string | Text | `a.models[0].name` |
+| `instructions`\* | string or null | Text | `a.models[0].parameters.prompt` |
+| `tools`\* | array | TBA | `a.tools` |
+| `file_ids`\* | array | TBA | `a.files` |
+| `metadata`\* | map | TBA | `a.metadata` |
+| `models` | array | TBA | `a.models` |
+
+### Create Assistant
+
+#### `POST /v1/assistants`
+
+- https://platform.openai.com/docs/api-reference/assistants/createAssistant
+
+### Retrieve Assistant
+
+#### `GET v1/assistants/{assistant_id}`
+
+- https://platform.openai.com/docs/api-reference/assistants/getAssistant
+
+### Modify Assistant
+
+#### `POST v1/assistants/{assistant_id}`
+
+- https://platform.openai.com/docs/api-reference/assistants/modifyAssistant
+
+### Delete Assistant
+
+#### `DELETE v1/assistants/{assistant_id}`
+
+- https://platform.openai.com/docs/api-reference/assistants/deleteAssistant
+
+### List Assistants
+
+#### `GET v1/assistants`
+
+- https://platform.openai.com/docs/api-reference/assistants/listAssistants
+
+### CRUD Assistant.Models
+
+- This is a Jan-only endpoint, since Jan supports the ModelPicker, i.e. an `assistant` can be created to run with many `models`.
+
+#### `POST /v1/assistants/{assistant_id}/models`
+
+#### `GET /v1/assistants/{assistant_id}/models`
+
+#### `GET /v1/assistants/{assistant_id}/models/{model_id}`
+
+#### `DELETE /v1/assistants/{assistant_id}/models`
+
+Note: There's no need to implement `Modify Assistant.Models`
diff --git a/docs/docs/docs/specs/chats.md b/docs/docs/docs/specs/chats.md
index 58047c4c8..fedd6a9c8 100644
--- a/docs/docs/docs/specs/chats.md
+++ b/docs/docs/docs/specs/chats.md
@@ -2,6 +2,12 @@
title: "Chats"
---
+:::warning
+
+Draft Specification: functionality has not been implemented yet.
+
+:::
+
Chats are essentially inference requests to a model
> OpenAI Equivalent: https://platform.openai.com/docs/api-reference/chat
diff --git a/docs/docs/docs/specs/files.md b/docs/docs/docs/specs/files.md
index 70c3e345f..4d62e33d5 100644
--- a/docs/docs/docs/specs/files.md
+++ b/docs/docs/docs/specs/files.md
@@ -2,6 +2,12 @@
title: "Files"
---
+:::warning
+
+Draft Specification: functionality has not been implemented yet.
+
+:::
+
Files can be used by `threads`, `assistants` and `fine-tuning`
> Equivalent to: https://platform.openai.com/docs/api-reference/files
diff --git a/docs/docs/docs/specs/messages.md b/docs/docs/docs/specs/messages.md
index 8bc79d1ae..ed5c5d95b 100644
--- a/docs/docs/docs/specs/messages.md
+++ b/docs/docs/docs/specs/messages.md
@@ -2,6 +2,14 @@
title: "Messages"
---
+:::warning
+
+Draft Specification: functionality has not been implemented yet.
+
+Feedback: [HackMD: Threads Spec](https://hackmd.io/BM_8o_OCQ-iLCYhunn2Aug)
+
+:::
+
Messages are within `threads` and capture additional metadata.
- Equivalent to: https://platform.openai.com/docs/api-reference/messages
diff --git a/docs/docs/docs/specs/models.md b/docs/docs/docs/specs/models.md
index 482a7a037..03b0de7c8 100644
--- a/docs/docs/docs/specs/models.md
+++ b/docs/docs/docs/specs/models.md
@@ -1,4 +1,16 @@
-# Model Specs
+---
+title: "Models"
+---
+
+:::warning
+
+Draft Specification: functionality has not been implemented yet.
+
+Feedback: [HackMD: Models Spec](https://hackmd.io/ulO3uB1AQCqLa5SAAMFOQw)
+
+:::
+
+Models are AI models like Llama and Mistral
> OpenAI Equivalent: https://platform.openai.com/docs/api-reference/models
diff --git a/docs/docs/docs/specs/threads.md b/docs/docs/docs/specs/threads.md
index f36f923ed..e7d0fe978 100644
--- a/docs/docs/docs/specs/threads.md
+++ b/docs/docs/docs/specs/threads.md
@@ -2,52 +2,133 @@
title: "Threads"
---
-Threads contain `messages` history with assistants. Messages in a thread share context.
+:::warning
-- Note: For now, threads "lock the model" after a `message` is sent
- - When a new `thread` is created with Jan, users can choose the models
- - Users can still edit model parameters/system prompts
- - Note: future Assistants may customize this behavior
-- Note: Assistants will be able to specify default thread location in the future
- - Jan uses root-level threads, to allow for future multi-assistant threads
- - Assistant Y may store threads in its own folder, to allow for [long-term assistant memory](https://github.com/janhq/jan/issues/344)
+Draft Specification: functionality has not been implemented yet.
-> OpenAI Equivalent: https://platform.openai.com/docs/api-reference/threads
+Feedback: [HackMD: Threads Spec](https://hackmd.io/BM_8o_OCQ-iLCYhunn2Aug)
-## Thread Object
+:::
-- `thread.json`
-- Equivalent to: https://platform.openai.com/docs/api-reference/threads/object
+## User Stories
+
+_Users can chat with an assistant in a thread_
+
+- See [Messages Spec]
+
+_Users can change model in a new thread_
+
+- Wireframes here
+
+_Users can change model parameters in a thread_
+
+- Wireframes of
+
+_Users can delete all thread history_
+
+- Wireframes of settings page.
+
+## Jan Thread Object
+
+- A `Jan Thread Object` is a "representation of a conversation thread" between an `assistant` and the user
+- Objects are defined by `thread-uuid.json` files in `json` format
+- Objects are designed to be compatible with `OpenAI Thread Objects` with additional properties needed to run on our infrastructure.
+- Objects contain a `models` field, to track when the user overrides the assistant's default model parameters.
+
+| Property | Type | Description | Validation |
+| ---------- | ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ |
+| `object` | enum: `model`, `assistant`, `thread`, `message` | The Jan Object type | Defaults to `thread` |
+| `models` | array | An array of Jan Model Objects. Threads can "override" an assistant's model run parameters. Thread-level model parameters are directly saved in the `thread.models` property! (see Models spec) | Defaults to `assistant.models` |
+| `messages` | array | An array of Jan Message Objects. (see Messages spec) | Defaults to `[]` |
+| `metadata` | map | Useful for storing additional information about the object in a structured format. | Defaults to `{}` |
+
+### Generic Example
```json
-{
- // Jan specific properties:
- "summary": "HCMC restaurant recommendations",
- "messages": {see below}
-
- // OpenAI compatible properties: https://platform.openai.com/docs/api-reference/threads)
- "id": "thread_abc123",
- "object": "thread",
- "created_at": 1698107661,
- "metadata": {}
-}
+// janroot/threads/jan_1700123404.json
+"messages": [
+ {...message0}, {...message1}
+],
+"metadata": {
+ "summary": "funny physics joke",
+},
```
-## Threads API
+## Filesystem
-- Equivalent to: https://platform.openai.com/docs/api-reference/threads
-
-```sh=
-POST https://localhost:1337/v1/threads/{thread_id} # Create thread
-GET https://localhost:1337/v1/threads/{thread_id} # Get thread
-DELETE https://localhost:1337/v1/models/{thread_id} # Delete thread
-```
-
-## Threads Filesystem
+- `Jan Thread Objects`' `json` files always has the naming schema: `assistant_uuid` + `unix_time_thread_created_at. See below.
+- Threads are all saved in the `janroot/threads` folder in a flat folder structure.
+- The folder is standalone and can be easily zipped, exported, and cleared.
```sh
-/assistants
- /homework-helper
- /threads # context is "permanently remembered" by assistant in future conversations
-/threads # context is only retained within a single thread
+janroot/
+ threads/
+ jan_1700123404.json
+ homework_helper_700120003.json
```
+
+## Jan API
+
+### Thread API Object
+
+#### `GET /v1/threads/{thread_id}`
+
+- The `Jan Thread Object` maps into the `OpenAI Thread Object`.
+- Properties marked with `*` are compatible with the [OpenAI `thread` object](https://platform.openai.com/docs/api-reference/threads)
+- Note: The `Jan Thread Object` has additional properties when retrieved via its API endpoint.
+- https://platform.openai.com/docs/api-reference/threads/getThread
+
+| Property | Type | Public Description | Jan Thread Object (`t`) Property |
+| -------------- | ------- | ------------------------------------------------------------------- | -------------------------------- |
+| `id`\* | string | Thread uuid, also the name of the Jan Thread Object file: `id.json` | `json` filename |
+| `object`\* | string | Always "thread" | `t.object` |
+| `created_at`\* | integer | | `json` file creation time |
+| `metadata`\* | map | | `t.metadata` |
+| `models` | array | | `t.models` |
+| `messages` | array | | `t.messages` |
+
+### Create Thread
+
+#### `POST /v1/threads`
+
+- https://platform.openai.com/docs/api-reference/threads/createThread
+
+### Retrieve Thread
+
+#### `GET v1/threads/{thread_id}`
+
+- https://platform.openai.com/docs/api-reference/threads/getThread
+
+### Modify Thread
+
+#### `POST v1/threads/{thread_id}`
+
+- https://platform.openai.com/docs/api-reference/threads/modifyThread
+
+### Delete Thread
+
+#### `DELETE v1/threads/{thread_id}`
+
+- https://platform.openai.com/docs/api-reference/threads/deleteThread
+
+### List Threads
+
+> This is a Jan-only endpoint, not supported by OAI yet.
+
+#### `GET v1/threads`
+
+### Get & Modify `Thread.Models`
+
+> This is a Jan-only endpoint, not supported by OAI yet.
+
+#### `GET v1/threads/{thread_id}/models`
+
+#### `POST v1/threads/{thread_id}/models/{model_id}`
+
+- Since users can change model parameters in an existing thread
+
+### List `Thread.Messages`
+
+> This is a Jan-only endpoint, not supported by OAI yet.
+
+#### `GET v1/threads/{thread_id}/messages`
diff --git a/plugins/conversational-json/src/index.ts b/plugins/conversational-json/src/index.ts
index 0e8465fd5..94082bb45 100644
--- a/plugins/conversational-json/src/index.ts
+++ b/plugins/conversational-json/src/index.ts
@@ -1,6 +1,6 @@
import { PluginType, fs } from '@janhq/core'
import { ConversationalPlugin } from '@janhq/core/lib/plugins'
-import { Conversation } from '@janhq/core/lib/types'
+import { Thread } from '@janhq/core/lib/types'
import { join } from 'path'
/**
@@ -35,7 +35,7 @@ export default class JSONConversationalPlugin implements ConversationalPlugin {
/**
* Returns a Promise that resolves to an array of Conversation objects.
*/
- async getConversations(): Promise {
+ async getConversations(): Promise {
try {
const convoIds = await this.getConversationDocs()
@@ -46,7 +46,7 @@ export default class JSONConversationalPlugin implements ConversationalPlugin {
const convos = promiseResults
.map((result) => {
if (result.status === 'fulfilled') {
- return JSON.parse(result.value) as Conversation
+ return JSON.parse(result.value) as Thread
}
})
.filter((convo) => convo != null)
@@ -66,7 +66,7 @@ export default class JSONConversationalPlugin implements ConversationalPlugin {
* Saves a Conversation object to a Markdown file.
* @param conversation The Conversation object to save.
*/
- saveConversation(conversation: Conversation): Promise {
+ saveConversation(conversation: Thread): Promise {
return fs
.mkdir(`${JSONConversationalPlugin._homeDir}/${conversation.id}`)
.then(() =>
diff --git a/plugins/conversational-plugin/package.json b/plugins/conversational-plugin/package.json
deleted file mode 100644
index e7a29f9e7..000000000
--- a/plugins/conversational-plugin/package.json
+++ /dev/null
@@ -1,39 +0,0 @@
-{
- "name": "@janhq/conversational-plugin",
- "version": "1.0.7",
- "description": "Conversational Plugin - Stores jan app conversations",
- "main": "dist/index.js",
- "author": "Jan ",
- "requiredVersion": "^0.3.1",
- "license": "MIT",
- "activationPoints": [
- "init"
- ],
- "scripts": {
- "build": "tsc -b . && webpack --config webpack.config.js",
- "build:publish": "rimraf *.tgz --glob npm run build && && npm pack && cpx *.tgz ../../electron/core/pre-install"
- },
- "exports": {
- ".": "./dist/index.js",
- "./main": "./dist/module.js"
- },
- "devDependencies": {
- "cpx": "^1.5.0",
- "rimraf": "^3.0.2",
- "webpack": "^5.88.2",
- "webpack-cli": "^5.1.4"
- },
- "dependencies": {
- "@janhq/core": "file:../../core",
- "ts-loader": "^9.5.0"
- },
- "engines": {
- "node": ">=18.0.0"
- },
- "files": [
- "dist/*",
- "package.json",
- "README.md"
- ],
- "bundleDependencies": []
-}
diff --git a/plugins/conversational-plugin/src/index.ts b/plugins/conversational-plugin/src/index.ts
deleted file mode 100644
index 01045b6c8..000000000
--- a/plugins/conversational-plugin/src/index.ts
+++ /dev/null
@@ -1,214 +0,0 @@
-import { PluginType, fs } from "@janhq/core";
-import { ConversationalPlugin } from "@janhq/core/lib/plugins";
-import { Message, Conversation } from "@janhq/core/lib/types";
-
-/**
- * JanConversationalPlugin is a ConversationalPlugin implementation that provides
- * functionality for managing conversations.
- */
-export default class JanConversationalPlugin implements ConversationalPlugin {
- /**
- * Returns the type of the plugin.
- */
- type(): PluginType {
- return PluginType.Conversational;
- }
-
- /**
- * Called when the plugin is loaded.
- */
- onLoad() {
- console.debug("JanConversationalPlugin loaded");
- fs.mkdir("conversations");
- }
-
- /**
- * Called when the plugin is unloaded.
- */
- onUnload() {
- console.debug("JanConversationalPlugin unloaded");
- }
-
- /**
- * Returns a Promise that resolves to an array of Conversation objects.
- */
- getConversations(): Promise {
- return this.getConversationDocs().then((conversationIds) =>
- Promise.all(
- conversationIds.map((conversationId) =>
- this.loadConversationFromMarkdownFile(
- `conversations/${conversationId}/${conversationId}.md`
- )
- )
- ).then((conversations) =>
- conversations.sort(
- (a, b) =>
- new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
- )
- )
- );
- }
-
- /**
- * Saves a Conversation object to a Markdown file.
- * @param conversation The Conversation object to save.
- */
- saveConversation(conversation: Conversation): Promise {
- return this.writeMarkdownToFile(conversation);
- }
-
- /**
- * Deletes a conversation with the specified ID.
- * @param conversationId The ID of the conversation to delete.
- */
- deleteConversation(conversationId: string): Promise {
- return fs.rmdir(`conversations/${conversationId}`);
- }
-
- /**
- * Returns a Promise that resolves to an array of conversation IDs.
- * The conversation IDs are the names of the Markdown files in the "conversations" directory.
- * @private
- */
- private async getConversationDocs(): Promise {
- return fs.listFiles("conversations").then((files: string[]) => {
- return Promise.all(files.filter((file) => file.startsWith("jan-")));
- });
- }
-
- /**
- * Parses a Markdown string and returns a Conversation object.
- * @param markdown The Markdown string to parse.
- * @private
- */
- private parseConversationMarkdown(markdown: string): Conversation {
- const conversation: Conversation = {
- id: "",
- name: "",
- messages: [],
- };
- var currentMessage: Message | undefined = undefined;
- for (const line of markdown.split("\n")) {
- const trimmedLine = line.trim();
- if (trimmedLine.startsWith("- id:")) {
- conversation.id = trimmedLine.replace("- id:", "").trim();
- } else if (trimmedLine.startsWith("- modelId:")) {
- conversation.modelId = trimmedLine.replace("- modelId:", "").trim();
- } else if (trimmedLine.startsWith("- name:")) {
- conversation.name = trimmedLine.replace("- name:", "").trim();
- } else if (trimmedLine.startsWith("- lastMessage:")) {
- conversation.message = trimmedLine.replace("- lastMessage:", "").trim();
- } else if (trimmedLine.startsWith("- summary:")) {
- conversation.summary = trimmedLine.replace("- summary:", "").trim();
- } else if (
- trimmedLine.startsWith("- createdAt:") &&
- currentMessage === undefined
- ) {
- conversation.createdAt = trimmedLine.replace("- createdAt:", "").trim();
- } else if (trimmedLine.startsWith("- updatedAt:")) {
- conversation.updatedAt = trimmedLine.replace("- updatedAt:", "").trim();
- } else if (trimmedLine.startsWith("- botId:")) {
- conversation.botId = trimmedLine.replace("- botId:", "").trim();
- } else if (trimmedLine.startsWith("- user:")) {
- if (currentMessage)
- currentMessage.user = trimmedLine.replace("- user:", "").trim();
- } else if (trimmedLine.startsWith("- createdAt:")) {
- if (currentMessage)
- currentMessage.createdAt = trimmedLine
- .replace("- createdAt:", "")
- .trim();
-
- currentMessage.updatedAt = currentMessage.createdAt;
- } else if (trimmedLine.startsWith("- message:")) {
- if (currentMessage)
- currentMessage.message = trimmedLine.replace("- message:", "").trim();
- } else if (trimmedLine.startsWith("- Message ")) {
- const messageMatch = trimmedLine.match(/- Message (m-\d+):/);
- if (messageMatch) {
- if (currentMessage) {
- conversation.messages.push(currentMessage);
- }
- currentMessage = { id: messageMatch[1] };
- }
- } else if (
- currentMessage?.message &&
- !trimmedLine.startsWith("## Messages")
- ) {
- currentMessage.message = currentMessage.message + "\n" + line.trim();
- } else if (trimmedLine.startsWith("## Messages")) {
- currentMessage = undefined;
- }
- }
-
- if (currentMessage) {
- conversation.messages.push(currentMessage);
- }
- return conversation;
- }
-
- /**
- * Loads a Conversation object from a Markdown file.
- * @param filePath The path to the Markdown file.
- * @private
- */
- private async loadConversationFromMarkdownFile(
- filePath: string
- ): Promise {
- try {
- const markdown: string = await fs.readFile(filePath);
- return this.parseConversationMarkdown(markdown);
- } catch (err) {
- return undefined;
- }
- }
-
- /**
- * Generates a Markdown string from a Conversation object.
- * @param conversation The Conversation object to generate Markdown from.
- * @private
- */
- private generateMarkdown(conversation: Conversation): string {
- // Generate the Markdown content based on the Conversation object
- const conversationMetadata = `
- - id: ${conversation.id}
- - modelId: ${conversation.modelId}
- - name: ${conversation.name}
- - lastMessage: ${conversation.message}
- - summary: ${conversation.summary}
- - createdAt: ${conversation.createdAt}
- - updatedAt: ${conversation.updatedAt}
- - botId: ${conversation.botId}
- `;
-
- const messages = conversation.messages.map(
- (message) => `
- - Message ${message.id}:
- - createdAt: ${message.createdAt}
- - user: ${message.user}
- - message: ${message.message?.trim()}
- `
- );
-
- return `## Conversation Metadata
- ${conversationMetadata}
-## Messages
- ${messages.map((msg) => msg.trim()).join("\n")}
- `;
- }
-
- /**
- * Writes a Conversation object to a Markdown file.
- * @param conversation The Conversation object to write to a Markdown file.
- * @private
- */
- private async writeMarkdownToFile(conversation: Conversation) {
- // Generate the Markdown content
- const markdownContent = this.generateMarkdown(conversation);
- await fs.mkdir(`conversations/${conversation.id}`);
- // Write the content to a Markdown file
- await fs.writeFile(
- `conversations/${conversation.id}/${conversation.id}.md`,
- markdownContent
- );
- }
-}
diff --git a/plugins/conversational-plugin/tsconfig.json b/plugins/conversational-plugin/tsconfig.json
deleted file mode 100644
index 2477d58ce..000000000
--- a/plugins/conversational-plugin/tsconfig.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "compilerOptions": {
- "target": "es2016",
- "module": "ES6",
- "moduleResolution": "node",
- "outDir": "./dist",
- "esModuleInterop": true,
- "forceConsistentCasingInFileNames": true,
- "strict": false,
- "skipLibCheck": true,
- "rootDir": "./src"
- },
- "include": ["./src"]
-}
diff --git a/plugins/conversational-plugin/webpack.config.js b/plugins/conversational-plugin/webpack.config.js
deleted file mode 100644
index d4b0db2bd..000000000
--- a/plugins/conversational-plugin/webpack.config.js
+++ /dev/null
@@ -1,31 +0,0 @@
-const path = require("path");
-const webpack = require("webpack");
-
-module.exports = {
- experiments: { outputModule: true },
- entry: "./src/index.ts", // Adjust the entry point to match your project's main file
- mode: "production",
- module: {
- rules: [
- {
- test: /\.tsx?$/,
- use: "ts-loader",
- exclude: /node_modules/,
- },
- ],
- },
- output: {
- filename: "index.js", // Adjust the output file name as needed
- path: path.resolve(__dirname, "dist"),
- library: { type: "module" }, // Specify ESM output format
- },
- plugins: [new webpack.DefinePlugin({})],
- resolve: {
- extensions: [".ts", ".js"],
- },
- // Do not minify the output, otherwise it breaks the class registration
- optimization: {
- minimize: false,
- },
- // Add loaders and other configuration as needed for your project
-};
diff --git a/plugins/inference-plugin/src/index.ts b/plugins/inference-plugin/src/index.ts
index b02c0f628..5841b7abc 100644
--- a/plugins/inference-plugin/src/index.ts
+++ b/plugins/inference-plugin/src/index.ts
@@ -7,10 +7,13 @@
*/
import {
+ ChatCompletionMessage,
+ ChatCompletionRole,
EventName,
- MessageHistory,
- NewMessageRequest,
+ MessageRequest,
+ MessageStatus,
PluginType,
+ ThreadMessage,
events,
executeOnMain,
} from "@janhq/core";
@@ -70,29 +73,19 @@ export default class JanInferencePlugin implements InferencePlugin {
/**
* Makes a single response inference request.
- * @param {NewMessageRequest} data - The data for the inference request.
+ * @param {MessageRequest} data - The data for the inference request.
* @returns {Promise} A promise that resolves with the inference response.
*/
- async inferenceRequest(data: NewMessageRequest): Promise {
+ async inferenceRequest(data: MessageRequest): Promise {
const message = {
...data,
message: "",
user: "assistant",
createdAt: new Date().toISOString(),
};
- const prompts: [MessageHistory] = [
- {
- role: "user",
- content: data.message,
- },
- ];
- const recentMessages = data.history ?? prompts;
return new Promise(async (resolve, reject) => {
- requestInference([
- ...recentMessages,
- { role: "user", content: data.message },
- ]).subscribe({
+ requestInference(data.messages ?? []).subscribe({
next: (content) => {
message.message = content;
},
@@ -108,37 +101,33 @@ export default class JanInferencePlugin implements InferencePlugin {
/**
* Handles a new message request by making an inference request and emitting events.
- * @param {NewMessageRequest} data - The data for the new message request.
+ * @param {MessageRequest} data - The data for the new message request.
*/
- private async handleMessageRequest(data: NewMessageRequest) {
- const prompts: [MessageHistory] = [
- {
- role: "user",
- content: data.message,
- },
- ];
- const recentMessages = data.history ?? prompts;
- const message = {
- ...data,
- message: "",
- user: "assistant",
+ private async handleMessageRequest(data: MessageRequest) {
+ const message: ThreadMessage = {
+ threadId: data.threadId,
+ content: "",
+ role: ChatCompletionRole.Assistant,
createdAt: new Date().toISOString(),
id: ulid(),
+ status: MessageStatus.Pending,
};
events.emit(EventName.OnNewMessageResponse, message);
- requestInference(recentMessages).subscribe({
+ requestInference(data.messages).subscribe({
next: (content) => {
- message.message = content;
+ message.content = content;
events.emit(EventName.OnMessageResponseUpdate, message);
},
complete: async () => {
- message.message = message.message.trim();
+ message.content = message.content.trim();
+ message.status = MessageStatus.Ready;
events.emit(EventName.OnMessageResponseFinished, message);
},
error: async (err) => {
- message.message =
- message.message.trim() + "\n" + "Error occurred: " + err.message;
+ message.content =
+ message.content.trim() + "\n" + "Error occurred: " + err.message;
+ message.status = MessageStatus.Ready;
events.emit(EventName.OnMessageResponseUpdate, message);
},
});
diff --git a/plugins/model-plugin/src/index.ts b/plugins/model-plugin/src/index.ts
index 2e599c2d4..5fb487017 100644
--- a/plugins/model-plugin/src/index.ts
+++ b/plugins/model-plugin/src/index.ts
@@ -42,7 +42,7 @@ export default class JanModelPlugin implements ModelPlugin {
*/
async downloadModel(model: Model): Promise {
// create corresponding directory
- const directoryPath = join(JanModelPlugin._homeDir, model.productName)
+ const directoryPath = join(JanModelPlugin._homeDir, model.name)
await fs.mkdir(directoryPath)
// path to model binary
@@ -72,7 +72,7 @@ export default class JanModelPlugin implements ModelPlugin {
* @returns A Promise that resolves when the model is saved.
*/
async saveModel(model: Model): Promise {
- const directoryPath = join(JanModelPlugin._homeDir, model.productName)
+ const directoryPath = join(JanModelPlugin._homeDir, model.name)
const jsonFilePath = join(directoryPath, `${model.id}.json`)
try {
diff --git a/web/containers/ModalCancelDownload/index.tsx b/web/containers/ModalCancelDownload/index.tsx
index e62dda0ca..3413d02c4 100644
--- a/web/containers/ModalCancelDownload/index.tsx
+++ b/web/containers/ModalCancelDownload/index.tsx
@@ -32,9 +32,9 @@ export default function ModalCancelDownload({
const { modelDownloadStateAtom } = useDownloadState()
useGetPerformanceTag()
const downloadAtom = useMemo(
- () => atom((get) => get(modelDownloadStateAtom)[suitableModel.id]),
+ () => atom((get) => get(modelDownloadStateAtom)[suitableModel.name]),
// eslint-disable-next-line react-hooks/exhaustive-deps
- [suitableModel.id]
+ [suitableModel.name]
)
const downloadState = useAtomValue(downloadAtom)
diff --git a/web/containers/Providers/EventHandler.tsx b/web/containers/Providers/EventHandler.tsx
index ff2934282..2c2ad3bd7 100644
--- a/web/containers/Providers/EventHandler.tsx
+++ b/web/containers/Providers/EventHandler.tsx
@@ -4,38 +4,31 @@ import { ReactNode, useEffect, useRef } from 'react'
import {
events,
EventName,
- NewMessageResponse,
+ ThreadMessage,
PluginType,
- ChatMessage,
+ MessageStatus,
} from '@janhq/core'
-import { Conversation, Message, MessageStatus } from '@janhq/core'
import { ConversationalPlugin, ModelPlugin } from '@janhq/core/lib/plugins'
import { useAtomValue, useSetAtom } from 'jotai'
import { useDownloadState } from '@/hooks/useDownloadState'
import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
-import { toChatMessage } from '@/utils/message'
-
import {
addNewMessageAtom,
chatMessages,
updateMessageAtom,
} from '@/helpers/atoms/ChatMessage.atom'
import {
- updateConversationAtom,
updateConversationWaitingForResponseAtom,
userConversationsAtom,
} from '@/helpers/atoms/Conversation.atom'
import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom'
import { pluginManager } from '@/plugin'
-let currentConversation: Conversation | undefined = undefined
-
export default function EventHandler({ children }: { children: ReactNode }) {
const addNewMessage = useSetAtom(addNewMessageAtom)
const updateMessage = useSetAtom(updateMessageAtom)
- const updateConversation = useSetAtom(updateConversationAtom)
const { setDownloadState, setDownloadStateSuccess } = useDownloadState()
const { downloadedModels, setDownloadedModels } = useGetDownloadedModels()
@@ -52,92 +45,55 @@ export default function EventHandler({ children }: { children: ReactNode }) {
convoRef.current = conversations
}, [messages, conversations])
- async function handleNewMessageResponse(message: NewMessageResponse) {
- if (message.conversationId) {
- const convo = convoRef.current.find((e) => e.id == message.conversationId)
+ async function handleNewMessageResponse(message: ThreadMessage) {
+ if (message.threadId) {
+ const convo = convoRef.current.find((e) => e.id == message.threadId)
if (!convo) return
- const newResponse = toChatMessage(message)
- addNewMessage(newResponse)
+ addNewMessage(message)
}
}
- async function handleMessageResponseUpdate(
- messageResponse: NewMessageResponse
- ) {
+ async function handleMessageResponseUpdate(messageResponse: ThreadMessage) {
if (
- messageResponse.conversationId &&
+ messageResponse.threadId &&
messageResponse.id &&
- messageResponse.message
+ messageResponse.content
) {
updateMessage(
messageResponse.id,
- messageResponse.conversationId,
- messageResponse.message,
+ messageResponse.threadId,
+ messageResponse.content,
MessageStatus.Pending
)
}
-
- if (messageResponse.conversationId) {
- if (
- !currentConversation ||
- currentConversation.id !== messageResponse.conversationId
- ) {
- if (convoRef.current && messageResponse.conversationId)
- currentConversation = convoRef.current.find(
- (e) => e.id == messageResponse.conversationId
- )
- }
-
- if (currentConversation) {
- const updatedConv: Conversation = {
- ...currentConversation,
- lastMessage: messageResponse.message,
- }
-
- updateConversation(updatedConv)
- }
- }
}
- async function handleMessageResponseFinished(
- messageResponse: NewMessageResponse
- ) {
- if (!messageResponse.conversationId || !convoRef.current) return
- updateConvWaiting(messageResponse.conversationId, false)
+ async function handleMessageResponseFinished(messageResponse: ThreadMessage) {
+ if (!messageResponse.threadId || !convoRef.current) return
+ updateConvWaiting(messageResponse.threadId, false)
if (
- messageResponse.conversationId &&
+ messageResponse.threadId &&
messageResponse.id &&
- messageResponse.message
+ messageResponse.content
) {
updateMessage(
messageResponse.id,
- messageResponse.conversationId,
- messageResponse.message,
+ messageResponse.threadId,
+ messageResponse.content,
MessageStatus.Ready
)
}
- const convo = convoRef.current.find(
- (e) => e.id == messageResponse.conversationId
+ const thread = convoRef.current.find(
+ (e) => e.id == messageResponse.threadId
)
- if (convo) {
- const messagesData = (messagesRef.current ?? [])[convo.id].map(
- (e: ChatMessage) => ({
- id: e.id,
- message: e.text,
- user: e.senderUid,
- updatedAt: new Date(e.createdAt).toISOString(),
- createdAt: new Date(e.createdAt).toISOString(),
- })
- )
+ if (thread) {
pluginManager
.get(PluginType.Conversational)
?.saveConversation({
- ...convo,
- id: convo.id ?? '',
- name: convo.name ?? '',
- message: convo.lastMessage ?? '',
- messages: messagesData,
+ ...thread,
+ id: thread.id ?? '',
+ messages: messagesRef.current[thread.id] ?? [],
})
}
}
@@ -149,7 +105,7 @@ export default function EventHandler({ children }: { children: ReactNode }) {
function handleDownloadSuccess(state: any) {
if (state && state.fileName && state.success === true) {
- state.fileName = state.fileName.replace('models/', '')
+ state.fileName = state.fileName.split('/').pop() ?? ''
setDownloadStateSuccess(state.fileName)
const model = models.find((e) => e.id === state.fileName)
if (model)
diff --git a/web/containers/Providers/EventListener.tsx b/web/containers/Providers/EventListener.tsx
index 2e91a70cf..9d4c2d890 100644
--- a/web/containers/Providers/EventListener.tsx
+++ b/web/containers/Providers/EventListener.tsx
@@ -27,6 +27,11 @@ export default function EventListenerWrapper({ children }: PropsWithChildren) {
const { setDownloadedModels, downloadedModels } = useGetDownloadedModels()
const { setDownloadState, setDownloadStateSuccess, setDownloadStateFailed } =
useDownloadState()
+ const downloadedModelRef = useRef(downloadedModels)
+
+ useEffect(() => {
+ downloadedModelRef.current = downloadedModels
+ }, [downloadedModels])
useEffect(() => {
if (window && window.electronAPI) {
@@ -60,7 +65,7 @@ export default function EventListenerWrapper({ children }: PropsWithChildren) {
.get(PluginType.Model)
?.saveModel(model)
.then(() => {
- setDownloadedModels([...downloadedModels, model])
+ setDownloadedModels([...downloadedModelRef.current, model])
})
}
}
diff --git a/web/helpers/atoms/ChatMessage.atom.ts b/web/helpers/atoms/ChatMessage.atom.ts
index d343d0f7c..14ad95a80 100644
--- a/web/helpers/atoms/ChatMessage.atom.ts
+++ b/web/helpers/atoms/ChatMessage.atom.ts
@@ -1,17 +1,21 @@
-import { ChatMessage, MessageStatus } from '@janhq/core'
+import { MessageStatus, ThreadMessage } from '@janhq/core'
import { atom } from 'jotai'
-import { getActiveConvoIdAtom } from './Conversation.atom'
+import {
+ conversationStatesAtom,
+ getActiveConvoIdAtom,
+ updateThreadStateLastMessageAtom,
+} from './Conversation.atom'
/**
* Stores all chat messages for all conversations
*/
-export const chatMessages = atom>({})
+export const chatMessages = atom>({})
/**
* Return the chat messages for the current active conversation
*/
-export const getCurrentChatMessagesAtom = atom((get) => {
+export const getCurrentChatMessagesAtom = atom((get) => {
const activeConversationId = get(getActiveConvoIdAtom)
if (!activeConversationId) return []
const messages = get(chatMessages)[activeConversationId]
@@ -20,11 +24,11 @@ export const getCurrentChatMessagesAtom = atom((get) => {
export const setCurrentChatMessagesAtom = atom(
null,
- (get, set, messages: ChatMessage[]) => {
+ (get, set, messages: ThreadMessage[]) => {
const currentConvoId = get(getActiveConvoIdAtom)
if (!currentConvoId) return
- const newData: Record = {
+ const newData: Record = {
...get(chatMessages),
}
newData[currentConvoId] = messages
@@ -34,8 +38,8 @@ export const setCurrentChatMessagesAtom = atom(
export const setConvoMessagesAtom = atom(
null,
- (get, set, messages: ChatMessage[], convoId: string) => {
- const newData: Record = {
+ (get, set, messages: ThreadMessage[], convoId: string) => {
+ const newData: Record = {
...get(chatMessages),
}
newData[convoId] = messages
@@ -48,14 +52,14 @@ export const setConvoMessagesAtom = atom(
*/
export const addOldMessagesAtom = atom(
null,
- (get, set, newMessages: ChatMessage[]) => {
+ (get, set, newMessages: ThreadMessage[]) => {
const currentConvoId = get(getActiveConvoIdAtom)
if (!currentConvoId) return
const currentMessages = get(chatMessages)[currentConvoId] ?? []
const updatedMessages = [...currentMessages, ...newMessages]
- const newData: Record = {
+ const newData: Record = {
...get(chatMessages),
}
newData[currentConvoId] = updatedMessages
@@ -65,23 +69,25 @@ export const addOldMessagesAtom = atom(
export const addNewMessageAtom = atom(
null,
- (get, set, newMessage: ChatMessage) => {
+ (get, set, newMessage: ThreadMessage) => {
const currentConvoId = get(getActiveConvoIdAtom)
if (!currentConvoId) return
const currentMessages = get(chatMessages)[currentConvoId] ?? []
const updatedMessages = [newMessage, ...currentMessages]
- const newData: Record = {
+ const newData: Record = {
...get(chatMessages),
}
newData[currentConvoId] = updatedMessages
set(chatMessages, newData)
+ // Update thread last message
+ set(updateThreadStateLastMessageAtom, currentConvoId, newMessage.content)
}
)
export const deleteConversationMessage = atom(null, (get, set, id: string) => {
- const newData: Record = {
+ const newData: Record = {
...get(chatMessages),
}
newData[id] = []
@@ -101,15 +107,17 @@ export const updateMessageAtom = atom(
const messages = get(chatMessages)[conversationId] ?? []
const message = messages.find((e) => e.id === id)
if (message) {
- message.text = text
+ message.content = text
message.status = status
const updatedMessages = [...messages]
- const newData: Record = {
+ const newData: Record = {
...get(chatMessages),
}
newData[conversationId] = updatedMessages
set(chatMessages, newData)
+ // Update thread last message
+ set(updateThreadStateLastMessageAtom, conversationId, text)
}
}
)
@@ -130,14 +138,14 @@ export const updateLastMessageAsReadyAtom = atom(
if (!messageToUpdate) return
const index = currentMessages.indexOf(messageToUpdate)
- const updatedMsg: ChatMessage = {
+ const updatedMsg: ThreadMessage = {
...messageToUpdate,
status: MessageStatus.Ready,
- text: text,
+ content: text,
}
currentMessages[index] = updatedMsg
- const newData: Record = {
+ const newData: Record = {
...get(chatMessages),
}
newData[currentConvoId] = currentMessages
diff --git a/web/helpers/atoms/Conversation.atom.ts b/web/helpers/atoms/Conversation.atom.ts
index 2265c5c2e..60748e038 100644
--- a/web/helpers/atoms/Conversation.atom.ts
+++ b/web/helpers/atoms/Conversation.atom.ts
@@ -1,6 +1,8 @@
-import { Conversation, ConversationState } from '@janhq/core'
+import { Thread } from '@janhq/core'
import { atom } from 'jotai'
+import { ThreadState } from '@/types/conversation'
+
/**
* Stores the current active conversation id.
*/
@@ -19,23 +21,19 @@ export const waitingToSendMessage = atom(undefined)
/**
* Stores all conversation states for the current user
*/
-export const conversationStatesAtom = atom>(
- {}
-)
-export const currentConvoStateAtom = atom(
- (get) => {
- const activeConvoId = get(activeConversationIdAtom)
- if (!activeConvoId) {
- console.debug('Active convo id is undefined')
- return undefined
- }
-
- return get(conversationStatesAtom)[activeConvoId]
+export const conversationStatesAtom = atom>({})
+export const currentConvoStateAtom = atom((get) => {
+ const activeConvoId = get(activeConversationIdAtom)
+ if (!activeConvoId) {
+ console.debug('Active convo id is undefined')
+ return undefined
}
-)
+
+ return get(conversationStatesAtom)[activeConvoId]
+})
export const addNewConversationStateAtom = atom(
null,
- (get, set, conversationId: string, state: ConversationState) => {
+ (get, set, conversationId: string, state: ThreadState) => {
const currentState = { ...get(conversationStatesAtom) }
currentState[conversationId] = state
set(conversationStatesAtom, currentState)
@@ -73,16 +71,28 @@ export const updateConversationHasMoreAtom = atom(
}
)
+export const updateThreadStateLastMessageAtom = atom(
+ null,
+ (get, set, conversationId: string, lastMessage?: string) => {
+ const currentState = { ...get(conversationStatesAtom) }
+ currentState[conversationId] = {
+ ...currentState[conversationId],
+ lastMessage,
+ }
+ set(conversationStatesAtom, currentState)
+ }
+)
+
export const updateConversationAtom = atom(
null,
- (get, set, conversation: Conversation) => {
+ (get, set, conversation: Thread) => {
const id = conversation.id
if (!id) return
const convo = get(userConversationsAtom).find((c) => c.id === id)
if (!convo) return
- const newConversations: Conversation[] = get(userConversationsAtom).map(
- (c) => (c.id === id ? conversation : c)
+ const newConversations: Thread[] = get(userConversationsAtom).map((c) =>
+ c.id === id ? conversation : c
)
// sort new conversations based on updated at
@@ -99,7 +109,7 @@ export const updateConversationAtom = atom(
/**
* Stores all conversations for the current user
*/
-export const userConversationsAtom = atom([])
-export const currentConversationAtom = atom((get) =>
+export const userConversationsAtom = atom([])
+export const currentConversationAtom = atom((get) =>
get(userConversationsAtom).find((c) => c.id === get(getActiveConvoIdAtom))
)
diff --git a/web/hooks/useActiveModel.ts b/web/hooks/useActiveModel.ts
index efe05672e..3496f8396 100644
--- a/web/hooks/useActiveModel.ts
+++ b/web/hooks/useActiveModel.ts
@@ -1,4 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
+import { join } from 'path'
+
import { PluginType } from '@janhq/core'
import { InferencePlugin } from '@janhq/core/lib/plugins'
import { Model } from '@janhq/core/lib/types'
@@ -10,7 +12,6 @@ import { toaster } from '@/containers/Toast'
import { useGetDownloadedModels } from './useGetDownloadedModels'
import { pluginManager } from '@/plugin'
-import { join } from 'path'
const activeAssistantModelAtom = atom(undefined)
@@ -43,7 +44,7 @@ export function useActiveModel() {
const currentTime = Date.now()
console.debug('Init model: ', modelId)
- const path = join('models', model.productName, modelId)
+ const path = join('models', model.name, modelId)
const res = await initModel(path)
if (res?.error) {
const errorMessage = `${res.error}`
diff --git a/web/hooks/useCreateConversation.ts b/web/hooks/useCreateConversation.ts
index e5f2a669f..8984c0bfc 100644
--- a/web/hooks/useCreateConversation.ts
+++ b/web/hooks/useCreateConversation.ts
@@ -1,5 +1,5 @@
import { PluginType } from '@janhq/core'
-import { Conversation, Model } from '@janhq/core'
+import { Thread, Model } from '@janhq/core'
import { ConversationalPlugin } from '@janhq/core/lib/plugins'
import { useAtom, useSetAtom } from 'jotai'
@@ -20,11 +20,11 @@ export const useCreateConversation = () => {
const addNewConvoState = useSetAtom(addNewConversationStateAtom)
const requestCreateConvo = async (model: Model) => {
- const conversationName = model.name
- const mappedConvo: Conversation = {
+ const summary = model.name
+ const mappedConvo: Thread = {
id: generateConversationId(),
modelId: model.id,
- name: conversationName,
+ summary,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
messages: [],
@@ -37,11 +37,7 @@ export const useCreateConversation = () => {
pluginManager
.get(PluginType.Conversational)
- ?.saveConversation({
- ...mappedConvo,
- name: mappedConvo.name ?? '',
- messages: [],
- })
+ ?.saveConversation(mappedConvo)
setUserConversations([mappedConvo, ...userConversations])
setActiveConvoId(mappedConvo.id)
}
diff --git a/web/hooks/useDeleteModel.ts b/web/hooks/useDeleteModel.ts
index 4d527e8cb..f59860719 100644
--- a/web/hooks/useDeleteModel.ts
+++ b/web/hooks/useDeleteModel.ts
@@ -1,3 +1,5 @@
+import { join } from 'path'
+
import { PluginType } from '@janhq/core'
import { ModelPlugin } from '@janhq/core/lib/plugins'
import { Model } from '@janhq/core/lib/types'
@@ -7,13 +9,12 @@ import { toaster } from '@/containers/Toast'
import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
import { pluginManager } from '@/plugin/PluginManager'
-import { join } from 'path'
export default function useDeleteModel() {
const { setDownloadedModels, downloadedModels } = useGetDownloadedModels()
const deleteModel = async (model: Model) => {
- const path = join('models', model.productName, model.id)
+ const path = join('models', model.name, model.id)
await pluginManager.get(PluginType.Model)?.deleteModel(path)
// reload models
diff --git a/web/hooks/useDownloadModel.ts b/web/hooks/useDownloadModel.ts
index 3ec1bf330..bbe48f397 100644
--- a/web/hooks/useDownloadModel.ts
+++ b/web/hooks/useDownloadModel.ts
@@ -7,6 +7,7 @@ import { useAtom } from 'jotai'
import { useDownloadState } from './useDownloadState'
import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom'
+
import { pluginManager } from '@/plugin/PluginManager'
export default function useDownloadModel() {
@@ -20,28 +21,24 @@ export default function useDownloadModel() {
modelVersion: ModelVersion
): Model => {
return {
- // eslint-disable-next-line @typescript-eslint/naming-convention
- id: modelVersion.id,
- name: modelVersion.name,
- quantMethod: modelVersion.quantMethod,
+ /**
+ * Id will be used for the model file name
+ * Should be the version name
+ */
+ id: modelVersion.name,
+ name: model.name,
+ quantizationName: modelVersion.quantizationName,
bits: modelVersion.bits,
size: modelVersion.size,
maxRamRequired: modelVersion.maxRamRequired,
usecase: modelVersion.usecase,
downloadLink: modelVersion.downloadLink,
- startDownloadAt: modelVersion.startDownloadAt,
- finishDownloadAt: modelVersion.finishDownloadAt,
- productId: model.id,
- productName: model.name,
shortDescription: model.shortDescription,
longDescription: model.longDescription,
avatarUrl: model.avatarUrl,
author: model.author,
version: model.version,
modelUrl: model.modelUrl,
- createdAt: new Date(model.createdAt).getTime(),
- updatedAt: new Date(model.updatedAt ?? '').getTime(),
- status: '',
releaseDate: -1,
tags: model.tags,
}
@@ -53,7 +50,7 @@ export default function useDownloadModel() {
) => {
// set an initial download state
setDownloadState({
- modelId: modelVersion.id,
+ modelId: modelVersion.name,
time: {
elapsed: 0,
remaining: 0,
@@ -64,10 +61,9 @@ export default function useDownloadModel() {
total: 0,
transferred: 0,
},
- fileName: modelVersion.id,
+ fileName: modelVersion.name,
})
- modelVersion.startDownloadAt = Date.now()
const assistantModel = assistanModel(model, modelVersion)
setDownloadingModels([...downloadingModels, assistantModel])
diff --git a/web/hooks/useGetInputState.ts b/web/hooks/useGetInputState.ts
index d1b52f080..f7934ae2c 100644
--- a/web/hooks/useGetInputState.ts
+++ b/web/hooks/useGetInputState.ts
@@ -1,8 +1,11 @@
import { useEffect, useState } from 'react'
-import { Model, Conversation } from '@janhq/core'
+
+import { Model, Thread } from '@janhq/core'
import { useAtomValue } from 'jotai'
+
import { useActiveModel } from './useActiveModel'
import { useGetDownloadedModels } from './useGetDownloadedModels'
+
import { currentConversationAtom } from '@/helpers/atoms/Conversation.atom'
export default function useGetInputState() {
@@ -12,7 +15,7 @@ export default function useGetInputState() {
const { downloadedModels } = useGetDownloadedModels()
const handleInputState = (
- convo: Conversation | undefined,
+ convo: Thread | undefined,
currentModel: Model | undefined
) => {
if (convo == null) return
diff --git a/web/hooks/useGetUserConversations.ts b/web/hooks/useGetUserConversations.ts
index 5f0ad7435..3a6e4eb55 100644
--- a/web/hooks/useGetUserConversations.ts
+++ b/web/hooks/useGetUserConversations.ts
@@ -1,16 +1,14 @@
-import { PluginType, ChatMessage, ConversationState } from '@janhq/core'
+import { PluginType, Thread } from '@janhq/core'
import { ConversationalPlugin } from '@janhq/core/lib/plugins'
-import { Conversation } from '@janhq/core/lib/types'
import { useSetAtom } from 'jotai'
-import { toChatMessage } from '@/utils/message'
-
import { setConvoMessagesAtom } from '@/helpers/atoms/ChatMessage.atom'
import {
conversationStatesAtom,
userConversationsAtom,
} from '@/helpers/atoms/Conversation.atom'
import { pluginManager } from '@/plugin/PluginManager'
+import { ThreadState } from '@/types/conversation'
const useGetUserConversations = () => {
const setConversationStates = useSetAtom(conversationStatesAtom)
@@ -19,19 +17,17 @@ const useGetUserConversations = () => {
const getUserConversations = async () => {
try {
- const convos: Conversation[] | undefined = await pluginManager
+ const convos: Thread[] | undefined = await pluginManager
.get(PluginType.Conversational)
?.getConversations()
- const convoStates: Record = {}
+ const convoStates: Record = {}
convos?.forEach((convo) => {
convoStates[convo.id ?? ''] = {
hasMore: true,
waitingForResponse: false,
+ lastMessage: convo.messages[0]?.content ?? '',
}
- setConvoMessages(
- convo.messages.map((msg) => toChatMessage(msg)),
- convo.id ?? ''
- )
+ setConvoMessages(convo.messages, convo.id ?? '')
})
setConversationStates(convoStates)
setConversations(convos ?? [])
diff --git a/web/hooks/useSendChatMessage.ts b/web/hooks/useSendChatMessage.ts
index 5d5e1598c..676a75c2f 100644
--- a/web/hooks/useSendChatMessage.ts
+++ b/web/hooks/useSendChatMessage.ts
@@ -1,18 +1,21 @@
import {
+ ChatCompletionMessage,
+ ChatCompletionRole,
EventName,
- MessageHistory,
- NewMessageRequest,
+ MessageRequest,
+ MessageStatus,
PluginType,
+ Thread,
+ ThreadMessage,
events,
- ChatMessage,
- Message,
- Conversation,
- MessageSenderType,
} from '@janhq/core'
import { ConversationalPlugin, InferencePlugin } from '@janhq/core/lib/plugins'
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
-import { currentPromptAtom } from '@/containers/Providers/Jotai'
+
import { ulid } from 'ulid'
+
+import { currentPromptAtom } from '@/containers/Providers/Jotai'
+
import {
addNewMessageAtom,
getCurrentChatMessagesAtom,
@@ -23,7 +26,6 @@ import {
updateConversationWaitingForResponseAtom,
} from '@/helpers/atoms/Conversation.atom'
import { pluginManager } from '@/plugin/PluginManager'
-import { toChatMessage } from '@/utils/message'
export default function useSendChatMessage() {
const currentConvo = useAtomValue(currentConversationAtom)
@@ -35,7 +37,7 @@ export default function useSendChatMessage() {
let timeout: NodeJS.Timeout | undefined = undefined
- function updateConvSummary(newMessage: NewMessageRequest) {
+ function updateConvSummary(newMessage: MessageRequest) {
if (timeout) {
clearTimeout(timeout)
}
@@ -46,13 +48,19 @@ export default function useSendChatMessage() {
currentConvo.summary === '' ||
currentConvo.summary.startsWith('Prompt:')
) {
+ const summaryMsg: ChatCompletionMessage = {
+ role: ChatCompletionRole.User,
+ content:
+ 'summary this conversation in 5 words, the response should just include the summary',
+ }
// Request convo summary
setTimeout(async () => {
- newMessage.message =
- 'summary this conversation in 5 words, the response should just include the summary'
const result = await pluginManager
.get(PluginType.Inference)
- ?.inferenceRequest(newMessage)
+ ?.inferenceRequest({
+ ...newMessage,
+ messages: newMessage.messages?.concat([summaryMsg]),
+ })
if (
result?.message &&
@@ -68,15 +76,7 @@ export default function useSendChatMessage() {
.get(PluginType.Conversational)
?.saveConversation({
...updatedConv,
- name: updatedConv.name ?? '',
- message: updatedConv.lastMessage ?? '',
- messages: currentMessages.map((e: ChatMessage) => ({
- id: e.id,
- message: e.text,
- user: e.senderUid,
- updatedAt: new Date(e.createdAt).toISOString(),
- createdAt: new Date(e.createdAt).toISOString(),
- })),
+ messages: currentMessages,
})
}
}, 1000)
@@ -85,64 +85,60 @@ export default function useSendChatMessage() {
}
const sendChatMessage = async () => {
- const convoId = currentConvo?.id
- if (!convoId) {
+ const threadId = currentConvo?.id
+ if (!threadId) {
console.error('No conversation id')
return
}
setCurrentPrompt('')
- updateConvWaiting(convoId, true)
+ updateConvWaiting(threadId, true)
const prompt = currentPrompt.trim()
- const messageHistory: MessageHistory[] = currentMessages
- .map((msg) => ({
- role: msg.senderUid,
- content: msg.text ?? '',
+ const messages: ChatCompletionMessage[] = currentMessages
+ .map((msg) => ({
+ role: msg.role ?? ChatCompletionRole.User,
+ content: msg.content ?? '',
}))
.reverse()
.concat([
{
- role: MessageSenderType.User,
+ role: ChatCompletionRole.User,
content: prompt,
- } as MessageHistory,
+ } as ChatCompletionMessage,
])
- const newMessage: NewMessageRequest = {
+ const messageRequest: MessageRequest = {
id: ulid(),
- conversationId: convoId,
- message: prompt,
- user: MessageSenderType.User,
- createdAt: new Date().toISOString(),
- history: messageHistory,
+ threadId: threadId,
+ messages,
}
- const newChatMessage = toChatMessage(newMessage)
- addNewMessage(newChatMessage)
+ const threadMessage: ThreadMessage = {
+ id: messageRequest.id,
+ threadId: messageRequest.threadId,
+ content: prompt,
+ role: ChatCompletionRole.User,
+ createdAt: new Date().toISOString(),
+ status: MessageStatus.Ready,
+ }
+ addNewMessage(threadMessage)
// delay randomly from 50 - 100ms
// to prevent duplicate message id
const delay = Math.floor(Math.random() * 50) + 50
await new Promise((resolve) => setTimeout(resolve, delay))
- events.emit(EventName.OnNewMessageRequest, newMessage)
+ events.emit(EventName.OnNewMessageRequest, messageRequest)
if (!currentConvo?.summary && currentConvo) {
- const updatedConv: Conversation = {
+ const updatedConv: Thread = {
...currentConvo,
- lastMessage: prompt,
summary: `Prompt: ${prompt}`,
}
- updateConversation(updatedConv)
- } else if (currentConvo) {
- const updatedConv: Conversation = {
- ...currentConvo,
- lastMessage: prompt,
- }
-
updateConversation(updatedConv)
}
- updateConvSummary(newMessage)
+ updateConvSummary(messageRequest)
}
return {
diff --git a/web/screens/Chat/ChatBody/index.tsx b/web/screens/Chat/ChatBody/index.tsx
index 755e3d6b7..b5c635262 100644
--- a/web/screens/Chat/ChatBody/index.tsx
+++ b/web/screens/Chat/ChatBody/index.tsx
@@ -10,7 +10,7 @@ const ChatBody: React.FC = () => {
return (