diff --git a/.github/scripts/auto-sign.sh b/.github/scripts/auto-sign.sh index e1c735860..5e6ef9750 100755 --- a/.github/scripts/auto-sign.sh +++ b/.github/scripts/auto-sign.sh @@ -1,5 +1,10 @@ #!/bin/bash -APP_PATH=${APP_PATH} -DEVELOPER_ID=${DEVELOPER_ID} -find $APP_PATH \( -type f -perm +111 -o -name "*.node" \) -exec codesign -s "$DEVELOPER_ID" --options=runtime {} \; \ No newline at end of file +# Check if both APP_PATH and DEVELOPER_ID environment variables are set +if [[ -z "$APP_PATH" ]] || [[ -z "$DEVELOPER_ID" ]]; then + echo "Either APP_PATH or DEVELOPER_ID is not set. Skipping script execution." + exit 0 +fi + +# If both variables are set, execute the following commands +find "$APP_PATH" \( -type f -perm +111 -o -name "*.node" \) -exec codesign -s "$DEVELOPER_ID" --options=runtime {} \; diff --git a/.github/workflows/jan-electron-build.yml b/.github/workflows/jan-electron-build.yml index f1c0f5c1e..34005e068 100644 --- a/.github/workflows/jan-electron-build.yml +++ b/.github/workflows/jan-electron-build.yml @@ -60,14 +60,14 @@ jobs: run: | yarn build:core yarn install - yarn build:plugins-darwin + yarn build:plugins env: APP_PATH: "." DEVELOPER_ID: ${{ secrets.DEVELOPER_ID }} - name: Build and publish app run: | - yarn build:publish-darwin + yarn build:publish env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} CSC_LINK: "/tmp/codesign.p12" @@ -122,11 +122,11 @@ jobs: yarn build:core yarn install $env:NITRO_VERSION = Get-Content .\plugins\inference-plugin\nitro\version.txt; echo $env:NITRO_VERSION - yarn build:plugins-win32 + yarn build:plugins - name: Build and publish app run: | - yarn build:publish-win32 + yarn build:publish env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -178,11 +178,11 @@ jobs: yarn config set network-timeout 300000 yarn build:core yarn install - yarn build:plugins-linux + yarn build:plugins - name: Build and publish app run: | - yarn build:publish-linux + yarn build:publish env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/jan-electron-linter-and-test.yml b/.github/workflows/jan-electron-linter-and-test.yml index 1938a40f5..92f29c368 100644 --- a/.github/workflows/jan-electron-linter-and-test.yml +++ b/.github/workflows/jan-electron-linter-and-test.yml @@ -95,8 +95,8 @@ jobs: yarn build:core yarn install $env:NITRO_VERSION = Get-Content .\plugins\inference-plugin\nitro\version.txt; echo $env:NITRO_VERSION - yarn build:plugins-win32 - yarn build:test-win32 + yarn build:plugins + yarn build:test $env:CI="e2e" yarn test @@ -131,6 +131,6 @@ jobs: yarn config set network-timeout 300000 yarn build:core yarn install - yarn build:plugins-linux - yarn build:test-linux + yarn build:plugins + yarn build:test yarn test 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 b39a38408..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/fs.ts b/core/src/fs.ts index 04046705a..73ac31636 100644 --- a/core/src/fs.ts +++ b/core/src/fs.ts @@ -8,6 +8,21 @@ const writeFile: (path: string, data: string) => Promise = (path, data) => window.coreAPI?.writeFile(path, data) ?? window.electronAPI?.writeFile(path, data); +/** + * Gets the user space path. + * @returns {Promise} A Promise that resolves with the user space path. + */ +const getUserSpace = (): Promise => + window.coreAPI?.getUserSpace() ?? window.electronAPI?.getUserSpace(); + +/** + * Checks whether the path is a directory. + * @param path - The path to check. + * @returns {boolean} A boolean indicating whether the path is a directory. + */ +const isDirectory = (path: string): Promise => + window.coreAPI?.isDirectory(path) ?? window.electronAPI?.isDirectory(path); + /** * Reads the contents of a file at the specified path. * @param {string} path - The path of the file to read. @@ -48,6 +63,8 @@ const deleteFile: (path: string) => Promise = (path) => window.coreAPI?.deleteFile(path) ?? window.electronAPI?.deleteFile(path); export const fs = { + isDirectory, + getUserSpace, writeFile, readFile, listFiles, diff --git a/core/src/index.ts b/core/src/index.ts index 39a69d702..76827f6b3 100644 --- a/core/src/index.ts +++ b/core/src/index.ts @@ -20,12 +20,9 @@ export { events } from "./events"; * Events types exports. * @module */ -export { - EventName, - NewMessageRequest, - NewMessageResponse, - MessageHistory, -} from "./events"; +export * from "./events"; + +export * from "./types/index"; /** * Filesystem module exports. @@ -37,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 c2062b5d1..dd227081a 100644 --- a/core/src/types/index.ts +++ b/core/src/types/index.ts @@ -1,91 +1,183 @@ -export interface Conversation { - _id: string; - modelId?: string; - botId?: string; - name: string; - message?: string; - summary?: string; - createdAt?: string; - updatedAt?: string; - messages: Message[]; -} -export interface Message { - message?: string; - user?: string; - _id: string; - createdAt?: string; - updatedAt?: string; +/** + * Message Request and Response + * ============================ + * */ + +/** + * The role of the author of this message. + * @data_transfer_object + */ +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", +} +/** + * 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. - * Being used as file name. MUST be unique. - */ - _id: string; + /** Combination of owner and model name.*/ + id: string; + /** The name of the model.*/ name: string; - quantMethod: 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; - modelFile?: string; - /** - * For tracking download info - */ - startDownloadAt?: number; - finishDownloadAt?: number; - productId: string; - productName: 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; - createdAt: number; - updatedAt?: number; - status: 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 { - _id: string; + /** The unique id of the model.*/ + id: string; + /** The name of the model.*/ name: string; - shortDescription: 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; - createdAt: number; - updatedAt?: number; - status: 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 stored in the database + * Model type which will be present a version of ModelCatalog + * @data_transfer_object */ export type ModelVersion = { - /** - * Combination of owner and model name. - * Being used as file name. Should be unique. - */ - _id: string; + /** The name of this model version.*/ name: string; - quantMethod: 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; - productId: string; - /** - * For tracking download state - */ - startDownloadAt?: number; - finishDownloadAt?: number; }; diff --git a/docs/docs/about/about.md b/docs/docs/about/about.md index 7931c030f..3ed5750c4 100644 --- a/docs/docs/about/about.md +++ b/docs/docs/about/about.md @@ -2,11 +2,11 @@ title: About Jan --- -Jan is a free, open source alternative to OpenAI's platform 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. We believe in the need for an open source AI ecosystem, and are building the infra and tooling to allow open source AIs to be as usable and comprehensive as proprietary ones. -Jan's long-term vision is to build a cognitive framework for future robots. We build towards a future where humans and businesses are augmented by practical, useful assistants in everyday life. +Jan's long-term vision is to build a cognitive framework for future robots, who are practical, useful assistants for humans and businesses in everyday life. ## Why does Jan Exist? diff --git a/docs/docs/docs/modules/01_chats.md b/docs/docs/docs/modules/01_chats.md deleted file mode 100644 index 58047c4c8..000000000 --- a/docs/docs/docs/modules/01_chats.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -title: "Chats" ---- - -Chats are essentially inference requests to a model - -> OpenAI Equivalent: https://platform.openai.com/docs/api-reference/chat - -## Chat Object - -- Equivalent to: https://platform.openai.com/docs/api-reference/chat/object - -## Chat API - -See [/chat](/api/chat) - -- Equivalent to: https://platform.openai.com/docs/api-reference/chat - -```sh -POST https://localhost:1337/v1/chat/completions - -TODO: -# Figure out how to incorporate tools -``` - -## Chat Filesystem - -- Chats will be persisted to `messages` within `threads` -- There is no data structure specific to Chats diff --git a/docs/docs/docs/modules/02_models.md b/docs/docs/docs/modules/02_models.md deleted file mode 100644 index 29b45f6a0..000000000 --- a/docs/docs/docs/modules/02_models.md +++ /dev/null @@ -1,81 +0,0 @@ ---- -title: "Models" ---- - -Models are AI models like Llama and Mistral - -> OpenAI Equivalent: https://platform.openai.com/docs/api-reference/models - -## Model Object - -- `model.json` - -> Equivalent to: https://platform.openai.com/docs/api-reference/models/object - -```json -{ - // OpenAI model compatibility - // https://platform.openai.com/docs/api-reference/models) - "id": "llama-2-uuid", - "object": "model", - "created": 1686935002, - "owned_by": "you" - - // Model settings (benchmark: Ollama) - // https://github.com/jmorganca/ollama/blob/main/docs/modelfile.md#template - "model_name": "llama2", - "model_path": "ROOT/models/...", - "parameters": { - "temperature": "..", - "token-limit": "..", - "top-k": "..", - "top-p": ".." - }, - "template": "This is a full prompt template", - "system": "This is a system prompt", - - // Model metadata (benchmark: HuggingFace) - "version": "...", - "author": "...", - "tags": "...", - ... -} -``` - -## Model API - -See [/model](/api/model) - -- Equivalent to: https://platform.openai.com/docs/api-reference/models - -```sh -GET https://localhost:1337/v1/models # List models -GET https://localhost:1337/v1/models/{model} # Get model object -DELETE https://localhost:1337/v1/models/{model} # Delete model - -TODO: -# Start model -# Stop model -``` - -## Model Filesystem - -How `models` map onto your local filesystem - -```sh -/janroot - /models - /modelA - model.json # Default model params - modelA.gguf - modelA.bin - /modelB/* - model.json - modelB.gguf - /assistants - model.json # Defines model, default: looks in `/models` - /models # Optional /models folder that overrides root - /modelA - model.json - modelA.bin -``` diff --git a/docs/docs/docs/modules/03_assistants.md b/docs/docs/docs/modules/03_assistants.md deleted file mode 100644 index 1d326fd2e..000000000 --- a/docs/docs/docs/modules/03_assistants.md +++ /dev/null @@ -1,80 +0,0 @@ ---- -title: "Assistants" ---- - -Assistants can use models and tools. - -- Jan's `Assistants` are even more powerful than OpenAI due to customizable code in `index.js` - -> OpenAI Equivalent: https://platform.openai.com/docs/api-reference/assistants - -## Assistant Object - -- `assistant.json` -- Equivalent to: https://platform.openai.com/docs/api-reference/assistants/object - -```json -{ - // Jan specific properties - "avatar": "https://lala.png" - "thread_location": "ROOT/threads" // Default to root (optional field) - // TODO: add moar - - // 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": {} -} -``` - -## Assistants API - -- _TODO_: What would modifying Assistant do? (doesn't mutate `index.js`?) - -```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 -``` - -## Assistants Filesystem - -```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 -``` diff --git a/docs/docs/docs/modules/04_threads.md b/docs/docs/docs/modules/04_threads.md deleted file mode 100644 index f36f923ed..000000000 --- a/docs/docs/docs/modules/04_threads.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -title: "Threads" ---- - -Threads contain `messages` history with assistants. Messages in a thread share context. - -- 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) - -> OpenAI Equivalent: https://platform.openai.com/docs/api-reference/threads - -## Thread Object - -- `thread.json` -- Equivalent to: https://platform.openai.com/docs/api-reference/threads/object - -```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": {} -} -``` - -## Threads API - -- 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 - -```sh -/assistants - /homework-helper - /threads # context is "permanently remembered" by assistant in future conversations -/threads # context is only retained within a single thread -``` diff --git a/docs/docs/docs/modules/05_messages.md b/docs/docs/docs/modules/05_messages.md deleted file mode 100644 index 8bc79d1ae..000000000 --- a/docs/docs/docs/modules/05_messages.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -title: "Messages" ---- - -Messages are within `threads` and capture additional metadata. - -- Equivalent to: https://platform.openai.com/docs/api-reference/messages - -## Message Object - -- Equivalent to: https://platform.openai.com/docs/api-reference/messages/object - -```json -{ - // Jan specific properties - "updatedAt": "..." // that's it I think - - // OpenAI compatible properties: https://platform.openai.com/docs/api-reference/messages) - "id": "msg_dKYDWyQvtjDBi3tudL1yWKDa", - "object": "thread.message", - "created_at": 1698983503, - "thread_id": "thread_RGUhOuO9b2nrktrmsQ2uSR6I", - "role": "assistant", - "content": [ - { - "type": "text", - "text": { - "value": "Hi! How can I help you today?", - "annotations": [] - } - } - ], - "file_ids": [], - "assistant_id": "asst_ToSF7Gb04YMj8AMMm50ZLLtY", - "run_id": "run_BjylUJgDqYK9bOhy4yjAiMrn", - "metadata": {} -} -``` - -## Messages API - -- Equivalent to: https://platform.openai.com/docs/api-reference/messages - -```sh -POST https://api.openai.com/v1/threads/{thread_id}/messages # create msg -GET https://api.openai.com/v1/threads/{thread_id}/messages # list messages -GET https://api.openai.com/v1/threads/{thread_id}/messages/{message_id} - -# Get message file -GET https://api.openai.com/v1/threads/{thread_id}/messages/{message_id}/files/{file_id} -# List message files -GET https://api.openai.com/v1/threads/{thread_id}/messages/{message_id}/files -``` diff --git a/docs/docs/docs/specs/architecture.md b/docs/docs/docs/specs/architecture.md new file mode 100644 index 000000000..cd20109f9 --- /dev/null +++ b/docs/docs/docs/specs/architecture.md @@ -0,0 +1,40 @@ +--- +title: "Architecture and Concepts" +--- + +## Concepts +```mermaid +graph LR + A1[("A User Integrators")] -->|uses| B1[assistant] + B1 -->|persist conversational history| C1[("thread A")] + B1 -->|executes| D1[("built-in tools as module")] + B1 -.->|uses| E1[model] + E1 -.->|model.json| D1 + D1 --> F1[retrieval] + F1 -->|belongs to| G1[("web browsing")] + G1 --> H1[Google] + G1 --> H2[Duckduckgo] + F1 -->|belongs to| I1[("API calling")] + F1 --> J1[("knowledge files")] +``` +- User/ Integrator +- Assistant object +- Model object +- Thread object +- Built-in tool object + +## File system +```sh +janroot/ + assistants/ + assistant-a/ + assistant.json + src/ + index.ts + threads/ + thread-a/ + thread-b + models/ + model-a/ + model.json +``` \ No newline at end of file diff --git a/docs/docs/docs/specs/assistants.md b/docs/docs/docs/specs/assistants.md new file mode 100644 index 000000000..a744d16bd --- /dev/null +++ b/docs/docs/docs/specs/assistants.md @@ -0,0 +1,240 @@ +--- +title: "Assistants" +--- + +Assistants can use models and tools. +> OpenAI Equivalent: https://platform.openai.com/docs/api-reference/assistants +- Jan's `Assistants` are even more powerful than OpenAI due to customizable code in `index.js` + +## User Stories + +_Users can download an assistant via a web URL_ + +- Wireframes here + +_Users can import an assistant from local directory_ + +- Wireframes here + +_Users can configure assistant settings_ + +- Wireframes here + +## Assistant Object + +- `assistant.json` +> OpenAI Equivalen: https://platform.openai.com/docs/api-reference/assistants/object + +```json +{ + // Jan specific properties + "avatar": "https://lala.png", + "thread_location": "ROOT/threads", // Default to root (optional field) + // TODO: add moar + + // OpenAI compatible properties: https://platform.openai.com/docs/api-reference/assistants + "id": "asst_abc123", + "object": "assistant", + "created_at": 1698984975, + "name": "Math Tutor", + "description": null, + "instructions": "...", + "tools": [ + { + "type": "retrieval" + }, + { + "type": "web_browsing" + } + ], + "file_ids": ["file_id"], + "models": [""], + "metadata": {} +} +``` + +### Assistant lifecycle +Assistant has 4 states (enum) +- `to_download` +- `downloading` +- `ready` +- `running` + +## Assistants API + +- What would modifying Assistant do? (doesn't mutate `index.js`?) + - By default, `index.js` loads `assistant.json` file and executes exactly like so. This supports builders with little time to write code. + - The `assistant.json` is 1 source of truth for the definitions of `models` and `built-in tools` that they can use it without writing more code. + +### Get list assistants +> OpenAI Equivalent: https://platform.openai.com/docs/api-reference/assistants/listAssistants +- Example request +```shell + curl {JAN_URL}/v1/assistants?order=desc&limit=20 \ + -H "Content-Type: application/json" +``` +- Example response +```json +{ + "object": "list", + "data": [ + { + "id": "asst_abc123", + "object": "assistant", + "created_at": 1698982736, + "name": "Coding Tutor", + "description": null, + "models": ["model_zephyr_7b", "azure-openai-gpt4-turbo"], + "instructions": "You are a helpful assistant designed to make me better at coding!", + "tools": [], + "file_ids": [], + "metadata": {}, + "state": "ready" + }, + ], + "first_id": "asst_abc123", + "last_id": "asst_abc789", + "has_more": false +} +``` + +### Get assistant +> OpenAI Equivalent: https://platform.openai.com/docs/api-reference/assistants/getAssistant +- Example request +```shell + curl {JAN_URL}/v1/assistants/{assistant_id} \ + -H "Content-Type: application/json" +``` +- Example response +```json +{ + "id": "asst_abc123", + "object": "assistant", + "created_at": 1699009709, + "name": "HR Helper", + "description": null, + "models": ["model_zephyr_7b", "azure-openai-gpt4-turbo"], + "instructions": "You are an HR bot, and you have access to files to answer employee questions about company policies.", + "tools": [ + { + "type": "retrieval" + } + ], + "file_ids": [ + "file-abc123" + ], + "metadata": {}, + "state": "ready" +} +``` + +### Create an assistant +Create an assistant with models and instructions. +> OpenAI Equivalent: https://platform.openai.com/docs/api-reference/assistants/createAssistant +- Example request +```shell + curl -X POST {JAN_URL}/v1/assistants \ + -H "Content-Type: application/json" \ + -d { + "instructions": "You are a personal math tutor. When asked a question, write and run Python code to answer the question.", + "name": "Math Tutor", + "tools": [{"type": "retrieval"}], + "model": ["model_zephyr_7b", "azure-openai-gpt4-turbo"] + } +``` +- Example response +```json +{ + "id": "asst_abc123", + "object": "assistant", + "created_at": 1698984975, + "name": "Math Tutor", + "description": null, + "model": ["model_zephyr_7b", "azure-openai-gpt4-turbo"] + "instructions": "You are a personal math tutor. When asked a question, write and run Python code to answer the question.", + "tools": [ + { + "type": "retrieval" + } + ], + "file_ids": [], + "metadata": {}, + "state": "ready" +} +``` +### Modify an assistant +> OpenAI Equivalent: https://platform.openai.com/docs/api-reference/assistants/modifyAssistant +- Example request +```shell + curl -X POST {JAN_URL}/v1/assistants/{assistant_id} \ + -H "Content-Type: application/json" \ + -d { + "instructions": "You are a personal math tutor. When asked a question, write and run Python code to answer the question.", + "name": "Math Tutor", + "tools": [{"type": "retrieval"}], + "model": ["model_zephyr_7b", "azure-openai-gpt4-turbo"] + } +``` +- Example response +```json +{ + "id": "asst_abc123", + "object": "assistant", + "created_at": 1698984975, + "name": "Math Tutor", + "description": null, + "model": ["model_zephyr_7b", "azure-openai-gpt4-turbo"] + "instructions": "You are a personal math tutor. When asked a question, write and run Python code to answer the question.", + "tools": [ + { + "type": "retrieval" + } + ], + "file_ids": [], + "metadata": {}, + "state": "ready" +} +``` +### Delete Assistant +> OpenAI Equivalent: https://platform.openai.com/docs/api-reference/assistants/deleteAssistant +`- Example request +```shell +curl -X DELETE {JAN_URL}/v1/assistant/model-zephyr-7B +``` +- Example response +```json +{ + "id": "asst_abc123", + "object": "assistant.deleted", + "deleted": true, + "state": "to_download" +} +``` + +## Assistants Filesystem + +```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 + package.json + /src + index.js + process.js + + /threads # Assistants remember conversations in the future + /models # Users can upload custom models + /finetuned-model +``` diff --git a/docs/docs/docs/specs/chats.md b/docs/docs/docs/specs/chats.md new file mode 100644 index 000000000..01d58b584 --- /dev/null +++ b/docs/docs/docs/specs/chats.md @@ -0,0 +1,16 @@ +--- +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 + +- This should reference Nitro ChatCompletion API page to reduce duplication. +- We are fine with adding Jan API for this but it makes sense to use Nitro as reference as Nitro is default inference engine for Jan in this release \ No newline at end of file diff --git a/docs/docs/docs/modules/06_files.md b/docs/docs/docs/specs/files.md similarity index 63% rename from docs/docs/docs/modules/06_files.md rename to docs/docs/docs/specs/files.md index 70c3e345f..0775f779d 100644 --- a/docs/docs/docs/modules/06_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 @@ -25,6 +31,20 @@ Files can be used by `threads`, `assistants` and `fine-tuning` ``` ## File API +### List Files +> OpenAI Equivalent: https://platform.openai.com/docs/api-reference/files/list + +### Upload file +> OpenAI Equivalent: https://platform.openai.com/docs/api-reference/files/create + +### Delete file +> OpenAI Equivalent: https://platform.openai.com/docs/api-reference/files/delete + +### Retrieve file +> OpenAI Equivalent: https://platform.openai.com/docs/api-reference/files/retrieve + +### Retrieve file content +> OpenAI Equivalent: https://platform.openai.com/docs/api-reference/files/retrieve-contents ## Files Filesystem @@ -39,5 +59,4 @@ Files can be used by `threads`, `assistants` and `fine-tuning` /threads /jan-12938912 /files # thread-specific files - ``` diff --git a/docs/docs/docs/specs/fine-tuning.md b/docs/docs/docs/specs/fine-tuning.md new file mode 100644 index 000000000..df6723ff1 --- /dev/null +++ b/docs/docs/docs/specs/fine-tuning.md @@ -0,0 +1,4 @@ +--- +title: "Fine tuning" +--- +Todo: @hiro \ No newline at end of file diff --git a/docs/docs/docs/specs/messages.md b/docs/docs/docs/specs/messages.md new file mode 100644 index 000000000..74cf5bf90 --- /dev/null +++ b/docs/docs/docs/specs/messages.md @@ -0,0 +1,178 @@ +--- +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. +> OpenAI Equivalent: https://platform.openai.com/docs/api-reference/messages + +## Message Object +> OpenAI Equivalent: https://platform.openai.com/docs/api-reference/messages/object +```json +{ + // Jan specific properties + "updatedAt": "...", // that's it I think + + // OpenAI compatible properties: https://platform.openai.com/docs/api-reference/messages) + "id": "msg_dKYDWyQvtjDBi3tudL1yWKDa", + "object": "thread.message", + "created_at": 1698983503, + "thread_id": "thread_RGUhOuO9b2nrktrmsQ2uSR6I", + "role": "assistant", + "content": [ + { + "type": "text", + "text": { + "value": "Hi! How can I help you today?", + "annotations": [] + } + } + ], + "file_ids": [], + "assistant_id": "asst_ToSF7Gb04YMj8AMMm50ZLLtY", + "run_id": "run_BjylUJgDqYK9bOhy4yjAiMrn", + "metadata": {} +} +``` + +## Messages API +> OpenAI Equivalent: https://platform.openai.com/docs/api-reference/messages + +### Get list message +> OpenAI Equivalent: https://platform.openai.com/docs/api-reference/messages/getMessage +- Example request +```shell + curl {JAN_URL}/v1/threads/{thread_id}/messages/{message_id} \ + -H "Content-Type: application/json" +``` +- Example response +```json +{ + "id": "msg_abc123", + "object": "thread.message", + "created_at": 1699017614, + "thread_id": "thread_abc123", + "role": "user", + "content": [ + { + "type": "text", + "text": { + "value": "How does AI work? Explain it in simple terms.", + "annotations": [] + } + } + ], + "file_ids": [], + "assistant_id": null, + "run_id": null, + "metadata": {} +} +``` +### Create message +> OpenAI Equivalent: https://platform.openai.com/docs/api-reference/messages/createMessage +- Example request +```shell + curl -X POST {JAN_URL}/v1/threads/{thread_id}/messages \ + -H "Content-Type: application/json" \ + -d '{ + "role": "user", + "content": "How does AI work? Explain it in simple terms." + }' +``` +- Example response +```json + { + "id": "msg_abc123", + "object": "thread.message", + "created_at": 1699017614, + "thread_id": "thread_abc123", + "role": "user", + "content": [ + { + "type": "text", + "text": { + "value": "How does AI work? Explain it in simple terms.", + "annotations": [] + } + } + ], + "file_ids": [], + "assistant_id": null, + "run_id": null, + "metadata": {} + } +``` +### Get message +> OpenAI Equivalent: https://platform.openai.com/docs/api-reference/assistants/listAssistants +- Example request +```shell + curl {JAN_URL}/v1/threads/{thread_id}/messages/{message_id} \ + -H "Content-Type: application/json" +``` +- Example response +```json + { + "id": "msg_abc123", + "object": "thread.message", + "created_at": 1699017614, + "thread_id": "thread_abc123", + "role": "user", + "content": [ + { + "type": "text", + "text": { + "value": "How does AI work? Explain it in simple terms.", + "annotations": [] + } + } + ], + "file_ids": [], + "assistant_id": null, + "run_id": null, + "metadata": {} + } +``` + +### Modify message +> Jan: TODO: Do we need to modify message? Or let user create new message? + +# Get message file +> OpenAI Equivalent: https://api.openai.com/v1/threads/{thread_id}/messages/{message_id}/files/{file_id} +- Example request +```shell + curl {JAN_URL}/v1/threads/{thread_id}/messages/{message_id}/files/{file_id} \ + -H "Content-Type: application/json" +``` +- Example response +```json + { + "id": "file-abc123", + "object": "thread.message.file", + "created_at": 1699061776, + "message_id": "msg_abc123" + } +``` +# List message files +> OpenAI Equivalent: https://api.openai.com/v1/threads/{thread_id}/messages/{message_id}/files +``` +- Example request +```shell + curl {JAN_URL}/v1/threads/{thread_id}/messages/{message_id}/files/{file_id} \ + -H "Content-Type: application/json" +``` +- Example response +```json + { + "id": "file-abc123", + "object": "thread.message.file", + "created_at": 1699061776, + "message_id": "msg_abc123" + } +``` \ No newline at end of file diff --git a/docs/docs/docs/specs/models.md b/docs/docs/docs/specs/models.md new file mode 100644 index 000000000..a5951fab0 --- /dev/null +++ b/docs/docs/docs/specs/models.md @@ -0,0 +1,372 @@ +--- +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 + +## User Stories + +_Users can download a model via a web URL_ + +- Wireframes here + +_Users can import a model from local directory_ + +- Wireframes here + +_Users can configure model settings, like run parameters_ + +- Wireframes here + +_Users can override run settings at runtime_ + +- See Assistant Spec and Thread + +## Jan Model Object + +- A `Jan Model Object` is a “representation" of a model +- Objects are defined by `model-name.json` files in `json` format +- Objects are identified by `folder-name/model-name`, where its `id` is indicative of its file location. +- Objects are designed to be compatible with `OpenAI Model Objects`, with additional properties needed to run on our infrastructure. +- ALL object properties are optional, i.e. users should be able to run a model declared by an empty `json` file. + +| Property | Type | Description | Validation | +| ----------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------ | +| `source_url` | string | The model download source. It can be an external url or a local filepath. | Defaults to `pwd`. See [Source_url](#Source_url) | +| `object` | enum: `model`, `assistant`, `thread`, `message` | Type of the Jan Object. Always `model` | Defaults to "model" | +| `name` | string | A vanity name | Defaults to filename | +| `description` | string | A vanity description of the model | Defaults to "" | +| `state` | enum[`to_download` , `downloading`, `ready` , `running`] | Needs more thought | Defaults to `to_download` | +| `parameters` | map | Defines default model run parameters used by any assistant. | Defaults to `{}` | +| `metadata` | map | Stores additional structured information about the model. | Defaults to `{}` | +| `metadata.engine` | enum: `llamacpp`, `api`, `tensorrt` | The model backend used to run model. | Defaults to "llamacpp" | +| `metadata.quantization` | string | Supported formats only | See [Custom importers](#Custom-importers) | +| `metadata.binaries` | array | Supported formats only. | See [Custom importers](#Custom-importers) | + +### Source_url + +- Users can download models from a `remote` source or reference an existing `local` model. +- If this property is not specified in the Model Object file, then the default behavior is to look in the current directory. + +#### Local source_url + +- Users can import a local model by providing the filepath to the model + +```json +// ./models/llama2/llama2-7bn-gguf.json +"source_url": "~/Downloads/llama-2-7bn-q5-k-l.gguf", + +// Default, if property is omitted +"source_url": "./", +``` + +#### Remote source_url + +- Users can download a model by remote URL. +- Supported url formats: + - `https://huggingface.co/TheBloke/Llama-2-7B-Chat-GGUF/blob/main/llama-2-7b-chat.Q3_K_L.gguf` + - `https://any-source.com/.../model-binary.bin` + +#### Custom importers + +Additionally, Jan supports importing popular formats. For example, if you provide a HuggingFace URL for a `TheBloke` model, Jan automatically downloads and catalogs all quantizations. Custom importers autofills properties like `metadata.quantization` and `metadata.size`. + +Supported URL formats with custom importers: + +- `huggingface/thebloke`: [Link](https://huggingface.co/TheBloke/Llama-2-7B-GGUF) +- `huggingface/thebloke`: [Link](https://huggingface.co/TheBloke/Llama-2-7B-GGUF) +- `janhq`: `TODO: put URL here` +- `azure_openai`: `https://docs-test-001.openai.azure.com/openai.azure.com/docs-test-001/gpt4-turbo` +- `openai`: `api.openai.com` + +### Generic Example + +- Model has 1 binary `model-zephyr-7B.json` +- See [source](https://huggingface.co/TheBloke/zephyr-7B-beta-GGUF/) + +```json +// ./models/zephr/zephyr-7b-beta-Q4_K_M.json +// Note: Default fields omitted for brevity +"source_url": "https://huggingface.co/TheBloke/zephyr-7B-beta-GGUF/blob/main/zephyr-7b-beta.Q4_K_M.gguf", +"parameters": { + "init": { + "ctx_len": "2048", + "ngl": "100", + "embedding": "true", + "n_parallel": "4", + "pre_prompt": "A chat between a curious user and an artificial intelligence", + "user_prompt": "USER: ", + "ai_prompt": "ASSISTANT: " + }, + "runtime": { + "temperature": "0.7", + "token_limit": "2048", + "top_k": "0", + "top_p": "1", + "stream": "true" + } +}, +"metadata": { + "engine": "llamacpp", + "quantization": "Q3_K_L", + "size": "7B", +} +``` + +### Example: multiple binaries + +- Model has multiple binaries `model-llava-1.5-ggml.json` +- See [source](https://huggingface.co/mys/ggml_llava-v1.5-13b) + +```json +"source_url": "https://huggingface.co/mys/ggml_llava-v1.5-13b", +"parameters": {"init": {}, "runtime": {}} +"metadata": { + "mmproj_binary": "https://huggingface.co/mys/ggml_llava-v1.5-13b/blob/main/mmproj-model-f16.gguf", + "ggml_binary": "https://huggingface.co/mys/ggml_llava-v1.5-13b/blob/main/ggml-model-q5_k.gguf", + "engine": "llamacpp", + "quantization": "Q5_K" +} +``` + +### Example: Azure API + +- Using a remote API to access model `model-azure-openai-gpt4-turbo.json` +- See [source](https://learn.microsoft.com/en-us/azure/ai-services/openai/quickstart?tabs=command-line%2Cpython&pivots=rest-api) + +```json +"source_url": "https://docs-test-001.openai.azure.com/openai.azure.com/docs-test-001/gpt4-turbo", +"parameters": { + "init" { + "API-KEY": "", + "DEPLOYMENT-NAME": "", + "api-version": "2023-05-15" + }, + "runtime": { + "temperature": "0.7", + "max_tokens": "2048", + "presence_penalty": "0", + "top_p": "1", + "stream": "true" + } +} +"metadata": { + "engine": "api", +} +``` + +## Filesystem + +- Everything needed to represent a `model` is packaged into an `Model folder`. +- The `folder` is standalone and can be easily zipped, imported, and exported, e.g. to Github. +- The `folder` always contains at least one `Model Object`, declared in a `json` format. +- The `folder` and `file` do not have to share the same name +- The model `id` is made up of `folder_name/filename` and is thus always unique. + +```sh +/janroot + /models + azure-openai/ # Folder name + azure-openai-gpt3-5.json # File name + + llama2-70b/ + model.json + .gguf +``` + +### Default ./model folder +- Jan ships with a default model folders containing recommended models +- Only the Model Object `json` files are included +- Users must later explicitly download the model binaries +```sh +models/ + mistral-7b/ + mistral-7b.json + hermes-7b/ + hermes-7b.json +``` +### Multiple quantizations + +- Each quantization has its own `Jan Model Object` file + +```sh +llama2-7b-gguf/ + llama2-7b-gguf-Q2.json + llama2-7b-gguf-Q3_K_L.json + .bin +``` +### Multiple model partitions + +- A Model that is partitioned into several binaries use just 1 file + +```sh +llava-ggml/ + llava-ggml-Q5.json + .proj + ggml +``` +### Your locally fine-tuned model + +- ?? + +```sh +llama-70b-finetune/ + llama-70b-finetune-q5.json + .bin +``` +## Jan API +### Model API Object +- The `Jan Model Object` maps into the `OpenAI Model Object`. +- Properties marked with `*` are compatible with the [OpenAI `model` object](https://platform.openai.com/docs/api-reference/models) +- Note: The `Jan Model Object` has additional properties when retrieved via its API endpoint. +> OpenAI Equivalent: https://platform.openai.com/docs/api-reference/models/object + +### Model lifecycle +Model has 4 states (enum) +- `to_download` +- `downloading` +- `ready` +- `running` + +### Get Model +> OpenAI Equivalent: https://platform.openai.com/docs/api-reference/models/retrieve +- Example request +```shell +curl {JAN_URL}/v1/models/{model_id} +``` +- Example response +```json +{ + "id": "model-zephyr-7B", + "object": "model", + "created_at": 1686935002, + "owned_by": "thebloke", + "state": "running", + "source_url": "https://huggingface.co/TheBloke/zephyr-7B-beta-GGUF/blob/main/zephyr-7b-beta.Q4_K_M.gguf", + "parameters": { + "ctx_len": 2048, + "ngl": 100, + "embedding": true, + "n_parallel": 4, + "pre_prompt": "A chat between a curious user and an artificial intelligence", + "user_prompt": "USER: ", + "ai_prompt": "ASSISTANT: ", + "temperature": "0.7", + "token_limit": "2048", + "top_k": "0", + "top_p": "1", + }, + "metadata": { + "engine": "llamacpp", + "quantization": "Q3_K_L", + "size": "7B", + } +} +``` +### List models +Lists the currently available models, and provides basic information about each one such as the owner and availability. +> OpenAI Equivalent: https://platform.openai.com/docs/api-reference/models/list +- Example request +```shell= +curl {JAN_URL}/v1/models +``` +- Example response +```json +{ + "object": "list", + "data": [ + { + "id": "model-zephyr-7B", + "object": "model", + "created_at": 1686935002, + "owned_by": "thebloke", + "state": "running" + }, + { + "id": "ft-llama-70b-gguf", + "object": "model", + "created_at": 1686935002, + "owned_by": "you", + "state": "stopped" + }, + { + "id": "model-azure-openai-gpt4-turbo", + "object": "model", + "created_at": 1686935002, + "owned_by": "azure_openai", + "state": "running" + }, + ], + "object": "list" +} +``` +### Delete Model +> OpenAI Equivalent: https://platform.openai.com/docs/api-reference/models/delete +`- Example request +```shell +curl -X DELETE {JAN_URL}/v1/models/{model_id} +``` +- Example response +```json +{ + "id": "model-zephyr-7B", + "object": "model", + "deleted": true, + "state": "to_download" +} +``` +### Start Model +> Jan-only endpoint +The request to start `model` by changing model state from `ready` to `running` +- Example request +```shell +curl -X PUT {JAN_URL}/v1/models{model_id}/start +``` +- Example response +```json +{ + "id": "model-zephyr-7B", + "object": "model", + "state": "running" +} +``` +### Stop Model +> Jan-only endpoint +The request to start `model` by changing model state from `running` to `ready` +- Example request +```shell +curl -X PUT {JAN_URL}/v1/models/{model_id}/stop +``` +- Example response +```json +{ + "id": "model-zephyr-7B", + "object": "model", + "state": "ready" +} +``` +### Download Model +> Jan-only endpoint +The request to download `model` by changing model state from `to_download` to `downloading` then `ready`once it's done. +- Example request +```shell +curl -X POST {JAN_URL}/v1/models/ +``` +- Example response +```json +{ + "id": "model-zephyr-7B", + "object": "model", + "state": "downloading" +} +``` \ No newline at end of file diff --git a/docs/docs/docs/specs/threads.md b/docs/docs/docs/specs/threads.md new file mode 100644 index 000000000..09521ff1a --- /dev/null +++ b/docs/docs/docs/specs/threads.md @@ -0,0 +1,193 @@ +--- +title: "Threads" +--- + +:::warning + +Draft Specification: functionality has not been implemented yet. + +Feedback: [HackMD: Threads Spec](https://hackmd.io/BM_8o_OCQ-iLCYhunn2Aug) + +::: + +## User Stories + +_Users can chat with an assistant in a thread_ + +- See [Messages Spec](./messages.md) + +_Users can change assistant and 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` | +| `assistants` | array | An array of Jan Assistant Objects. Threads can "override" an assistant's parameters. Thread-level model parameters are directly saved in the `thread.models` property! (see Models spec) | Defaults to `assistant.name` | +| `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 +// janroot/threads/jan_1700123404.json +"assistants": ["assistant-123"], +"messages": [ + {...message0}, {...message1} +], +"metadata": { + "summary": "funny physics joke", +}, +``` + +## Filesystem + +- `Jan Thread Objects`'s `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 +janroot/ + threads/ + jan_1700123404.json + homework_helper_700120003.json +``` + +## Jan API +### Get thread +> OpenAI Equivalent: https://platform.openai.com/docs/api-reference/threads/getThread +- Example request +```shell + curl {JAN_URL}/v1/threads/{thread_id} +``` +- Example response +```json + { + "id": "thread_abc123", + "object": "thread", + "created_at": 1699014083, + "assistants": ["assistant-001"], + "metadata": {}, + "messages": [] + } +``` +### Create Thread +> OpenAI Equivalent: https://platform.openai.com/docs/api-reference/threads/createThread +- Example request +```shell + curl -X POST {JAN_URL}/v1/threads \ + -H "Content-Type: application/json" \ + -d '{ + "messages": [{ + "role": "user", + "content": "Hello, what is AI?", + "file_ids": ["file-abc123"] + }, { + "role": "user", + "content": "How does AI work? Explain it in simple terms." + }] + }' +``` +- Example response +```json + { + "id": 'thread_abc123', + "object": 'thread', + "created_at": 1699014083, + "metadata": {} + } +``` +### Modify Thread +> OpenAI Equivalent: https://platform.openai.com/docs/api-reference/threads/modifyThread +- Example request +```shell + curl -X POST {JAN_URL}/v1/threads/{thread_id} \ + -H "Content-Type: application/json" \ + -d '{ + "messages": [{ + "role": "user", + "content": "Hello, what is AI?", + "file_ids": ["file-abc123"] + }, { + "role": "user", + "content": "How does AI work? Explain it in simple terms." + }] + }' +``` +- Example response +```json + { + "id": 'thread_abc123', + "object": 'thread', + "created_at": 1699014083, + "metadata": {} + } +``` + +- https://platform.openai.com/docs/api-reference/threads/modifyThread + +### Delete Thread +> OpenAI Equivalent: https://platform.openai.com/docs/api-reference/threads/deleteThread +- Example request +```shell + curl -X DELETE {JAN_URL}/v1/threads/{thread_id} +``` +- Example response +```json + { + "id": "thread_abc123", + "object": "thread.deleted", + "deleted": true + } +``` + +### List Threads +> This is a Jan-only endpoint, not supported by OAI yet. +- Example request +```shell + curl {JAN_URL}/v1/threads \ + -H "Content-Type: application/json" \ +``` +- Example response +```json + [ + { + "id": "thread_abc123", + "object": "thread", + "created_at": 1699014083, + "assistants": ["assistant-001"], + "metadata": {}, + "messages": [] + }, + { + "id": "thread_abc456", + "object": "thread", + "created_at": 1699014083, + "assistants": ["assistant-002", "assistant-002"], + "metadata": {}, + } + ] +``` + +### Get & Modify `Thread.Assistants` +-> Can achieve this goal by calling `Modify Thread` API + +#### `GET v1/threads/{thread_id}/assistants` +-> Can achieve this goal by calling `Get Thread` API + +#### `POST v1/threads/{thread_id}/assistants/{assistant_id}` +-> Can achieve this goal by calling `Modify Assistant` API with `thread.assistant[]` + +### List `Thread.Messages` +-> Can achieve this goal by calling `Get Thread` API diff --git a/docs/docs/guides/img/chat.png b/docs/docs/getting-started/img/chat.png similarity index 100% rename from docs/docs/guides/img/chat.png rename to docs/docs/getting-started/img/chat.png diff --git a/docs/docs/guides/img/downloading.png b/docs/docs/getting-started/img/downloading.png similarity index 100% rename from docs/docs/guides/img/downloading.png rename to docs/docs/getting-started/img/downloading.png diff --git a/docs/docs/guides/img/explore-model.png b/docs/docs/getting-started/img/explore-model.png similarity index 100% rename from docs/docs/guides/img/explore-model.png rename to docs/docs/getting-started/img/explore-model.png diff --git a/docs/docs/guides/img/jan-download.png b/docs/docs/getting-started/img/jan-download.png similarity index 100% rename from docs/docs/guides/img/jan-download.png rename to docs/docs/getting-started/img/jan-download.png diff --git a/docs/docs/guides/img/model-catelog.png b/docs/docs/getting-started/img/model-catelog.png similarity index 100% rename from docs/docs/guides/img/model-catelog.png rename to docs/docs/getting-started/img/model-catelog.png diff --git a/docs/docs/guides/img/model-version.png b/docs/docs/getting-started/img/model-version.png similarity index 100% rename from docs/docs/guides/img/model-version.png rename to docs/docs/getting-started/img/model-version.png diff --git a/docs/docs/guides/img/set-up.png b/docs/docs/getting-started/img/set-up.png similarity index 100% rename from docs/docs/guides/img/set-up.png rename to docs/docs/getting-started/img/set-up.png diff --git a/docs/docs/guides/img/start-model.png b/docs/docs/getting-started/img/start-model.png similarity index 100% rename from docs/docs/guides/img/start-model.png rename to docs/docs/getting-started/img/start-model.png diff --git a/docs/docs/guides/img/window-defender.png b/docs/docs/getting-started/img/window-defender.png similarity index 100% rename from docs/docs/guides/img/window-defender.png rename to docs/docs/getting-started/img/window-defender.png diff --git a/docs/docs/guides/install/linux.md b/docs/docs/getting-started/install/linux.md similarity index 100% rename from docs/docs/guides/install/linux.md rename to docs/docs/getting-started/install/linux.md diff --git a/docs/docs/guides/install/mac.md b/docs/docs/getting-started/install/mac.md similarity index 100% rename from docs/docs/guides/install/mac.md rename to docs/docs/getting-started/install/mac.md diff --git a/docs/docs/guides/install/windows.md b/docs/docs/getting-started/install/windows.md similarity index 100% rename from docs/docs/guides/install/windows.md rename to docs/docs/getting-started/install/windows.md diff --git a/docs/docs/guides/troubleshooting.md b/docs/docs/getting-started/troubleshooting.md similarity index 100% rename from docs/docs/guides/troubleshooting.md rename to docs/docs/getting-started/troubleshooting.md diff --git a/docs/docs/guides/overview.md b/docs/docs/guides/overview.md deleted file mode 100644 index bf44ca685..000000000 --- a/docs/docs/guides/overview.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -title: Overview -slug: /guides ---- -- Jan Platform: Desktop app/ Cloud native SaaS that can run on Linux, Windows, Mac, or even a Server that comes with extensibilities, toolbox, and state-of-the-art but optimized models for next-gen Apps. -- Jan App: Next-gen App built on Jan Plaform as `portable intelligence` that can be run everywhere. -- Models: - - Large Language Models - - Stable Diffusion models diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index 4e1525e3f..4c76686ed 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -138,13 +138,7 @@ const config = { src: "img/logo.svg", }, items: [ - // Navbar left - { - type: "docSidebar", - sidebarId: "guidesSidebar", - position: "left", - label: "User Guide", - }, + // Navbar Left { type: "docSidebar", sidebarId: "docsSidebar", diff --git a/docs/sidebars.js b/docs/sidebars.js index ea6a22a71..7736b60eb 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -29,39 +29,52 @@ const sidebars = { }, ], - guidesSidebar: [ - "guides/overview", - { - type: "category", - label: "Installation", - collapsible: true, - collapsed: false, - items: [ - { - type: "autogenerated", - dirName: "guides/install", - }, - ], - }, - "guides/troubleshooting", - ], - docsSidebar: [ - "docs/introduction", - "docs/quickstart", { type: "category", - label: "Modules", - collapsible: true, + label: "Getting Started", + collapsible: false, collapsed: false, items: [ + "docs/introduction", { - type: "autogenerated", - dirName: "docs/modules", + type: "category", + label: "Installation", + collapsible: true, + collapsed: true, + items: [ + { + type: "autogenerated", + dirName: "getting-started/install", + }, + ], + }, + "docs/quickstart", + ], + }, + { + type: "category", + label: "Building Jan", + collapsible: false, + collapsed: false, + items: [ + "docs/user-interface", + { + type: "category", + label: "Specifications", + collapsible: true, + collapsed: true, + items: [ + "docs/specs/chats", + "docs/specs/models", + "docs/specs/threads", + "docs/specs/messages", + "docs/specs/assistants", + "docs/specs/files", + ], }, ], }, - "docs/user-interface", ], apiSidebar: [ 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/electron/.prettierrc b/electron/.prettierrc new file mode 100644 index 000000000..46f1abcb0 --- /dev/null +++ b/electron/.prettierrc @@ -0,0 +1,8 @@ +{ + "semi": false, + "singleQuote": true, + "quoteProps": "consistent", + "trailingComma": "es5", + "endOfLine": "auto", + "plugins": ["prettier-plugin-tailwindcss"] +} diff --git a/electron/handlers/download.ts b/electron/handlers/download.ts index ab672f25c..3a1fc36d1 100644 --- a/electron/handlers/download.ts +++ b/electron/handlers/download.ts @@ -1,10 +1,10 @@ -import { app, ipcMain } from "electron"; -import { DownloadManager } from "../managers/download"; -import { resolve, join } from "path"; -import { WindowManager } from "../managers/window"; -import request from "request"; -import { createWriteStream, unlink } from "fs"; -const progress = require("request-progress"); +import { app, ipcMain } from 'electron' +import { DownloadManager } from '../managers/download' +import { resolve, join } from 'path' +import { WindowManager } from '../managers/window' +import request from 'request' +import { createWriteStream, unlink } from 'fs' +const progress = require('request-progress') export function handleDownloaderIPCs() { /** @@ -12,18 +12,18 @@ export function handleDownloaderIPCs() { * @param _event - The IPC event object. * @param fileName - The name of the file being downloaded. */ - ipcMain.handle("pauseDownload", async (_event, fileName) => { - DownloadManager.instance.networkRequests[fileName]?.pause(); - }); + ipcMain.handle('pauseDownload', async (_event, fileName) => { + DownloadManager.instance.networkRequests[fileName]?.pause() + }) /** * Handles the "resumeDownload" IPC message by resuming the download associated with the provided fileName. * @param _event - The IPC event object. * @param fileName - The name of the file being downloaded. */ - ipcMain.handle("resumeDownload", async (_event, fileName) => { - DownloadManager.instance.networkRequests[fileName]?.resume(); - }); + ipcMain.handle('resumeDownload', async (_event, fileName) => { + DownloadManager.instance.networkRequests[fileName]?.resume() + }) /** * Handles the "abortDownload" IPC message by aborting the download associated with the provided fileName. @@ -31,24 +31,26 @@ export function handleDownloaderIPCs() { * @param _event - The IPC event object. * @param fileName - The name of the file being downloaded. */ - ipcMain.handle("abortDownload", async (_event, fileName) => { - const rq = DownloadManager.instance.networkRequests[fileName]; - DownloadManager.instance.networkRequests[fileName] = undefined; - const userDataPath = app.getPath("userData"); - const fullPath = join(userDataPath, fileName); - rq?.abort(); - let result = "NULL"; + ipcMain.handle('abortDownload', async (_event, fileName) => { + const rq = DownloadManager.instance.networkRequests[fileName] + DownloadManager.instance.networkRequests[fileName] = undefined + const userDataPath = app.getPath('userData') + const fullPath = join(userDataPath, fileName) + rq?.abort() + let result = 'NULL' unlink(fullPath, function (err) { - if (err && err.code == "ENOENT") { - result = `File not exist: ${err}`; + if (err && err.code == 'ENOENT') { + result = `File not exist: ${err}` } else if (err) { - result = `File delete error: ${err}`; + result = `File delete error: ${err}` } else { - result = "File deleted successfully"; + result = 'File deleted successfully' } - console.log(`Delete file ${fileName} from ${fullPath} result: ${result}`); - }); - }); + console.debug( + `Delete file ${fileName} from ${fullPath} result: ${result}` + ) + }) + }) /** * Downloads a file from a given URL. @@ -56,51 +58,51 @@ export function handleDownloaderIPCs() { * @param url - The URL to download the file from. * @param fileName - The name to give the downloaded file. */ - ipcMain.handle("downloadFile", async (_event, url, fileName) => { - const userDataPath = app.getPath("userData"); - const destination = resolve(userDataPath, fileName); - const rq = request(url); + ipcMain.handle('downloadFile', async (_event, url, fileName) => { + const userDataPath = join(app.getPath('home'), 'jan') + const destination = resolve(userDataPath, fileName) + const rq = request(url) progress(rq, {}) - .on("progress", function (state: any) { + .on('progress', function (state: any) { WindowManager?.instance.currentWindow?.webContents.send( - "FILE_DOWNLOAD_UPDATE", + 'FILE_DOWNLOAD_UPDATE', { ...state, fileName, } - ); + ) }) - .on("error", function (err: Error) { + .on('error', function (err: Error) { WindowManager?.instance.currentWindow?.webContents.send( - "FILE_DOWNLOAD_ERROR", + 'FILE_DOWNLOAD_ERROR', { fileName, err, } - ); + ) }) - .on("end", function () { + .on('end', function () { if (DownloadManager.instance.networkRequests[fileName]) { WindowManager?.instance.currentWindow?.webContents.send( - "FILE_DOWNLOAD_COMPLETE", + 'FILE_DOWNLOAD_COMPLETE', { fileName, } - ); - DownloadManager.instance.setRequest(fileName, undefined); + ) + DownloadManager.instance.setRequest(fileName, undefined) } else { WindowManager?.instance.currentWindow?.webContents.send( - "FILE_DOWNLOAD_ERROR", + 'FILE_DOWNLOAD_ERROR', { fileName, - err: "Download cancelled", + err: 'Download cancelled', } - ); + ) } }) - .pipe(createWriteStream(destination)); + .pipe(createWriteStream(destination)) - DownloadManager.instance.setRequest(fileName, rq); - }); + DownloadManager.instance.setRequest(fileName, rq) + }) } diff --git a/electron/handlers/fs.ts b/electron/handlers/fs.ts index af77e3002..c1e8a85e4 100644 --- a/electron/handlers/fs.ts +++ b/electron/handlers/fs.ts @@ -1,28 +1,53 @@ -import { app, ipcMain } from "electron"; -import * as fs from "fs"; -import { join } from "path"; +import { app, ipcMain } from 'electron' +import * as fs from 'fs' +import { join } from 'path' /** * Handles file system operations. */ export function handleFsIPCs() { + const userSpacePath = join(app.getPath('home'), 'jan') + + /** + * Gets the path to the user data directory. + * @param event - The event object. + * @returns A promise that resolves with the path to the user data directory. + */ + ipcMain.handle( + 'getUserSpace', + (): Promise => Promise.resolve(userSpacePath) + ) + + /** + * Checks whether the path is a directory. + * @param event - The event object. + * @param path - The path to check. + * @returns A promise that resolves with a boolean indicating whether the path is a directory. + */ + ipcMain.handle('isDirectory', (_event, path: string): Promise => { + const fullPath = join(userSpacePath, path) + return Promise.resolve( + fs.existsSync(fullPath) && fs.lstatSync(fullPath).isDirectory() + ) + }) + /** * Reads a file from the user data directory. * @param event - The event object. * @param path - The path of the file to read. * @returns A promise that resolves with the contents of the file. */ - ipcMain.handle("readFile", async (event, path: string): Promise => { + ipcMain.handle('readFile', async (event, path: string): Promise => { return new Promise((resolve, reject) => { - fs.readFile(join(app.getPath("userData"), path), "utf8", (err, data) => { + fs.readFile(join(userSpacePath, path), 'utf8', (err, data) => { if (err) { - reject(err); + reject(err) } else { - resolve(data); + resolve(data) } - }); - }); - }); + }) + }) + }) /** * Writes data to a file in the user data directory. @@ -32,24 +57,19 @@ export function handleFsIPCs() { * @returns A promise that resolves when the file has been written. */ ipcMain.handle( - "writeFile", + 'writeFile', async (event, path: string, data: string): Promise => { return new Promise((resolve, reject) => { - fs.writeFile( - join(app.getPath("userData"), path), - data, - "utf8", - (err) => { - if (err) { - reject(err); - } else { - resolve(); - } + fs.writeFile(join(userSpacePath, path), data, 'utf8', (err) => { + if (err) { + reject(err) + } else { + resolve() } - ); - }); + }) + }) } - ); + ) /** * Creates a directory in the user data directory. @@ -57,21 +77,17 @@ export function handleFsIPCs() { * @param path - The path of the directory to create. * @returns A promise that resolves when the directory has been created. */ - ipcMain.handle("mkdir", async (event, path: string): Promise => { + ipcMain.handle('mkdir', async (event, path: string): Promise => { return new Promise((resolve, reject) => { - fs.mkdir( - join(app.getPath("userData"), path), - { recursive: true }, - (err) => { - if (err) { - reject(err); - } else { - resolve(); - } + fs.mkdir(join(userSpacePath, path), { recursive: true }, (err) => { + if (err) { + reject(err) + } else { + resolve() } - ); - }); - }); + }) + }) + }) /** * Removes a directory in the user data directory. @@ -79,21 +95,17 @@ export function handleFsIPCs() { * @param path - The path of the directory to remove. * @returns A promise that resolves when the directory is removed successfully. */ - ipcMain.handle("rmdir", async (event, path: string): Promise => { + ipcMain.handle('rmdir', async (event, path: string): Promise => { return new Promise((resolve, reject) => { - fs.rmdir( - join(app.getPath("userData"), path), - { recursive: true }, - (err) => { - if (err) { - reject(err); - } else { - resolve(); - } + fs.rmdir(join(userSpacePath, path), { recursive: true }, (err) => { + if (err) { + reject(err) + } else { + resolve() } - ); - }); - }); + }) + }) + }) /** * Lists the files in a directory in the user data directory. @@ -102,19 +114,19 @@ export function handleFsIPCs() { * @returns A promise that resolves with an array of file names. */ ipcMain.handle( - "listFiles", + 'listFiles', async (event, path: string): Promise => { return new Promise((resolve, reject) => { - fs.readdir(join(app.getPath("userData"), path), (err, files) => { + fs.readdir(join(userSpacePath, path), (err, files) => { if (err) { - reject(err); + reject(err) } else { - resolve(files); + resolve(files) } - }); - }); + }) + }) } - ); + ) /** * Deletes a file from the user data folder. @@ -122,22 +134,23 @@ export function handleFsIPCs() { * @param filePath - The path to the file to delete. * @returns A string indicating the result of the operation. */ - ipcMain.handle("deleteFile", async (_event, filePath) => { - const userDataPath = app.getPath("userData"); - const fullPath = join(userDataPath, filePath); + ipcMain.handle('deleteFile', async (_event, filePath) => { + const fullPath = join(userSpacePath, filePath) - let result = "NULL"; + let result = 'NULL' fs.unlink(fullPath, function (err) { - if (err && err.code == "ENOENT") { - result = `File not exist: ${err}`; + if (err && err.code == 'ENOENT') { + result = `File not exist: ${err}` } else if (err) { - result = `File delete error: ${err}`; + result = `File delete error: ${err}` } else { - result = "File deleted successfully"; + result = 'File deleted successfully' } - console.log(`Delete file ${filePath} from ${fullPath} result: ${result}`); - }); + console.debug( + `Delete file ${filePath} from ${fullPath} result: ${result}` + ) + }) - return result; - }); + return result + }) } diff --git a/electron/handlers/plugin.ts b/electron/handlers/plugin.ts index 26eb3c583..22bf253e6 100644 --- a/electron/handlers/plugin.ts +++ b/electron/handlers/plugin.ts @@ -30,7 +30,7 @@ export function handlePluginIPCs() { if (typeof module[method] === "function") { return module[method](...args); } else { - console.log(module[method]); + console.debug(module[method]); console.error(`Function "${method}" does not exist in the module.`); } } @@ -75,7 +75,7 @@ export function handlePluginIPCs() { const fullPath = join(userDataPath, "plugins"); rmdir(fullPath, { recursive: true }, function (err) { - if (err) console.log(err); + if (err) console.error(err); ModuleManager.instance.clearImportedModules(); // just relaunch if packaged, should launch manually in development mode diff --git a/electron/handlers/update.ts b/electron/handlers/update.ts index 096d09bac..340db54b9 100644 --- a/electron/handlers/update.ts +++ b/electron/handlers/update.ts @@ -42,7 +42,7 @@ export function handleAppUpdates() { /* App Update Progress */ autoUpdater.on("download-progress", (progress: any) => { - console.log("app update progress: ", progress.percent); + console.debug("app update progress: ", progress.percent); WindowManager.instance.currentWindow?.webContents.send( "APP_UPDATE_PROGRESS", { diff --git a/electron/main.ts b/electron/main.ts index 741a75867..5f1d6b086 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -1,23 +1,23 @@ -import { app, BrowserWindow } from "electron"; -import { join } from "path"; -import { setupMenu } from "./utils/menu"; -import { handleFsIPCs } from "./handlers/fs"; +import { app, BrowserWindow } from 'electron' +import { join } from 'path' +import { setupMenu } from './utils/menu' +import { handleFsIPCs } from './handlers/fs' /** * Managers **/ -import { WindowManager } from "./managers/window"; -import { ModuleManager } from "./managers/module"; -import { PluginManager } from "./managers/plugin"; +import { WindowManager } from './managers/window' +import { ModuleManager } from './managers/module' +import { PluginManager } from './managers/plugin' /** * IPC Handlers **/ -import { handleDownloaderIPCs } from "./handlers/download"; -import { handleThemesIPCs } from "./handlers/theme"; -import { handlePluginIPCs } from "./handlers/plugin"; -import { handleAppIPCs } from "./handlers/app"; -import { handleAppUpdates } from "./handlers/update"; +import { handleDownloaderIPCs } from './handlers/download' +import { handleThemesIPCs } from './handlers/theme' +import { handlePluginIPCs } from './handlers/plugin' +import { handleAppIPCs } from './handlers/app' +import { handleAppUpdates } from './handlers/update' app .whenReady() @@ -28,56 +28,56 @@ app .then(handleAppUpdates) .then(createMainWindow) .then(() => { - app.on("activate", () => { + app.on('activate', () => { if (!BrowserWindow.getAllWindows().length) { - createMainWindow(); + createMainWindow() } - }); - }); + }) + }) -app.on("window-all-closed", () => { - ModuleManager.instance.clearImportedModules(); - app.quit(); -}); +app.on('window-all-closed', () => { + ModuleManager.instance.clearImportedModules() + app.quit() +}) -app.on("quit", () => { - ModuleManager.instance.clearImportedModules(); - app.quit(); -}); +app.on('quit', () => { + ModuleManager.instance.clearImportedModules() + app.quit() +}) function createMainWindow() { /* Create main window */ const mainWindow = WindowManager.instance.createWindow({ webPreferences: { nodeIntegration: true, - preload: join(__dirname, "preload.js"), + preload: join(__dirname, 'preload.js'), webSecurity: false, }, - }); + }) const startURL = app.isPackaged - ? `file://${join(__dirname, "../renderer/index.html")}` - : "http://localhost:3000"; + ? `file://${join(__dirname, '../renderer/index.html')}` + : 'http://localhost:3000' /* Load frontend app to the window */ - mainWindow.loadURL(startURL); + mainWindow.loadURL(startURL) - mainWindow.once("ready-to-show", () => mainWindow?.show()); - mainWindow.on("closed", () => { - if (process.platform !== "darwin") app.quit(); - }); + mainWindow.once('ready-to-show', () => mainWindow?.show()) + mainWindow.on('closed', () => { + if (process.platform !== 'darwin') app.quit() + }) /* Enable dev tools for development */ - if (!app.isPackaged) mainWindow.webContents.openDevTools(); + if (!app.isPackaged) mainWindow.webContents.openDevTools() } /** * Handles various IPC messages from the renderer process. */ function handleIPCs() { - handleFsIPCs(); - handleDownloaderIPCs(); - handleThemesIPCs(); - handlePluginIPCs(); - handleAppIPCs(); + handleFsIPCs() + handleDownloaderIPCs() + handleThemesIPCs() + handlePluginIPCs() + handleAppIPCs() } diff --git a/electron/managers/plugin.ts b/electron/managers/plugin.ts index 889425ec7..227eab34e 100644 --- a/electron/managers/plugin.ts +++ b/electron/managers/plugin.ts @@ -42,14 +42,14 @@ export class PluginManager { return new Promise((resolve) => { const store = new Store(); if (store.get("migrated_version") !== app.getVersion()) { - console.log("start migration:", store.get("migrated_version")); + console.debug("start migration:", store.get("migrated_version")); const userDataPath = app.getPath("userData"); const fullPath = join(userDataPath, "plugins"); rmdir(fullPath, { recursive: true }, function (err) { - if (err) console.log(err); + if (err) console.error(err); store.set("migrated_version", app.getVersion()); - console.log("migrate plugins done"); + console.debug("migrate plugins done"); resolve(undefined); }); } else { diff --git a/electron/package.json b/electron/package.json index 107264805..0eed33b2e 100644 --- a/electron/package.json +++ b/electron/package.json @@ -52,18 +52,18 @@ "lint": "eslint . --ext \".js,.jsx,.ts,.tsx\"", "test:e2e": "playwright test --workers=1", "dev": "tsc -p . && electron .", - "build": "tsc -p . && electron-builder -p never -m", - "build:test": "tsc -p . && electron-builder --dir -p never -m", - "build:test-darwin": "tsc -p . && electron-builder -p never -m --x64 --arm64 --dir", - "build:test-win32": "tsc -p . && electron-builder -p never -w --dir", - "build:test-linux": "tsc -p . && electron-builder -p never -l --dir", - "build:darwin": "tsc -p . && electron-builder -p never -m --x64 --arm64", + "build": "run-script-os", + "build:test": "run-script-os", + "build:test:darwin": "tsc -p . && electron-builder -p never -m --dir", + "build:test:win32": "tsc -p . && electron-builder -p never -w --dir", + "build:test:linux": "tsc -p . && electron-builder -p never -l --dir", + "build:darwin": "tsc -p . && electron-builder -p never -m", "build:win32": "tsc -p . && electron-builder -p never -w", "build:linux": "tsc -p . && electron-builder -p never --linux deb", - "build:publish": "tsc -p . && electron-builder -p onTagOrDraft -m", - "build:publish-darwin": "tsc -p . && electron-builder -p onTagOrDraft -m --x64 --arm64", - "build:publish-win32": "tsc -p . && electron-builder -p onTagOrDraft -w", - "build:publish-linux": "tsc -p . && electron-builder -p onTagOrDraft --linux deb " + "build:publish": "run-script-os", + "build:publish:darwin": "tsc -p . && electron-builder -p onTagOrDraft -m --x64 --arm64", + "build:publish:win32": "tsc -p . && electron-builder -p onTagOrDraft -w", + "build:publish:linux": "tsc -p . && electron-builder -p onTagOrDraft -l deb" }, "dependencies": { "@npmcli/arborist": "^7.1.0", @@ -86,7 +86,8 @@ "electron": "26.2.1", "electron-builder": "^24.6.4", "electron-playwright-helpers": "^1.6.0", - "eslint-plugin-react": "^7.33.2" + "eslint-plugin-react": "^7.33.2", + "run-script-os": "^1.1.6" }, "installConfig": { "hoistingLimits": "workspaces" diff --git a/electron/preload.ts b/electron/preload.ts index 398913da2..dfba13bd1 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -33,6 +33,8 @@ * @property {Function} relaunch - Relaunches the app. * @property {Function} openAppDirectory - Opens the app directory. * @property {Function} deleteFile - Deletes the file at the given path. + * @property {Function} isDirectory - Returns true if the file at the given path is a directory. + * @property {Function} getUserSpace - Returns the user space. * @property {Function} readFile - Reads the file at the given path. * @property {Function} writeFile - Writes the given data to the file at the given path. * @property {Function} listFiles - Lists the files in the directory at the given path. @@ -52,81 +54,85 @@ */ // Make Pluggable Electron's facade available to the renderer on window.plugins -import { useFacade } from "./core/plugin/facade"; +import { useFacade } from './core/plugin/facade' -useFacade(); +useFacade() -const { contextBridge, ipcRenderer } = require("electron"); +const { contextBridge, ipcRenderer } = require('electron') -contextBridge.exposeInMainWorld("electronAPI", { +contextBridge.exposeInMainWorld('electronAPI', { invokePluginFunc: (plugin: any, method: any, ...args: any[]) => - ipcRenderer.invoke("invokePluginFunc", plugin, method, ...args), + ipcRenderer.invoke('invokePluginFunc', plugin, method, ...args), - setNativeThemeLight: () => ipcRenderer.invoke("setNativeThemeLight"), + setNativeThemeLight: () => ipcRenderer.invoke('setNativeThemeLight'), - setNativeThemeDark: () => ipcRenderer.invoke("setNativeThemeDark"), + setNativeThemeDark: () => ipcRenderer.invoke('setNativeThemeDark'), - setNativeThemeSystem: () => ipcRenderer.invoke("setNativeThemeSystem"), + setNativeThemeSystem: () => ipcRenderer.invoke('setNativeThemeSystem'), - basePlugins: () => ipcRenderer.invoke("basePlugins"), + basePlugins: () => ipcRenderer.invoke('basePlugins'), - pluginPath: () => ipcRenderer.invoke("pluginPath"), + pluginPath: () => ipcRenderer.invoke('pluginPath'), - appDataPath: () => ipcRenderer.invoke("appDataPath"), + appDataPath: () => ipcRenderer.invoke('appDataPath'), - reloadPlugins: () => ipcRenderer.invoke("reloadPlugins"), + reloadPlugins: () => ipcRenderer.invoke('reloadPlugins'), - appVersion: () => ipcRenderer.invoke("appVersion"), + appVersion: () => ipcRenderer.invoke('appVersion'), - openExternalUrl: (url: string) => ipcRenderer.invoke("openExternalUrl", url), + openExternalUrl: (url: string) => ipcRenderer.invoke('openExternalUrl', url), - relaunch: () => ipcRenderer.invoke("relaunch"), + relaunch: () => ipcRenderer.invoke('relaunch'), - openAppDirectory: () => ipcRenderer.invoke("openAppDirectory"), + openAppDirectory: () => ipcRenderer.invoke('openAppDirectory'), - deleteFile: (filePath: string) => ipcRenderer.invoke("deleteFile", filePath), + deleteFile: (filePath: string) => ipcRenderer.invoke('deleteFile', filePath), - readFile: (path: string) => ipcRenderer.invoke("readFile", path), + isDirectory: (filePath: string) => ipcRenderer.invoke('isDirectory', filePath), + + getUserSpace: () => ipcRenderer.invoke('getUserSpace'), + + readFile: (path: string) => ipcRenderer.invoke('readFile', path), writeFile: (path: string, data: string) => - ipcRenderer.invoke("writeFile", path, data), + ipcRenderer.invoke('writeFile', path, data), - listFiles: (path: string) => ipcRenderer.invoke("listFiles", path), + listFiles: (path: string) => ipcRenderer.invoke('listFiles', path), - mkdir: (path: string) => ipcRenderer.invoke("mkdir", path), + mkdir: (path: string) => ipcRenderer.invoke('mkdir', path), - rmdir: (path: string) => ipcRenderer.invoke("rmdir", path), + rmdir: (path: string) => ipcRenderer.invoke('rmdir', path), installRemotePlugin: (pluginName: string) => - ipcRenderer.invoke("installRemotePlugin", pluginName), + ipcRenderer.invoke('installRemotePlugin', pluginName), downloadFile: (url: string, path: string) => - ipcRenderer.invoke("downloadFile", url, path), + ipcRenderer.invoke('downloadFile', url, path), pauseDownload: (fileName: string) => - ipcRenderer.invoke("pauseDownload", fileName), + ipcRenderer.invoke('pauseDownload', fileName), resumeDownload: (fileName: string) => - ipcRenderer.invoke("resumeDownload", fileName), + ipcRenderer.invoke('resumeDownload', fileName), abortDownload: (fileName: string) => - ipcRenderer.invoke("abortDownload", fileName), + ipcRenderer.invoke('abortDownload', fileName), onFileDownloadUpdate: (callback: any) => - ipcRenderer.on("FILE_DOWNLOAD_UPDATE", callback), + ipcRenderer.on('FILE_DOWNLOAD_UPDATE', callback), onFileDownloadError: (callback: any) => - ipcRenderer.on("FILE_DOWNLOAD_ERROR", callback), + ipcRenderer.on('FILE_DOWNLOAD_ERROR', callback), onFileDownloadSuccess: (callback: any) => - ipcRenderer.on("FILE_DOWNLOAD_COMPLETE", callback), + ipcRenderer.on('FILE_DOWNLOAD_COMPLETE', callback), onAppUpdateDownloadUpdate: (callback: any) => - ipcRenderer.on("APP_UPDATE_PROGRESS", callback), + ipcRenderer.on('APP_UPDATE_PROGRESS', callback), onAppUpdateDownloadError: (callback: any) => - ipcRenderer.on("APP_UPDATE_ERROR", callback), + ipcRenderer.on('APP_UPDATE_ERROR', callback), onAppUpdateDownloadSuccess: (callback: any) => - ipcRenderer.on("APP_UPDATE_COMPLETE", callback), -}); + ipcRenderer.on('APP_UPDATE_COMPLETE', callback), +}) diff --git a/package.json b/package.json index 32506ea80..036542c38 100644 --- a/package.json +++ b/package.json @@ -36,27 +36,16 @@ "build:electron": "yarn workspace jan build", "build:electron:test": "yarn workspace jan build:test", "build:plugins": "rimraf ./electron/core/pre-install/*.tgz && concurrently --kill-others-on-fail \"cd ./plugins/conversational-json && npm install && npm run build:publish\" \"cd ./plugins/inference-plugin && npm install && npm run build:publish\" \"cd ./plugins/model-plugin && npm install && npm run build:publish\" \"cd ./plugins/monitoring-plugin && npm install && npm run build:publish\"", - "build:plugins-win32": "rimraf ./electron/core/pre-install/*.tgz && concurrently --kill-others-on-fail \"cd ./plugins/conversational-json && npm install && npm run build:publish\" \"cd ./plugins/inference-plugin && npm install && npm run build:publish-win32\" \"cd ./plugins/model-plugin && npm install && npm run build:publish\" \"cd ./plugins/monitoring-plugin && npm install && npm run build:publish\"", - "build:plugins-linux": "rimraf ./electron/core/pre-install/*.tgz && concurrently --kill-others-on-fail \"cd ./plugins/conversational-json && npm install && npm run build:publish\" \"cd ./plugins/inference-plugin && npm install && npm run build:publish-linux\" \"cd ./plugins/model-plugin && npm install && npm run build:publish\" \"cd ./plugins/monitoring-plugin && npm install && npm run build:publish\"", - "build:plugins-darwin": "rimraf ./electron/core/pre-install/*.tgz && concurrently --kill-others-on-fail \"cd ./plugins/conversational-json && npm install && npm run build:publish\" \"cd ./plugins/inference-plugin && npm install && npm run build:publish-darwin\" \"cd ./plugins/model-plugin && npm install && npm run build:publish\" \"cd ./plugins/monitoring-plugin && npm install && npm run build:publish\"", - "build:test": "yarn build:web && yarn build:electron:test", - "build:test-darwin": "yarn build:web && yarn workspace jan build:test-darwin", - "build:test-win32": "yarn build:web && yarn workspace jan build:test-win32", - "build:test-linux": "yarn build:web && yarn workspace jan build:test-linux", - "build": "yarn build:web && yarn build:electron", - "build:darwin": "yarn build:web && yarn workspace jan build:darwin", - "build:win32": "yarn build:web && yarn workspace jan build:win32", - "build:linux": "yarn build:web && yarn workspace jan build:linux", - "build:publish": "yarn build:web && yarn workspace jan build:publish", - "build:publish-darwin": "yarn build:web && yarn workspace jan build:publish-darwin", - "build:publish-win32": "yarn build:web && yarn workspace jan build:publish-win32", - "build:publish-linux": "yarn build:web && yarn workspace jan build:publish-linux" + "build:test": "yarn build:web && yarn workspace jan build:test", + "build": "yarn build:web && yarn workspace jan build", + "build:publish": "yarn build:web && yarn workspace jan build:publish" }, "devDependencies": { "concurrently": "^8.2.1", "cpx": "^1.5.0", "rimraf": "^3.0.2", - "wait-on": "^7.0.1" + "wait-on": "^7.0.1", + "run-script-os": "^1.1.6" }, "version": "0.0.0" } diff --git a/plugins/conversational-json/package.json b/plugins/conversational-json/package.json index 198756f82..520970664 100644 --- a/plugins/conversational-json/package.json +++ b/plugins/conversational-json/package.json @@ -24,6 +24,7 @@ }, "dependencies": { "@janhq/core": "file:../../core", + "path-browserify": "^1.0.1", "ts-loader": "^9.5.0" }, "engines": { diff --git a/plugins/conversational-json/src/index.ts b/plugins/conversational-json/src/index.ts index b87f52a84..94082bb45 100644 --- a/plugins/conversational-json/src/index.ts +++ b/plugins/conversational-json/src/index.ts @@ -1,12 +1,15 @@ 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' /** * JSONConversationalPlugin is a ConversationalPlugin implementation that provides * functionality for managing conversations. */ export default class JSONConversationalPlugin implements ConversationalPlugin { + private static readonly _homeDir = 'threads' + /** * Returns the type of the plugin. */ @@ -18,7 +21,7 @@ export default class JSONConversationalPlugin implements ConversationalPlugin { * Called when the plugin is loaded. */ onLoad() { - fs.mkdir('conversations') + fs.mkdir(JSONConversationalPlugin._homeDir) console.debug('JSONConversationalPlugin loaded') } @@ -32,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() @@ -43,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) @@ -63,12 +66,16 @@ 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(`conversations/${conversation._id}`) + .mkdir(`${JSONConversationalPlugin._homeDir}/${conversation.id}`) .then(() => fs.writeFile( - `conversations/${conversation._id}/${conversation._id}.json`, + join( + JSONConversationalPlugin._homeDir, + conversation.id, + `${conversation.id}.json` + ), JSON.stringify(conversation) ) ) @@ -79,7 +86,9 @@ export default class JSONConversationalPlugin implements ConversationalPlugin { * @param conversationId The ID of the conversation to delete. */ deleteConversation(conversationId: string): Promise { - return fs.rmdir(`conversations/${conversationId}`) + return fs.rmdir( + join(JSONConversationalPlugin._homeDir, `${conversationId}`) + ) } /** @@ -88,7 +97,9 @@ export default class JSONConversationalPlugin implements ConversationalPlugin { * @returns data of the conversation */ private async readConvo(convoId: string): Promise { - return fs.readFile(`conversations/${convoId}/${convoId}.json`) + return fs.readFile( + join(JSONConversationalPlugin._homeDir, convoId, `${convoId}.json`) + ) } /** @@ -97,8 +108,10 @@ export default class JSONConversationalPlugin implements ConversationalPlugin { * @private */ private async getConversationDocs(): Promise { - return fs.listFiles(`conversations`).then((files: string[]) => { - return Promise.all(files.filter((file) => file.startsWith('jan-'))) - }) + return fs + .listFiles(JSONConversationalPlugin._homeDir) + .then((files: string[]) => { + return Promise.all(files.filter((file) => file.startsWith('jan-'))) + }) } } diff --git a/plugins/conversational-json/webpack.config.js b/plugins/conversational-json/webpack.config.js index d4b0db2bd..36e338295 100644 --- a/plugins/conversational-json/webpack.config.js +++ b/plugins/conversational-json/webpack.config.js @@ -22,6 +22,9 @@ module.exports = { plugins: [new webpack.DefinePlugin({})], resolve: { extensions: [".ts", ".js"], + fallback: { + path: require.resolve('path-browserify'), + }, }, // Do not minify the output, otherwise it breaks the class registration optimization: { 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 b1c5bd937..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/package.json b/plugins/inference-plugin/package.json index 5c52d13d0..3b7159c3c 100644 --- a/plugins/inference-plugin/package.json +++ b/plugins/inference-plugin/package.json @@ -13,18 +13,14 @@ ], "scripts": { "build": "tsc -b . && webpack --config webpack.config.js", - "downloadnitro:linux-cpu": "NITRO_VERSION=$(cat ./nitro/version.txt) && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-linux-amd64.zip -e --strip 1 -o ./nitro/linux-cpu && chmod +x ./nitro/linux-cpu/nitro && chmod +x ./nitro/linux-start.sh ", - "downloadnitro:linux-cuda": "NITRO_VERSION=$(cat ./nitro/version.txt) && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-linux-amd64-cuda.zip -e --strip 1 -o ./nitro/linux-cuda && chmod +x ./nitro/linux-cuda/nitro && chmod +x ./nitro/linux-start.sh", - "downloadnitro:darwin-arm64": "NITRO_VERSION=$(cat ./nitro/version.txt) && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-mac-arm64.zip -e --strip 1 -o ./nitro/mac-arm64 && chmod +x ./nitro/mac-arm64/nitro", - "downloadnitro:darwin-x64": "NITRO_VERSION=$(cat ./nitro/version.txt) && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-mac-amd64.zip -e --strip 1 -o ./nitro/mac-x64 && chmod +x ./nitro/mac-x64/nitro", - "downloadnitro:win32-cpu": "download https://github.com/janhq/nitro/releases/download/v%NITRO_VERSION%/nitro-%NITRO_VERSION%-win-amd64.zip -e --strip 1 -o ./nitro/win-cpu", - "downloadnitro:win32-cuda": "download https://github.com/janhq/nitro/releases/download/v%NITRO_VERSION%/nitro-%NITRO_VERSION%-win-amd64-cuda.zip -e --strip 1 -o ./nitro/win-cuda", - "downloadnitro:all": "npm run downloadnitro:darwin-arm64 && npm run downloadnitro:darwin-x64 && downloadnitro:win32-cpu && npm run downloadnitro:win32-cuda && npm run downloadnitro:linux-cpu && npm run downloadnitro:linux-cuda", - "build:publish": "rimraf *.tgz --glob && npm run build && npm run downloadnitro:darwin-arm64 && npm run downloadnitro:darwin-x64 && cpx \"nitro/**\" \"dist/nitro\" && npm pack && cpx *.tgz ../../electron/core/pre-install", - "build:publish-darwin": "rimraf *.tgz --glob && npm run build && npm run downloadnitro:darwin-arm64 && npm run downloadnitro:darwin-x64 && ../../.github/scripts/auto-sign.sh && cpx \"nitro/**\" \"dist/nitro\" && npm pack && cpx *.tgz ../../electron/core/pre-install", - "build:publish-win32": "rimraf *.tgz --glob && npm run build && npm run downloadnitro:win32-cpu && npm run downloadnitro:win32-cuda && cpx \"nitro/**\" \"dist/nitro\" && npm pack && cpx *.tgz ../../electron/core/pre-install", - "build:publish-linux": "rimraf *.tgz --glob && npm run build && npm run downloadnitro:linux-cpu && npm run downloadnitro:linux-cuda && cpx \"nitro/**\" \"dist/nitro\" && npm pack && cpx *.tgz ../../electron/core/pre-install", - "build:publish-all": "rimraf *.tgz --glob && npm run build && npm run downloadnitro:all && cpx \"nitro/**\" \"dist/nitro\" && npm pack && cpx *.tgz ../../electron/core/pre-install" + "downloadnitro:linux": "NITRO_VERSION=$(cat ./nitro/version.txt) && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-linux-amd64.zip -e --strip 1 -o ./nitro/linux-cpu && chmod +x ./nitro/linux-cpu/nitro && chmod +x ./nitro/linux-start.sh && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-linux-amd64-cuda.zip -e --strip 1 -o ./nitro/linux-cuda && chmod +x ./nitro/linux-cuda/nitro && chmod +x ./nitro/linux-start.sh", + "downloadnitro:darwin": "NITRO_VERSION=$(cat ./nitro/version.txt) && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-mac-arm64.zip -e --strip 1 -o ./nitro/mac-arm64 && chmod +x ./nitro/mac-arm64/nitro && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-mac-amd64.zip -e --strip 1 -o ./nitro/mac-x64 && chmod +x ./nitro/mac-x64/nitro", + "downloadnitro:win32": "download https://github.com/janhq/nitro/releases/download/v%NITRO_VERSION%/nitro-%NITRO_VERSION%-win-amd64.zip -e --strip 1 -o ./nitro/win-cpu && download https://github.com/janhq/nitro/releases/download/v%NITRO_VERSION%/nitro-%NITRO_VERSION%-win-amd64-cuda.zip -e --strip 1 -o ./nitro/win-cuda", + "downloadnitro": "run-script-os", + "build:publish:darwin": "rimraf *.tgz --glob && npm run build && npm run downloadnitro && ../../.github/scripts/auto-sign.sh && cpx \"nitro/**\" \"dist/nitro\" && npm pack && cpx *.tgz ../../electron/core/pre-install", + "build:publish:win32": "rimraf *.tgz --glob && npm run build && npm run downloadnitro && cpx \"nitro/**\" \"dist/nitro\" && npm pack && cpx *.tgz ../../electron/core/pre-install", + "build:publish:linux": "rimraf *.tgz --glob && npm run build && npm run downloadnitro && cpx \"nitro/**\" \"dist/nitro\" && npm pack && cpx *.tgz ../../electron/core/pre-install", + "build:publish": "run-script-os" }, "exports": { ".": "./dist/index.js", @@ -33,6 +29,7 @@ "devDependencies": { "cpx": "^1.5.0", "rimraf": "^3.0.2", + "run-script-os": "^1.1.6", "webpack": "^5.88.2", "webpack-cli": "^5.1.4" }, diff --git a/plugins/inference-plugin/src/index.ts b/plugins/inference-plugin/src/index.ts index ebd44657f..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"; @@ -18,7 +21,7 @@ import { InferencePlugin } from "@janhq/core/lib/plugins"; import { requestInference } from "./helpers/sse"; import { ulid } from "ulid"; import { join } from "path"; -import { appDataPath } from "@janhq/core"; +import { fs } from "@janhq/core"; /** * A class that implements the InferencePlugin interface from the @janhq/core package. @@ -54,8 +57,10 @@ export default class JanInferencePlugin implements InferencePlugin { * @returns {Promise} A promise that resolves when the model is initialized. */ async initModel(modelFileName: string): Promise { - const appPath = await appDataPath(); - return executeOnMain(MODULE, "initModel", join(appPath, modelFileName)); + const userSpacePath = await fs.getUserSpace(); + const modelFullPath = join(userSpacePath, modelFileName); + + return executeOnMain(MODULE, "initModel", modelFullPath); } /** @@ -68,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 = await (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; }, @@ -106,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(), + 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/inference-plugin/src/module.ts b/plugins/inference-plugin/src/module.ts index ba6afdf90..613ad9fca 100644 --- a/plugins/inference-plugin/src/module.ts +++ b/plugins/inference-plugin/src/module.ts @@ -124,7 +124,7 @@ function killSubprocess(): Promise { if (subprocess) { subprocess.kill(); subprocess = null; - console.log("Subprocess terminated."); + console.debug("Subprocess terminated."); } else { return kill(PORT, "tcp").then(console.log).catch(console.log); } diff --git a/plugins/model-plugin/.prettierrc b/plugins/model-plugin/.prettierrc new file mode 100644 index 000000000..46f1abcb0 --- /dev/null +++ b/plugins/model-plugin/.prettierrc @@ -0,0 +1,8 @@ +{ + "semi": false, + "singleQuote": true, + "quoteProps": "consistent", + "trailingComma": "es5", + "endOfLine": "auto", + "plugins": ["prettier-plugin-tailwindcss"] +} diff --git a/plugins/model-plugin/package.json b/plugins/model-plugin/package.json index 171f0a4e9..43d1ffa8e 100644 --- a/plugins/model-plugin/package.json +++ b/plugins/model-plugin/package.json @@ -29,6 +29,7 @@ ], "dependencies": { "@janhq/core": "file:../../core", + "path-browserify": "^1.0.1", "ts-loader": "^9.5.0" } } diff --git a/plugins/model-plugin/src/helpers/cloudNative.ts b/plugins/model-plugin/src/helpers/cloudNative.ts deleted file mode 100644 index 90c6d3f1e..000000000 --- a/plugins/model-plugin/src/helpers/cloudNative.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { EventName, events } from "@janhq/core"; - -export async function pollDownloadProgress(fileName: string) { - if ( - typeof window !== "undefined" && - typeof (window as any).electronAPI === "undefined" - ) { - const intervalId = setInterval(() => { - notifyProgress(fileName, intervalId); - }, 3000); - } -} - -export async function notifyProgress( - fileName: string, - intervalId: NodeJS.Timeout -): Promise { - const response = await fetch("/api/v1/downloadProgress", { - method: "POST", - body: JSON.stringify({ fileName: fileName }), - headers: { "Content-Type": "application/json", Authorization: "" }, - }); - - if (!response.ok) { - events.emit(EventName.OnDownloadError, null); - clearInterval(intervalId); - return; - } - const json = await response.json(); - if (isEmptyObject(json)) { - if (!fileName && intervalId) { - clearInterval(intervalId); - } - return Promise.resolve(""); - } - if (json.success === true) { - events.emit(EventName.OnDownloadSuccess, json); - clearInterval(intervalId); - return Promise.resolve(""); - } else { - events.emit(EventName.OnDownloadUpdate, json); - return Promise.resolve(json.fileName); - } -} - -function isEmptyObject(ojb: any): boolean { - return Object.keys(ojb).length === 0; -} diff --git a/plugins/model-plugin/src/helpers/modelParser.ts b/plugins/model-plugin/src/helpers/modelParser.ts index d8b8a81f5..826a2afba 100644 --- a/plugins/model-plugin/src/helpers/modelParser.ts +++ b/plugins/model-plugin/src/helpers/modelParser.ts @@ -1,8 +1,8 @@ export const parseToModel = (model) => { - const modelVersions = []; + const modelVersions = [] model.versions.forEach((v) => { const version = { - _id: `${model.author}-${v.name}`, + id: `${model.author}-${v.name}`, name: v.name, quantMethod: v.quantMethod, bits: v.bits, @@ -11,12 +11,12 @@ export const parseToModel = (model) => { usecase: v.usecase, downloadLink: v.downloadLink, productId: model.id, - }; - modelVersions.push(version); - }); + } + modelVersions.push(version) + }) const product = { - _id: model.id, + id: model.id, name: model.name, shortDescription: model.shortDescription, avatarUrl: model.avatarUrl, @@ -29,9 +29,9 @@ export const parseToModel = (model) => { type: model.type, createdAt: model.createdAt, longDescription: model.longDescription, - status: "Downloadable", + status: 'Downloadable', releaseDate: 0, availableVersions: modelVersions, - }; - return product; -}; + } + return product +} diff --git a/plugins/model-plugin/src/index.ts b/plugins/model-plugin/src/index.ts index ccfed6bfe..5fb487017 100644 --- a/plugins/model-plugin/src/index.ts +++ b/plugins/model-plugin/src/index.ts @@ -1,20 +1,21 @@ -import { PluginType, fs, downloadFile } from "@janhq/core"; -import { ModelPlugin } from "@janhq/core/lib/plugins"; -import { Model, ModelCatalog } from "@janhq/core/lib/types"; -import { pollDownloadProgress } from "./helpers/cloudNative"; -import { parseToModel } from "./helpers/modelParser"; +import { PluginType, fs, downloadFile } from '@janhq/core' +import { ModelPlugin } from '@janhq/core/lib/plugins' +import { Model, ModelCatalog } from '@janhq/core/lib/types' +import { parseToModel } from './helpers/modelParser' +import { join } from 'path' /** * A plugin for managing machine learning models. */ export default class JanModelPlugin implements ModelPlugin { + private static readonly _homeDir = 'models' /** * Implements type from JanPlugin. * @override * @returns The type of the plugin. */ type(): PluginType { - return PluginType.Model; + return PluginType.Model } /** @@ -25,6 +26,7 @@ export default class JanModelPlugin implements ModelPlugin { /** Cloud Native * TODO: Fetch all downloading progresses? **/ + fs.mkdir(JanModelPlugin._homeDir) } /** @@ -39,12 +41,13 @@ export default class JanModelPlugin implements ModelPlugin { * @returns A Promise that resolves when the model is downloaded. */ async downloadModel(model: Model): Promise { - await fs.mkdir("models"); - downloadFile(model.downloadLink, `models/${model._id}`); - /** Cloud Native - * MARK: Poll Downloading Progress - **/ - pollDownloadProgress(model._id); + // create corresponding directory + const directoryPath = join(JanModelPlugin._homeDir, model.name) + await fs.mkdir(directoryPath) + + // path to model binary + const path = join(directoryPath, model.id) + downloadFile(model.downloadLink, path) } /** @@ -52,10 +55,15 @@ export default class JanModelPlugin implements ModelPlugin { * @param filePath - The path to the model file to delete. * @returns A Promise that resolves when the model is deleted. */ - deleteModel(filePath: string): Promise { - return fs - .deleteFile(`models/${filePath}`) - .then(() => fs.deleteFile(`models/m-${filePath}.json`)); + async deleteModel(filePath: string): Promise { + try { + await Promise.allSettled([ + fs.deleteFile(filePath), + fs.deleteFile(`${filePath}.json`), + ]) + } catch (err) { + console.error(err) + } } /** @@ -64,30 +72,46 @@ export default class JanModelPlugin implements ModelPlugin { * @returns A Promise that resolves when the model is saved. */ async saveModel(model: Model): Promise { - await fs.writeFile(`models/m-${model._id}.json`, JSON.stringify(model)); + const directoryPath = join(JanModelPlugin._homeDir, model.name) + const jsonFilePath = join(directoryPath, `${model.id}.json`) + + try { + await fs.writeFile(jsonFilePath, JSON.stringify(model)) + } catch (err) { + console.error(err) + } } /** * Gets all downloaded models. * @returns A Promise that resolves with an array of all models. */ - getDownloadedModels(): Promise { - return fs - .listFiles("models") - .then((files: string[]) => { - return Promise.all( - files - .filter((file) => /^m-.*\.json$/.test(file)) - .map(async (file) => { - const model: Model = JSON.parse( - await fs.readFile(`models/${file}`) - ); - return model; - }) - ); - }) - .catch((e) => fs.mkdir("models").then(() => [])); + async getDownloadedModels(): Promise { + const results: Model[] = [] + const allDirs: string[] = await fs.listFiles(JanModelPlugin._homeDir) + for (const dir of allDirs) { + const modelDirPath = join(JanModelPlugin._homeDir, dir) + const isModelDir = await fs.isDirectory(modelDirPath) + if (!isModelDir) { + // if not a directory, ignore + continue + } + + const jsonFiles: string[] = (await fs.listFiles(modelDirPath)).filter( + (file: string) => file.endsWith('.json') + ) + + for (const json of jsonFiles) { + const model: Model = JSON.parse( + await fs.readFile(join(modelDirPath, json)) + ) + results.push(model) + } + } + + return results } + /** * Gets all available models. * @returns A Promise that resolves with an array of all models. @@ -96,10 +120,6 @@ export default class JanModelPlugin implements ModelPlugin { // Add a timestamp to the URL to prevent caching return import( /* webpackIgnore: true */ MODEL_CATALOG_URL + `?t=${Date.now()}` - ).then((module) => - module.default.map((e) => { - return parseToModel(e); - }) - ); + ).then((module) => module.default.map((e) => parseToModel(e))) } } diff --git a/plugins/model-plugin/webpack.config.js b/plugins/model-plugin/webpack.config.js index 60fa1a9b0..3475516ed 100644 --- a/plugins/model-plugin/webpack.config.js +++ b/plugins/model-plugin/webpack.config.js @@ -1,16 +1,16 @@ -const path = require("path"); -const webpack = require("webpack"); -const packageJson = require("./package.json"); +const path = require('path') +const webpack = require('webpack') +const packageJson = require('./package.json') module.exports = { experiments: { outputModule: true }, - entry: "./src/index.ts", // Adjust the entry point to match your project's main file - mode: "production", + entry: './src/index.ts', // Adjust the entry point to match your project's main file + mode: 'production', module: { rules: [ { test: /\.tsx?$/, - use: "ts-loader", + use: 'ts-loader', exclude: /node_modules/, }, ], @@ -20,20 +20,23 @@ module.exports = { PLUGIN_NAME: JSON.stringify(packageJson.name), MODULE_PATH: JSON.stringify(`${packageJson.name}/${packageJson.module}`), MODEL_CATALOG_URL: JSON.stringify( - "https://cdn.jsdelivr.net/npm/@janhq/models@latest/dist/index.js" + 'https://cdn.jsdelivr.net/npm/@janhq/models@latest/dist/index.js' ), }), ], output: { - filename: "index.js", // Adjust the output file name as needed - path: path.resolve(__dirname, "dist"), - library: { type: "module" }, // Specify ESM output format + filename: 'index.js', // Adjust the output file name as needed + path: path.resolve(__dirname, 'dist'), + library: { type: 'module' }, // Specify ESM output format }, resolve: { - extensions: [".ts", ".js"], + extensions: ['.ts', '.js'], + fallback: { + path: require.resolve('path-browserify'), + }, }, optimization: { minimize: false, }, // Add loaders and other configuration as needed for your project -}; +} diff --git a/web/containers/Layout/BottomBar/index.tsx b/web/containers/Layout/BottomBar/index.tsx index 9752888fa..9153da2c7 100644 --- a/web/containers/Layout/BottomBar/index.tsx +++ b/web/containers/Layout/BottomBar/index.tsx @@ -46,7 +46,7 @@ const BottomBar = () => { ⌘e to show your model ) } diff --git a/web/containers/Layout/TopBar/CommandListDownloadedModel/index.tsx b/web/containers/Layout/TopBar/CommandListDownloadedModel/index.tsx index 108aa8e82..fee918f3a 100644 --- a/web/containers/Layout/TopBar/CommandListDownloadedModel/index.tsx +++ b/web/containers/Layout/TopBar/CommandListDownloadedModel/index.tsx @@ -24,7 +24,7 @@ export default function CommandListDownloadedModel() { const { activeModel, startModel, stopModel } = useActiveModel() const onModelActionClick = (modelId: string) => { - if (activeModel && activeModel._id === modelId) { + if (activeModel && activeModel.id === modelId) { stopModel(modelId) } else { startModel(modelId) @@ -62,7 +62,7 @@ export default function CommandListDownloadedModel() { { - onModelActionClick(model._id) + onModelActionClick(model.id) setOpen(false) }} > @@ -72,7 +72,7 @@ export default function CommandListDownloadedModel() { />
{model.name} - {activeModel && activeModel._id === model._id && ( + {activeModel && activeModel.id === model.id && ( Active )}
diff --git a/web/containers/ModalCancelDownload/index.tsx b/web/containers/ModalCancelDownload/index.tsx index 2f753d705..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 f1b943427..2c2ad3bd7 100644 --- a/web/containers/Providers/EventHandler.tsx +++ b/web/containers/Providers/EventHandler.tsx @@ -1,10 +1,14 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { ReactNode, useEffect, useRef } from 'react' -import { events, EventName, NewMessageResponse, PluginType } from '@janhq/core' - +import { + events, + EventName, + ThreadMessage, + PluginType, + MessageStatus, +} from '@janhq/core' import { ConversationalPlugin, ModelPlugin } from '@janhq/core/lib/plugins' -import { Message } from '@janhq/core/lib/types' import { useAtomValue, useSetAtom } from 'jotai' import { useDownloadState } from '@/hooks/useDownloadState' @@ -16,22 +20,15 @@ import { updateMessageAtom, } from '@/helpers/atoms/ChatMessage.atom' import { - updateConversationAtom, updateConversationWaitingForResponseAtom, userConversationsAtom, } from '@/helpers/atoms/Conversation.atom' - import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom' -import { MessageStatus, toChatMessage } from '@/models/ChatMessage' import { pluginManager } from '@/plugin' -import { ChatMessage, Conversation } from '@/types/chatMessage' - -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() @@ -48,98 +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._id && - messageResponse.message + messageResponse.threadId && + messageResponse.id && + messageResponse.content ) { updateMessage( - messageResponse._id, - messageResponse.conversationId, - messageResponse.message, + messageResponse.id, + 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._id && - messageResponse.message + messageResponse.threadId && + messageResponse.id && + messageResponse.content ) { updateMessage( - messageResponse._id, - messageResponse.conversationId, - messageResponse.message, + messageResponse.id, + 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) => { - return { - // eslint-disable-next-line @typescript-eslint/naming-convention - _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, - // eslint-disable-next-line @typescript-eslint/naming-convention - _id: convo._id ?? '', - name: convo.name ?? '', - message: convo.lastMessage ?? '', - messages: messagesData, + ...thread, + id: thread.id ?? '', + messages: messagesRef.current[thread.id] ?? [], }) } } @@ -151,9 +105,9 @@ 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) + const model = models.find((e) => e.id === state.fileName) if (model) pluginManager .get(PluginType.Model) diff --git a/web/containers/Providers/EventListener.tsx b/web/containers/Providers/EventListener.tsx index 528cd254b..9d4c2d890 100644 --- a/web/containers/Providers/EventListener.tsx +++ b/web/containers/Providers/EventListener.tsx @@ -27,20 +27,28 @@ 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) { window.electronAPI.onFileDownloadUpdate( (_event: string, state: DownloadState | undefined) => { if (!state) return - setDownloadState(state) + setDownloadState({ + ...state, + fileName: state.fileName.split('/').pop() ?? '', + }) } ) window.electronAPI.onFileDownloadError( (_event: string, callback: any) => { - console.log('Download error', callback) - const fileName = callback.fileName.replace('models/', '') + console.error('Download error', callback) + const fileName = callback.fileName.split('/').pop() ?? '' setDownloadStateFailed(fileName) } ) @@ -48,16 +56,16 @@ export default function EventListenerWrapper({ children }: PropsWithChildren) { window.electronAPI.onFileDownloadSuccess( (_event: string, callback: any) => { if (callback && callback.fileName) { - const fileName = callback.fileName.replace('models/', '') + const fileName = callback.fileName.split('/').pop() ?? '' setDownloadStateSuccess(fileName) - const model = modelsRef.current.find((e) => e._id === fileName) + const model = modelsRef.current.find((e) => e.id === fileName) if (model) pluginManager .get(PluginType.Model) ?.saveModel(model) .then(() => { - setDownloadedModels([...downloadedModels, model]) + setDownloadedModels([...downloadedModelRef.current, model]) }) } } @@ -66,13 +74,13 @@ export default function EventListenerWrapper({ children }: PropsWithChildren) { window.electronAPI.onAppUpdateDownloadUpdate( (_event: string, progress: any) => { setProgress(progress.percent) - console.log('app update progress:', progress.percent) + console.debug('app update progress:', progress.percent) } ) window.electronAPI.onAppUpdateDownloadError( (_event: string, callback: any) => { - console.log('Download error', callback) + console.error('Download error', callback) setProgress(-1) } ) diff --git a/web/helpers/atoms/ChatMessage.atom.ts b/web/helpers/atoms/ChatMessage.atom.ts index c45808288..14ad95a80 100644 --- a/web/helpers/atoms/ChatMessage.atom.ts +++ b/web/helpers/atoms/ChatMessage.atom.ts @@ -1,18 +1,21 @@ +import { MessageStatus, ThreadMessage } from '@janhq/core' import { atom } from 'jotai' -import { getActiveConvoIdAtom } from './Conversation.atom' - -import { ChatMessage, MessageStatus } from '@/models/ChatMessage' +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] @@ -21,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 @@ -35,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 @@ -49,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 @@ -66,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] = [] @@ -102,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) } } ) @@ -131,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 3a661d385..60748e038 100644 --- a/web/helpers/atoms/Conversation.atom.ts +++ b/web/helpers/atoms/Conversation.atom.ts @@ -1,7 +1,7 @@ -import { Conversation, ConversationState } from '@/types/chatMessage' +import { Thread } from '@janhq/core' import { atom } from 'jotai' -// import { MainViewState, setMainViewStateAtom } from './MainView.atom' +import { ThreadState } from '@/types/conversation' /** * Stores the current active conversation id. @@ -21,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) @@ -75,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) => { - const id = conversation._id + (get, set, conversation: Thread) => { + const id = conversation.id if (!id) return - const convo = get(userConversationsAtom).find((c) => c._id === id) + 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 @@ -101,7 +109,7 @@ export const updateConversationAtom = atom( /** * Stores all conversations for the current user */ -export const userConversationsAtom = atom([]) -export const currentConversationAtom = atom((get) => - get(userConversationsAtom).find((c) => c._id === get(getActiveConvoIdAtom)) +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 a9d517fbf..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' @@ -21,16 +23,16 @@ export function useActiveModel() { const { downloadedModels } = useGetDownloadedModels() const startModel = async (modelId: string) => { - if (activeModel && activeModel._id === modelId) { + if (activeModel && activeModel.id === modelId) { console.debug(`Model ${modelId} is already init. Ignore..`) return } setStateModel({ state: 'start', loading: true, model: modelId }) - const model = await downloadedModels.find((e) => e._id === modelId) + const model = downloadedModels.find((e) => e.id === modelId) - if (!modelId) { + if (!model) { alert(`Model ${modelId} not found! Please re-download the model first.`) setStateModel(() => ({ state: 'start', @@ -42,8 +44,8 @@ export function useActiveModel() { const currentTime = Date.now() console.debug('Init model: ', modelId) - - const res = await initModel(`models/${modelId}`) + const path = join('models', model.name, modelId) + const res = await initModel(path) if (res?.error) { const errorMessage = `${res.error}` alert(errorMessage) diff --git a/web/hooks/useCreateConversation.ts b/web/hooks/useCreateConversation.ts index 755876c0a..8984c0bfc 100644 --- a/web/hooks/useCreateConversation.ts +++ b/web/hooks/useCreateConversation.ts @@ -1,7 +1,6 @@ import { PluginType } from '@janhq/core' +import { Thread, Model } from '@janhq/core' import { ConversationalPlugin } from '@janhq/core/lib/plugins' -import { Model } from '@janhq/core/lib/types' - import { useAtom, useSetAtom } from 'jotai' import { generateConversationId } from '@/utils/conversation' @@ -12,7 +11,6 @@ import { addNewConversationStateAtom, } from '@/helpers/atoms/Conversation.atom' import { pluginManager } from '@/plugin' -import { Conversation } from '@/types/chatMessage' export const useCreateConversation = () => { const [userConversations, setUserConversations] = useAtom( @@ -22,30 +20,26 @@ export const useCreateConversation = () => { const addNewConvoState = useSetAtom(addNewConversationStateAtom) const requestCreateConvo = async (model: Model) => { - const conversationName = model.name - const mappedConvo: Conversation = { - // eslint-disable-next-line @typescript-eslint/naming-convention - _id: generateConversationId(), - modelId: model._id, - name: conversationName, + const summary = model.name + const mappedConvo: Thread = { + id: generateConversationId(), + modelId: model.id, + summary, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), + messages: [], } - addNewConvoState(mappedConvo._id, { + addNewConvoState(mappedConvo.id, { hasMore: true, waitingForResponse: false, }) pluginManager .get(PluginType.Conversational) - ?.saveConversation({ - ...mappedConvo, - name: mappedConvo.name ?? '', - messages: [], - }) + ?.saveConversation(mappedConvo) setUserConversations([mappedConvo, ...userConversations]) - setActiveConvoId(mappedConvo._id) + setActiveConvoId(mappedConvo.id) } return { diff --git a/web/hooks/useDeleteConversation.ts b/web/hooks/useDeleteConversation.ts index 8826aab5b..459b527b5 100644 --- a/web/hooks/useDeleteConversation.ts +++ b/web/hooks/useDeleteConversation.ts @@ -41,7 +41,7 @@ export default function useDeleteConversation() { .get(PluginType.Conversational) ?.deleteConversation(activeConvoId) const currentConversations = userConversations.filter( - (c) => c._id !== activeConvoId + (c) => c.id !== activeConvoId ) setUserConversations(currentConversations) deleteMessages(activeConvoId) @@ -50,7 +50,7 @@ export default function useDeleteConversation() { description: `Delete chat with ${activeModel?.name} has been completed`, }) if (currentConversations.length > 0) { - setActiveConvoId(currentConversations[0]._id) + setActiveConvoId(currentConversations[0].id) } else { setActiveConvoId(undefined) } diff --git a/web/hooks/useDeleteModel.ts b/web/hooks/useDeleteModel.ts index 1980e4514..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' @@ -12,15 +14,14 @@ export default function useDeleteModel() { const { setDownloadedModels, downloadedModels } = useGetDownloadedModels() const deleteModel = async (model: Model) => { - await pluginManager - .get(PluginType.Model) - ?.deleteModel(model._id) + const path = join('models', model.name, model.id) + await pluginManager.get(PluginType.Model)?.deleteModel(path) // reload models - setDownloadedModels(downloadedModels.filter((e) => e._id !== model._id)) + setDownloadedModels(downloadedModels.filter((e) => e.id !== model.id)) toaster({ title: 'Delete a Model', - description: `Model ${model._id} has been deleted.`, + description: `Model ${model.id} has been deleted.`, }) } diff --git a/web/hooks/useDownloadModel.ts b/web/hooks/useDownloadModel.ts index 29a41333b..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 26a1c83d7..f7934ae2c 100644 --- a/web/hooks/useGetInputState.ts +++ b/web/hooks/useGetInputState.ts @@ -1,13 +1,12 @@ import { useEffect, useState } from 'react' -import { Model } from '@janhq/core/lib/types' +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' -import { Conversation } from '@/types/chatMessage' export default function useGetInputState() { const [inputState, setInputState] = useState('loading') @@ -16,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 @@ -27,7 +26,7 @@ export default function useGetInputState() { // check if convo model id is in downloaded models const isModelAvailable = downloadedModels.some( - (model) => model._id === convo.modelId + (model) => model.id === convo.modelId ) if (!isModelAvailable) { @@ -36,7 +35,7 @@ export default function useGetInputState() { return } - if (convo.modelId !== currentModel._id) { + if (convo.modelId !== currentModel.id) { // in case convo model and active model is different, // ask user to init the required model setInputState('model-mismatch') diff --git a/web/hooks/useGetUserConversations.ts b/web/hooks/useGetUserConversations.ts index 35e659ee2..3a6e4eb55 100644 --- a/web/hooks/useGetUserConversations.ts +++ b/web/hooks/useGetUserConversations.ts @@ -1,6 +1,5 @@ -import { PluginType } 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 { setConvoMessagesAtom } from '@/helpers/atoms/ChatMessage.atom' @@ -8,9 +7,8 @@ import { conversationStatesAtom, userConversationsAtom, } from '@/helpers/atoms/Conversation.atom' -import { toChatMessage } from '@/models/ChatMessage' import { pluginManager } from '@/plugin/PluginManager' -import { ChatMessage, ConversationState } from '@/types/chatMessage' +import { ThreadState } from '@/types/conversation' const useGetUserConversations = () => { const setConversationStates = useSetAtom(conversationStatesAtom) @@ -19,24 +17,22 @@ 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 ?? ''] = { + 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 ?? []) } catch (error) { - console.log(error) + console.error(error) } } diff --git a/web/hooks/useSendChatMessage.ts b/web/hooks/useSendChatMessage.ts index 256c701ef..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, } from '@janhq/core' - import { ConversationalPlugin, InferencePlugin } from '@janhq/core/lib/plugins' - -import { Message } from '@janhq/core/lib/types' 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, @@ -22,10 +25,7 @@ import { updateConversationAtom, updateConversationWaitingForResponseAtom, } from '@/helpers/atoms/Conversation.atom' -import { MessageSenderType, toChatMessage } from '@/models/ChatMessage' - import { pluginManager } from '@/plugin/PluginManager' -import { ChatMessage, Conversation } from '@/types/chatMessage' export default function useSendChatMessage() { const currentConvo = useAtomValue(currentConversationAtom) @@ -37,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) } @@ -48,18 +48,24 @@ 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 && result.message.split(' ').length <= 10 && - conv?._id + conv?.id ) { const updatedConv = { ...conv, @@ -70,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) @@ -87,61 +85,60 @@ export default function useSendChatMessage() { } const sendChatMessage = async () => { - const convoId = currentConvo?._id as string + 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 = { - // eslint-disable-next-line @typescript-eslint/naming-convention - _id: ulid(), - conversationId: convoId, - message: prompt, - user: MessageSenderType.User, - createdAt: new Date().toISOString(), - history: messageHistory, + const messageRequest: MessageRequest = { + id: ulid(), + 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/models/ChatMessage.ts b/web/models/ChatMessage.ts deleted file mode 100644 index b7f18d932..000000000 --- a/web/models/ChatMessage.ts +++ /dev/null @@ -1,84 +0,0 @@ -/* eslint-disable @typescript-eslint/naming-convention */ -import { NewMessageResponse } from '@janhq/core' -import { Message } from '@janhq/core/lib/types' - -export enum MessageType { - Text = 'Text', - Image = 'Image', - ImageWithText = 'ImageWithText', - Error = 'Error', -} - -export enum MessageSenderType { - Ai = 'assistant', - User = 'user', -} - -export enum MessageStatus { - Ready = 'ready', - Pending = 'pending', -} - -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 interface RawMessage { - _id?: string - conversationId?: string - user?: string - avatar?: string - message?: string - createdAt?: string - updatedAt?: string -} - -export const toChatMessage = ( - m: RawMessage | Message | NewMessageResponse, - - conversationId?: string -): ChatMessage => { - const createdAt = new Date(m.createdAt ?? '').getTime() - const imageUrls: string[] = [] - const imageUrl = undefined - if (imageUrl) { - imageUrls.push(imageUrl) - } - - const messageType = MessageType.Text - const messageSenderType = - m.user === 'user' ? MessageSenderType.User : MessageSenderType.Ai - - const content = m.message ?? '' - - const senderName = m.user === 'user' ? 'You' : 'Assistant' - - return { - id: (m._id ?? 0).toString(), - conversationId: ( - (m as RawMessage | NewMessageResponse)?.conversationId ?? - conversationId ?? - 0 - ).toString(), - messageType: messageType, - messageSenderType: messageSenderType, - senderUid: m.user?.toString() || '0', - senderName: senderName, - senderAvatarUrl: - m.user === 'user' ? 'icons/avatar.svg' : 'icons/app_icon.svg', - text: content, - imageUrls: imageUrls, - createdAt: createdAt, - status: MessageStatus.Ready, - } -} 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 6b4f0b3f8..a085c3dc8 100644 --- a/web/screens/Chat/ChatItem/index.tsx +++ b/web/screens/Chat/ChatItem/index.tsx @@ -1,25 +1,14 @@ import React, { forwardRef } from 'react' -import SimpleTextMessage from '../SimpleTextMessage' -import { ChatMessage } from '@/types/chatMessage' +import { ThreadMessage } from '@janhq/core' -type Props = { - message: ChatMessage -} +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 628023e0c..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,21 +43,21 @@ 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 } - const model = downloadedModels.find((e) => e._id === convo.modelId) + const model = downloadedModels.find((e) => e.id === convo.modelId) if (convo == null) { console.debug('modelId is undefined') return } if (model != null) { - startModel(model._id) + startModel(model.id) } - if (activeConvoId !== convo._id) { - setActiveConvoId(convo._id) + if (activeConvoId !== convo.id) { + setActiveConvoId(convo.id) } } @@ -83,24 +85,28 @@ 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 && ( + {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 (
@@ -71,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 285a2764e..5d7d22f37 100644 --- a/web/screens/Chat/index.tsx +++ b/web/screens/Chat/index.tsx @@ -56,7 +56,7 @@ const ChatScreen = () => { const conversations = useAtomValue(userConversationsAtom) const isEnableChat = (currentConvo && activeModel) || conversations.length > 0 const [isModelAvailable, setIsModelAvailable] = useState( - downloadedModels.some((x) => x._id === currentConvo?.modelId) + downloadedModels.some((x) => x.id === currentConvo?.modelId) ) const textareaRef = useRef(null) @@ -72,7 +72,7 @@ const ChatScreen = () => { useEffect(() => { setIsModelAvailable( - downloadedModels.some((x) => x._id === currentConvo?.modelId) + downloadedModels.some((x) => x.id === currentConvo?.modelId) ) }, [currentConvo, downloadedModels]) @@ -126,7 +126,7 @@ const ChatScreen = () => { {isEnableChat && currentConvo && (
- {currentConvo?.name ?? ''} + {currentConvo?.summary ?? ''}
{ disabled={ !activeModel || stateModel.loading || - activeModel._id !== currentConvo?.modelId + activeModel.id !== currentConvo?.modelId } value={currentPrompt} onChange={(e) => { diff --git a/web/screens/ExploreModels/ExploreModelItem/index.tsx b/web/screens/ExploreModels/ExploreModelItem/index.tsx index b13816d39..6682c6385 100644 --- a/web/screens/ExploreModels/ExploreModelItem/index.tsx +++ b/web/screens/ExploreModels/ExploreModelItem/index.tsx @@ -33,7 +33,7 @@ const ExploreModelItem = forwardRef(({ 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 fd4babfd3..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 = (