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 -![Project Cover](https://github.com/janhq/jan/assets/89722390/be4f07ef-13df-4621-8f25-b861f1d5b7b3) +![Jan banner](https://github.com/janhq/jan/assets/89722390/35daac7d-b895-487c-a6ac-6663daaad78e)

@@ -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 0ad4b158d..7dea62115 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 @@ -18,7 +30,7 @@ _Users can configure model settings, like run parameters_ _Users can override run settings at runtime_ -- See [assistant]() and [thread]() +- See Assistant Spec and Thread ## Jan Model Object 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/docs/src/components/Elements/downloadLink.js b/docs/src/components/Elements/downloadLink.js new file mode 100644 index 000000000..744961380 --- /dev/null +++ b/docs/src/components/Elements/downloadLink.js @@ -0,0 +1,127 @@ +import React, { useState, useEffect } from "react"; +import axios from "axios"; + +const systemsTemplate = [ + { + name: "Download for Mac (M1/M2)", + logo: require("@site/static/img/apple-logo-white.png").default, + fileFormat: "{appname}-mac-arm64-{tag}.dmg", + }, + { + name: "Download for Mac (Intel)", + logo: require("@site/static/img/apple-logo-white.png").default, + fileFormat: "{appname}-mac-x64-{tag}.dmg", + }, + { + name: "Download for Windows", + logo: require("@site/static/img/windows-logo-white.png").default, + fileFormat: "{appname}-win-x64-{tag}.exe", + }, + { + name: "Download for Linux", + logo: require("@site/static/img/linux-logo-white.png").default, + fileFormat: "{appname}-linux-amd64-{tag}.deb", + }, +]; + +function classNames(...classes) { + return classes.filter(Boolean).join(" "); +} + +export default function DownloadLink() { + const [systems, setSystems] = useState(systemsTemplate); + const [defaultSystem, setDefaultSystem] = useState(systems[0]); + + const getLatestReleaseInfo = async (repoOwner, repoName) => { + const url = `https://api.github.com/repos/${repoOwner}/${repoName}/releases/latest`; + try { + const response = await axios.get(url); + return response.data; + } catch (error) { + console.error(error); + return null; + } + }; + + const extractAppName = (fileName) => { + // Extract appname using a regex that matches the provided file formats + const regex = /^(.*?)-(?:mac|win|linux)-(?:arm64|x64|amd64)-.*$/; + const match = fileName.match(regex); + return match ? match[1] : null; + }; + + const changeDefaultSystem = async (systems) => { + const userAgent = navigator.userAgent; + + const arc = await navigator?.userAgentData?.getHighEntropyValues([ + "architecture", + ]); + + if (userAgent.includes("Windows")) { + // windows user + setDefaultSystem(systems[2]); + } else if (userAgent.includes("Linux")) { + // linux user + setDefaultSystem(systems[3]); + } else if ( + userAgent.includes("Mac OS") && + arc && + arc.architecture === "arm" + ) { + setDefaultSystem(systems[0]); + } else { + setDefaultSystem(systems[1]); + } + }; + + useEffect(() => { + const updateDownloadLinks = async () => { + try { + const releaseInfo = await getLatestReleaseInfo("janhq", "jan"); + + // Extract appname from the first asset name + const firstAssetName = releaseInfo.assets[0].name; + const appname = extractAppName(firstAssetName); + + if (!appname) { + console.error( + "Failed to extract appname from file name:", + firstAssetName + ); + changeDefaultSystem(systems); + return; + } + + // Remove 'v' at the start of the tag_name + const tag = releaseInfo.tag_name.startsWith("v") + ? releaseInfo.tag_name.substring(1) + : releaseInfo.tag_name; + + const updatedSystems = systems.map((system) => { + const downloadUrl = system.fileFormat + .replace("{appname}", appname) + .replace("{tag}", tag); + return { + ...system, + href: `https://github.com/janhq/jan/releases/download/${releaseInfo.tag_name}/${downloadUrl}`, + }; + }); + + setSystems(updatedSystems); + changeDefaultSystem(updatedSystems); + } catch (error) { + console.error("Failed to update download links:", error); + } + }; + + updateDownloadLinks(); + }, []); + + return ( +

+ + Download Jan + +
+ ); +} diff --git a/docs/src/components/Elements/dropdown.js b/docs/src/components/Elements/dropdown.js index 31c2bf05a..5709a89ac 100644 --- a/docs/src/components/Elements/dropdown.js +++ b/docs/src/components/Elements/dropdown.js @@ -76,6 +76,7 @@ export default function Dropdown() { setDefaultSystem(systems[1]); } }; + useEffect(() => { const updateDownloadLinks = async () => { try { diff --git a/docs/src/pages/index.js b/docs/src/pages/index.js index bfefa0343..3bb1d0cf7 100644 --- a/docs/src/pages/index.js +++ b/docs/src/pages/index.js @@ -5,41 +5,12 @@ import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; import useBaseUrl from "@docusaurus/useBaseUrl"; import Layout from "@theme/Layout"; import AnnoncementBanner from "@site/src/components/Announcement"; -import { - CloudArrowUpIcon, - CursorArrowRaysIcon, - ShieldCheckIcon, - CpuChipIcon, - ClipboardDocumentIcon, - CubeTransparentIcon, - ComputerDesktopIcon, - FolderPlusIcon, -} from "@heroicons/react/24/outline"; + +import { AiOutlineGithub } from "react-icons/ai"; import ThemedImage from "@theme/ThemedImage"; -const features = [ - { - name: "Personal AI that runs on your computer", - desc: "Jan runs directly on your local machine, offering privacy, convenience and customizability.", - icon: ComputerDesktopIcon, - }, - { - name: "Private and offline, your data never leaves your machine", - desc: "Your conversations and data are with an AI that runs on your computer, where only you have access.", - icon: ShieldCheckIcon, - }, - { - name: "No subscription fees, the AI runs on your computer", - desc: "Say goodbye to monthly subscriptions or usage-based APIs. Jan runs 100% free on your own hardware.", - icon: CubeTransparentIcon, - }, - { - name: "Extendable via App and Plugin framework", - desc: "Jan has a versatile app and plugin framework, allowing you to customize it to your needs.", - icon: FolderPlusIcon, - }, -]; +import DownloadLink from "@site/src/components/Elements/downloadLink"; export default function Home() { const { siteConfig } = useDocusaurusContext(); @@ -48,8 +19,7 @@ export default function Home() {
@@ -75,7 +45,6 @@ export default function Home() {

*/} -

Own your AI

@@ -111,7 +80,6 @@ export default function Home() {
- {/*
*/}
- -
+

AI that you control

- Jan runs Large Language Models locally on Windows, Mac and Linux. - Available on Desktop and Cloud-Native. + Private. Local. Infinitely Customizable.

-
- {features.map((feat, i) => { - return ( -
-
+ +
+
+
+

+ Your AI, forever. +

+

+ Apps come and go, but your AI and data should last.{" "} +

+
+
-
{feat.name}
-

{feat.desc}

+ Icon - Lock +

+ Jan uses open, standard and non-proprietary files stored + locally on your device. +

+
+
+ Icon - Camera +

+ You have total control over your AI, which means you can + use Jan offline and switch to another app easily if you + want. +

- ); - })} +
+
+ +
+ +

100% free on your own hardware

+ +
+
+
+ +
+

+ We are open-source.
Join Jan community. +

+
diff --git a/docs/src/styles/card.scss b/docs/src/styles/card.scss new file mode 100644 index 000000000..9149c3d0d --- /dev/null +++ b/docs/src/styles/card.scss @@ -0,0 +1,31 @@ +@layer components { + .card-link-bg { + background: linear-gradient(180deg, #fff 0%, #fff 100%); + box-shadow: + 0px 10px 10px -5px rgba(0, 0, 0, 0.1), + 0px 20px 25px -5px rgba(0, 0, 0, 0.1), + 0px 1px 2px 0px #f1f1f1 inset; + } + + .card-link-bg-dark { + background: linear-gradient(180deg, #101118 0%, #101118 100%); + box-shadow: + 0px 10px 10px -5px rgba(0, 0, 0, 0.3), + 0px 1px 2px 0px #525154 inset; + } + .card { + @apply rounded-3xl border bg-gray-100 border-gray-100 dark:border-[#202231] dark:bg-[#111217]; + + &-link { + display: inline-flex; + padding: 8px 16px; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 8px; + border-radius: 16px; + font-size: 14px; + cursor: pointer; + } + } +} diff --git a/docs/src/styles/main.scss b/docs/src/styles/main.scss index ea6e4f723..459a1197b 100644 --- a/docs/src/styles/main.scss +++ b/docs/src/styles/main.scss @@ -6,3 +6,4 @@ @import "./tweaks.scss"; @import "./base.scss"; @import "./components.scss"; +@import "./card.scss"; diff --git a/docs/static/img/card-element.png b/docs/static/img/card-element.png new file mode 100644 index 000000000..94fab7029 Binary files /dev/null and b/docs/static/img/card-element.png differ diff --git a/docs/static/img/card-framework-dark.png b/docs/static/img/card-framework-dark.png new file mode 100644 index 000000000..ca59ecd76 Binary files /dev/null and b/docs/static/img/card-framework-dark.png differ diff --git a/docs/static/img/card-framework-light.png b/docs/static/img/card-framework-light.png new file mode 100644 index 000000000..01bdb999a Binary files /dev/null and b/docs/static/img/card-framework-light.png differ diff --git a/docs/static/img/card-free-dark.png b/docs/static/img/card-free-dark.png new file mode 100644 index 000000000..875c01fd7 Binary files /dev/null and b/docs/static/img/card-free-dark.png differ diff --git a/docs/static/img/card-free-light.png b/docs/static/img/card-free-light.png new file mode 100644 index 000000000..a57439df5 Binary files /dev/null and b/docs/static/img/card-free-light.png differ diff --git a/docs/static/img/card-nitro-dark.png b/docs/static/img/card-nitro-dark.png new file mode 100644 index 000000000..749fd8f91 Binary files /dev/null and b/docs/static/img/card-nitro-dark.png differ diff --git a/docs/static/img/card-nitro-light.png b/docs/static/img/card-nitro-light.png new file mode 100644 index 000000000..13a2f2179 Binary files /dev/null and b/docs/static/img/card-nitro-light.png differ diff --git a/docs/static/img/discord-element-dark.png b/docs/static/img/discord-element-dark.png new file mode 100644 index 000000000..0dc882bca Binary files /dev/null and b/docs/static/img/discord-element-dark.png differ diff --git a/docs/static/img/discord-element-light.png b/docs/static/img/discord-element-light.png new file mode 100644 index 000000000..9f483bf28 Binary files /dev/null and b/docs/static/img/discord-element-light.png differ diff --git a/docs/static/img/discord-logo.png b/docs/static/img/discord-logo.png new file mode 100644 index 000000000..670007f91 Binary files /dev/null and b/docs/static/img/discord-logo.png differ diff --git a/docs/static/img/github-element-dark.png b/docs/static/img/github-element-dark.png new file mode 100644 index 000000000..5331467e5 Binary files /dev/null and b/docs/static/img/github-element-dark.png differ diff --git a/docs/static/img/group-chat-dark.png b/docs/static/img/group-chat-dark.png new file mode 100644 index 000000000..5b8be82a7 Binary files /dev/null and b/docs/static/img/group-chat-dark.png differ diff --git a/docs/static/img/group-chat-light.png b/docs/static/img/group-chat-light.png new file mode 100644 index 000000000..2f716f4a2 Binary files /dev/null and b/docs/static/img/group-chat-light.png differ diff --git a/docs/static/img/ic-baseline-control-camera.svg b/docs/static/img/ic-baseline-control-camera.svg new file mode 100644 index 000000000..46daf8f0e --- /dev/null +++ b/docs/static/img/ic-baseline-control-camera.svg @@ -0,0 +1,4 @@ + + + + diff --git a/docs/static/img/ic-park-solid-unlock.svg b/docs/static/img/ic-park-solid-unlock.svg new file mode 100644 index 000000000..3c8729f91 --- /dev/null +++ b/docs/static/img/ic-park-solid-unlock.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/docs/static/img/jan-icon-dark.png b/docs/static/img/jan-icon-dark.png new file mode 100644 index 000000000..d18b13fdd Binary files /dev/null and b/docs/static/img/jan-icon-dark.png differ diff --git a/docs/static/img/jan-icon-light.png b/docs/static/img/jan-icon-light.png new file mode 100644 index 000000000..182f8569c Binary files /dev/null and b/docs/static/img/jan-icon-light.png differ diff --git a/docs/tailwind.config.js b/docs/tailwind.config.js index 47235b6ef..24c343197 100644 --- a/docs/tailwind.config.js +++ b/docs/tailwind.config.js @@ -13,6 +13,7 @@ module.exports = { }, fontFamily: { sans: [ + "Inter", "-apple-system", "BlinkMacSystemFont", "Segoe UI", 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 (
{messages.map((message) => ( - + ))}
) diff --git a/web/screens/Chat/ChatItem/index.tsx b/web/screens/Chat/ChatItem/index.tsx index 86163bbbe..a085c3dc8 100644 --- a/web/screens/Chat/ChatItem/index.tsx +++ b/web/screens/Chat/ChatItem/index.tsx @@ -1,24 +1,14 @@ import React, { forwardRef } from 'react' -import { ChatMessage } from '@janhq/core' -import SimpleTextMessage from '../SimpleTextMessage' -type Props = { - message: ChatMessage -} +import { ThreadMessage } from '@janhq/core' + +import SimpleTextMessage from '../SimpleTextMessage' type Ref = HTMLDivElement -const ChatItem = forwardRef(({ message }, ref) => ( +const ChatItem = forwardRef((message, ref) => (
- +
)) diff --git a/web/screens/Chat/HistoryList/index.tsx b/web/screens/Chat/HistoryList/index.tsx index 34587e2ae..3d5f47a22 100644 --- a/web/screens/Chat/HistoryList/index.tsx +++ b/web/screens/Chat/HistoryList/index.tsx @@ -1,6 +1,6 @@ import { useEffect } from 'react' -import { Conversation, Model } from '@janhq/core/lib/types' +import { Thread, Model } from '@janhq/core' import { Button } from '@janhq/uikit' import { motion as m } from 'framer-motion' import { useAtomValue, useSetAtom } from 'jotai' @@ -17,6 +17,7 @@ import useGetUserConversations from '@/hooks/useGetUserConversations' import { displayDate } from '@/utils/datetime' import { + conversationStatesAtom, getActiveConvoIdAtom, setActiveConvoIdAtom, userConversationsAtom, @@ -24,6 +25,7 @@ import { export default function HistoryList() { const conversations = useAtomValue(userConversationsAtom) + const threadStates = useAtomValue(conversationStatesAtom) const { getUserConversations } = useGetUserConversations() const { activeModel, startModel } = useActiveModel() const { requestCreateConvo } = useCreateConversation() @@ -41,7 +43,7 @@ export default function HistoryList() { return } - const handleActiveModel = async (convo: Conversation) => { + const handleActiveModel = async (convo: Thread) => { if (convo.modelId == null) { console.debug('modelId is undefined') return @@ -83,6 +85,7 @@ export default function HistoryList() { ) : ( conversations.map((convo, i) => { + const lastMessage = threadStates[convo.id]?.lastMessage return (
handleActiveModel(convo as Conversation)} + onClick={() => handleActiveModel(convo as Thread)} >

{convo.updatedAt && displayDate(new Date(convo.updatedAt).getTime())}

-

{convo.summary ?? convo.name}

+

{convo.summary}

- {convo?.lastMessage ?? 'No new message'} + {/* TODO: Check latest message update */} + {lastMessage && lastMessage.length > 0 + ? lastMessage + : 'No new message'}

{activeModel && activeConvoId === convo.id && ( = ({ - senderName, - senderType, - createdAt, - // will use status as streaming text - // status, - text = '', -}) => { - const parsedText = marked.parse(text) - const isUser = senderType === 'user' +const SimpleTextMessage: React.FC = (props) => { + const parsedText = marked.parse(props.content ?? '') + const isUser = props.role === ChatCompletionRole.User return (
@@ -70,12 +54,12 @@ const SimpleTextMessage: React.FC = ({ )} > {!isUser && } -
{senderName}
-

{displayDate(createdAt)}

+
{props.role}
+

{displayDate(props.createdAt)}

- {text === '' ? ( + {!props.content || props.content === '' ? ( ) : ( <> diff --git a/web/screens/Chat/index.tsx b/web/screens/Chat/index.tsx index 35936575d..5d7d22f37 100644 --- a/web/screens/Chat/index.tsx +++ b/web/screens/Chat/index.tsx @@ -126,7 +126,7 @@ const ChatScreen = () => { {isEnableChat && currentConvo && (
- {currentConvo?.name ?? ''} + {currentConvo?.summary ?? ''}
(({ model }, ref) => { return null } - const { quantMethod, bits, maxRamRequired, usecase } = suitableModel + const { quantizationName, bits, maxRamRequired, usecase } = suitableModel return (
(({ model }, ref) => { Version
v{model.version} - {quantMethod && {quantMethod}} + {quantizationName && ( + {quantizationName} + )} {`${bits} Bits`}
@@ -105,7 +107,7 @@ const ExploreModelItem = forwardRef(({ model }, ref) => { )}
diff --git a/web/screens/ExploreModels/ExploreModelItemHeader/index.tsx b/web/screens/ExploreModels/ExploreModelItemHeader/index.tsx index bf55aec61..fe3c9c3e9 100644 --- a/web/screens/ExploreModels/ExploreModelItemHeader/index.tsx +++ b/web/screens/ExploreModels/ExploreModelItemHeader/index.tsx @@ -35,8 +35,8 @@ const ExploreModelItemHeader: React.FC = ({ const { performanceTag, title, getPerformanceForModel } = useGetPerformanceTag() const downloadAtom = useMemo( - () => atom((get) => get(modelDownloadStateAtom)[suitableModel.id]), - [suitableModel.id] + () => atom((get) => get(modelDownloadStateAtom)[suitableModel.name]), + [suitableModel.name] ) const downloadState = useAtomValue(downloadAtom) const { setMainViewState } = useMainViewState() @@ -51,8 +51,9 @@ const ExploreModelItemHeader: React.FC = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [exploreModel, suitableModel]) + // TODO: Comparing between Model Id and Version Name? const isDownloaded = - downloadedModels.find((model) => model.id === suitableModel.id) != null + downloadedModels.find((model) => model.id === suitableModel.name) != null let downloadButton = (