Merge branch 'main' into docs/openai-api

This commit is contained in:
Daniel 2023-11-19 04:20:02 +08:00 committed by GitHub
commit 6e571e8e7f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
124 changed files with 2368 additions and 1778 deletions

View File

@ -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 {} \;
# 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 {} \;

View File

@ -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 }}

View File

@ -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

View File

@ -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)
<p align="center">
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
@ -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:

View File

@ -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);
};

View File

@ -8,6 +8,21 @@ const writeFile: (path: string, data: string) => Promise<any> = (path, data) =>
window.coreAPI?.writeFile(path, data) ??
window.electronAPI?.writeFile(path, data);
/**
* Gets the user space path.
* @returns {Promise<any>} A Promise that resolves with the user space path.
*/
const getUserSpace = (): Promise<string> =>
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<boolean> =>
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<any> = (path) =>
window.coreAPI?.deleteFile(path) ?? window.electronAPI?.deleteFile(path);
export const fs = {
isDirectory,
getUserSpace,
writeFile,
readFile,
listFiles,

View File

@ -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";

View File

@ -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<void>} A promise that resolves when the conversation is saved.
*/
abstract saveConversation(conversation: Conversation): Promise<void>;
abstract saveConversation(conversation: Thread): Promise<void>;
/**
* Deletes a conversation.

View File

@ -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<any>;
abstract inferenceRequest(data: MessageRequest): Promise<any>;
}

View File

@ -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;
};

View File

@ -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?

View File

@ -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

View File

@ -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
```

View File

@ -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
```

View File

@ -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
```

View File

@ -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
```

View File

@ -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
```

View File

@ -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": ["<model_id>"],
"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
```

View File

@ -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

View File

@ -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
```

View File

@ -0,0 +1,4 @@
---
title: "Fine tuning"
---
Todo: @hiro

View File

@ -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"
}
```

View File

@ -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"
}
```

View File

@ -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

View File

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

View File

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 86 KiB

View File

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Before

Width:  |  Height:  |  Size: 186 KiB

After

Width:  |  Height:  |  Size: 186 KiB

View File

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

View File

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 85 KiB

View File

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

View File

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -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

View File

@ -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",

View File

@ -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: [

View File

@ -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 (
<div className="mt-2">
<a href={defaultSystem.href}>
<span className="text-blue-600 font-bold">Download Jan</span>
</a>
</div>
);
}

View File

@ -76,6 +76,7 @@ export default function Dropdown() {
setDefaultSystem(systems[1]);
}
};
useEffect(() => {
const updateDownloadLinks = async () => {
try {

View File

@ -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() {
<AnnoncementBanner />
<Layout
title={`${siteConfig.tagline}`}
description="Jan runs Large Language Models locally on Windows, Mac and Linux.
Available on Desktop and Cloud-Native."
description="Jan runs Large Language Models locally on Windows, Mac and Linux. Available on Desktop and Cloud-Native."
>
<main className="bg-gray-50 dark:bg-gray-950/95 relative">
<div className="relative">
@ -75,7 +45,6 @@ export default function Home() {
</p>
</a>
</div> */}
<h1 className="bg-gradient-to-r dark:from-white from-black to-gray-500 dark:to-gray-400 bg-clip-text text-4xl lg:text-6xl font-bold leading-tight text-transparent dark:text-transparent lg:leading-tight">
Own your AI
</h1>
@ -111,7 +80,6 @@ export default function Home() {
</div>
<div className="text-center relative ">
{/* <div className="el-blur-hero absolute -left-40 w-full top-1/2 -translate-y-1/2" /> */}
<div className="p-3 border dark:border-gray-500 border-gray-400 inline-block rounded-lg">
<ThemedImage
alt="App screenshot"
@ -127,28 +95,188 @@ export default function Home() {
</div>
</div>
</div>
<div className="container mt-10 mb-20 text-center">
<div className="container mt-10 mb-10 lg:mb-20 text-center">
<h2>AI that you control</h2>
<p className="text-base mt-2 w-full lg:w-2/5 mx-auto leading-relaxed">
Jan runs Large Language Models locally on Windows, Mac and Linux.
Available on Desktop and Cloud-Native.
Private. Local. Infinitely Customizable.
</p>
<div className="grid text-left lg:grid-cols-2 lg:px-48 mt-16 gap-16">
{features.map((feat, i) => {
return (
<div className="flex gap-x-4" key={i}>
<feat.icon
className="h-6 w-6 text-indigo-600 dark:text-indigo-400 flex-shrink-0"
aria-hidden="true"
/>
<div className="grid text-left lg:grid-cols-2 mt-16 gap-4">
<div className="card relative min-h-[380px] lg:min-h-[460px]">
<img
src="/img/card-element.png"
alt="Element"
className="absolute w-full bottom-0 left-0"
/>
<div class="p-8 relative z-40">
<h5>Personal AI that runs on your computer</h5>
<p className="mt-2">
Jan runs directly on your local machine, offering privacy,
convenience and customizability.
</p>
<ThemedImage
alt="Group Chat"
sources={{
light: useBaseUrl("/img/group-chat-light.png"),
dark: useBaseUrl("/img/group-chat-dark.png"),
}}
className="mt-10"
/>
</div>
</div>
<div className="card relative min-h-[380px] lg:min-h-[460px]">
<div className="p-8">
<h5>Extendable via App and Plugin framework</h5>
<p className="mt-2">
Jan has a versatile app and plugin framework, allowing you
to customize it to your needs.
</p>
</div>
<ThemedImage
alt="Framework"
sources={{
light: useBaseUrl("/img/card-framework-light.png"),
dark: useBaseUrl("/img/card-framework-dark.png"),
}}
className="w-11/12 ml-auto mt-auto"
/>
</div>
<div className="card relative min-h-[380px] lg:min-h-[460px]">
<div className="p-8">
<h5>
Private and offline, your data never leaves your machine
</h5>
<p className="mt-2">
Your conversations and data are with an AI that runs on your
computer, where only you have access.
</p>
</div>
<ThemedImage
alt="Group Chat"
sources={{
light: useBaseUrl("/img/card-nitro-light.png"),
dark: useBaseUrl("/img/card-nitro-dark.png"),
}}
className="w-3/4 mx-auto mt-auto"
/>
</div>
<div className="card relative min-h-[380px] lg:min-h-[460px]">
<div className="p-8">
<h5>No subscription fees, the AI runs on your computer</h5>
<p className="mt-2">
Say goodbye to monthly subscriptions or usage-based APIs.
Jan runs 100% free on your own hardware.
</p>
</div>
<ThemedImage
alt="Group Chat"
sources={{
light: useBaseUrl("/img/card-free-light.png"),
dark: useBaseUrl("/img/card-free-dark.png"),
}}
className="w-full mt-auto mx-auto"
/>
</div>
</div>
</div>
<div class="container lg:px-20 py-20 text-center lg:text-left">
<div class="flex flex-col lg:flex-row space-y-20 lg:space-y-0">
<div>
<h1 className="bg-gradient-to-r dark:from-white from-black to-gray-500 dark:to-gray-400 bg-clip-text text-4xl lg:text-6xl font-bold leading-tight text-transparent dark:text-transparent lg:leading-tight">
Your AI, forever.
</h1>
<p className="text-lg lg:text-2xl mt-2">
Apps come and go, but your AI and data should last.{" "}
</p>
<div class="w-full lg:w-3/4 mt-8">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-10 lg:gap-24">
<div>
<h6>{feat.name}</h6>
<p className="mt-2">{feat.desc}</p>
<img
src="/img/ic-park-solid-unlock.svg"
alt="Icon - Lock"
className="w-8 mb-4 mx-auto lg:mx-0"
/>
<p>
Jan uses open, standard and non-proprietary files stored
locally on your device.
</p>
</div>
<div>
<img
src="img/ic-baseline-control-camera.svg"
alt="Icon - Camera"
className="w-8 mb-4 mx-auto lg:mx-0"
/>
<p>
You have total control over your AI, which means you can
use Jan offline and switch to another app easily if you
want.
</p>
</div>
</div>
);
})}
</div>
</div>
<div className="w-full lg:w-80 text-center">
<ThemedImage
alt="App screenshot"
sources={{
light: useBaseUrl("/img/jan-icon-light.png"),
dark: useBaseUrl("/img/jan-icon-dark.png"),
}}
className="w-40 lg:w-full mx-auto"
/>
<p className="mt-1 font-bold">100% free on your own hardware</p>
<DownloadLink />
</div>
</div>
</div>
<div class="container pb-20 pt-10 text-center">
<h2>
We are open-source. <br /> Join Jan community.
</h2>
<div class="mt-14">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<a href="https://discord.com/invite/FTk2MvZwJH" target="_blank">
<div class="card h-52 relative flex items-center justify-center">
<div class="relative z-50">
<img
src="/img/discord-logo.png"
alt="Discord logo"
className="w-28"
/>
</div>
<div class="card-link card-link-bg dark:card-link-bg-dark absolute right-4 top-5">
Join our Discord
</div>
<ThemedImage
alt="Discord Element"
sources={{
light: useBaseUrl("/img/discord-element-light.png"),
dark: useBaseUrl("/img/discord-element-dark.png"),
}}
className="absolute"
/>
</div>
</a>
<a href="https://github.com/janhq/jan" target="_blank">
<div class="card h-52 relative flex items-center justify-center">
<div class="relative z-50">
<AiOutlineGithub className="text-8xl dark:text-white text-black" />
</div>
<div class="card-link card-link-bg dark:card-link-bg-dark absolute right-4 top-5">
View Github
</div>
<img
alt="Github Element"
src="/img/github-element-dark.png"
className="absolute left-8"
/>
</div>
</a>
</div>
</div>
</div>
</main>

31
docs/src/styles/card.scss Normal file
View File

@ -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;
}
}
}

View File

@ -6,3 +6,4 @@
@import "./tweaks.scss";
@import "./base.scss";
@import "./components.scss";
@import "./card.scss";

BIN
docs/static/img/card-element.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
docs/static/img/card-framework-dark.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

BIN
docs/static/img/card-framework-light.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

BIN
docs/static/img/card-free-dark.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

BIN
docs/static/img/card-free-light.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

BIN
docs/static/img/card-nitro-dark.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

BIN
docs/static/img/card-nitro-light.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

BIN
docs/static/img/discord-element-dark.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

BIN
docs/static/img/discord-logo.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
docs/static/img/github-element-dark.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
docs/static/img/group-chat-dark.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

BIN
docs/static/img/group-chat-light.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

View File

@ -0,0 +1,4 @@
<svg width="36" height="37" viewBox="0 0 36 37" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M23.31 8.41986L20.655 11.0599L18 8.41986L15.345 11.0599L12.69 8.41986L18 3.10986L23.31 8.41986ZM27.69 23.4199L25.05 20.7649L27.69 18.1099L25.05 15.4549L27.69 12.7999L33 18.1099L27.69 23.4199ZM12.69 27.7999L15.345 25.1599L18 27.7999L20.655 25.1599L23.31 27.7999L18 33.1099L12.69 27.7999ZM8.31 12.7999L10.95 15.4549L8.31 18.1099L10.95 20.7649L8.31 23.4199L3 18.1099L8.31 12.7999Z" fill="#3B82F6"/>
<path d="M18 22.6099C20.4853 22.6099 22.5 20.5951 22.5 18.1099C22.5 15.6246 20.4853 13.6099 18 13.6099C15.5147 13.6099 13.5 15.6246 13.5 18.1099C13.5 20.5951 15.5147 22.6099 18 22.6099Z" fill="#3B82F6"/>
</svg>

After

Width:  |  Height:  |  Size: 712 B

View File

@ -0,0 +1,10 @@
<svg width="36" height="37" viewBox="0 0 36 37" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_206_5233" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="3" y="1" width="30" height="34">
<path d="M29.25 16.6458H6.75C5.92157 16.6458 5.25 17.3173 5.25 18.1458V31.6458C5.25 32.4742 5.92157 33.1458 6.75 33.1458H29.25C30.0784 33.1458 30.75 32.4742 30.75 31.6458V18.1458C30.75 17.3173 30.0784 16.6458 29.25 16.6458Z" fill="white" stroke="white" stroke-width="3" stroke-linejoin="round"/>
<path d="M10.5 16.6097V10.6135C10.4963 6.76224 13.4423 3.53499 17.3145 3.14799C21.1868 2.76099 24.7253 5.34024 25.5 9.11424" stroke="white" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18 22.6099V27.1099" stroke="black" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
</mask>
<g mask="url(#mask0_206_5233)">
<path d="M0 0.109863H36V36.1099H0V0.109863Z" fill="#3B82F6"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 940 B

BIN
docs/static/img/jan-icon-dark.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
docs/static/img/jan-icon-light.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@ -13,6 +13,7 @@ module.exports = {
},
fontFamily: {
sans: [
"Inter",
"-apple-system",
"BlinkMacSystemFont",
"Segoe UI",

8
electron/.prettierrc Normal file
View File

@ -0,0 +1,8 @@
{
"semi": false,
"singleQuote": true,
"quoteProps": "consistent",
"trailingComma": "es5",
"endOfLine": "auto",
"plugins": ["prettier-plugin-tailwindcss"]
}

View File

@ -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)
})
}

View File

@ -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<string> => 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<boolean> => {
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<string> => {
ipcMain.handle('readFile', async (event, path: string): Promise<string> => {
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<void> => {
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<void> => {
ipcMain.handle('mkdir', async (event, path: string): Promise<void> => {
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<void> => {
ipcMain.handle('rmdir', async (event, path: string): Promise<void> => {
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<string[]> => {
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
})
}

View File

@ -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

View File

@ -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",
{

View File

@ -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()
}

View File

@ -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 {

View File

@ -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"

View File

@ -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),
})

View File

@ -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"
}

View File

@ -24,6 +24,7 @@
},
"dependencies": {
"@janhq/core": "file:../../core",
"path-browserify": "^1.0.1",
"ts-loader": "^9.5.0"
},
"engines": {

View File

@ -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<Conversation[]> {
async getConversations(): Promise<Thread[]> {
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<void> {
saveConversation(conversation: Thread): Promise<void> {
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<void> {
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<any> {
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<string[]> {
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-')))
})
}
}

View File

@ -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: {

View File

@ -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 <service@jan.ai>",
"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": []
}

View File

@ -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<Conversation[]> {
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<void> {
return this.writeMarkdownToFile(conversation);
}
/**
* Deletes a conversation with the specified ID.
* @param conversationId The ID of the conversation to delete.
*/
deleteConversation(conversationId: string): Promise<void> {
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<string[]> {
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<Conversation | undefined> {
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
);
}
}

View File

@ -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"]
}

View File

@ -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
};

View File

@ -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"
},

View File

@ -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<void>} A promise that resolves when the model is initialized.
*/
async initModel(modelFileName: string): Promise<void> {
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<any>} A promise that resolves with the inference response.
*/
async inferenceRequest(data: NewMessageRequest): Promise<any> {
async inferenceRequest(data: MessageRequest): Promise<any> {
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);
},
});

View File

@ -124,7 +124,7 @@ function killSubprocess(): Promise<void> {
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);
}

View File

@ -0,0 +1,8 @@
{
"semi": false,
"singleQuote": true,
"quoteProps": "consistent",
"trailingComma": "es5",
"endOfLine": "auto",
"plugins": ["prettier-plugin-tailwindcss"]
}

View File

@ -29,6 +29,7 @@
],
"dependencies": {
"@janhq/core": "file:../../core",
"path-browserify": "^1.0.1",
"ts-loader": "^9.5.0"
}
}

View File

@ -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<string> {
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;
}

View File

@ -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
}

View File

@ -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<void> {
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<void> {
return fs
.deleteFile(`models/${filePath}`)
.then(() => fs.deleteFile(`models/m-${filePath}.json`));
async deleteModel(filePath: string): Promise<void> {
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<void> {
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<Model[]> {
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<Model[]> {
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)))
}
}

View File

@ -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
};
}

View File

@ -46,7 +46,7 @@ const BottomBar = () => {
<SystemItem
name="Active model:"
value={
activeModel?._id || (
activeModel?.id || (
<Badge themes="secondary">e to show your model</Badge>
)
}

View File

@ -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() {
<CommandItem
key={i}
onSelect={() => {
onModelActionClick(model._id)
onModelActionClick(model.id)
setOpen(false)
}}
>
@ -72,7 +72,7 @@ export default function CommandListDownloadedModel() {
/>
<div className="flex w-full items-center justify-between">
<span>{model.name}</span>
{activeModel && activeModel._id === model._id && (
{activeModel && activeModel.id === model.id && (
<Badge themes="secondary">Active</Badge>
)}
</div>

View File

@ -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)

View File

@ -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<Message>(
(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<ConversationalPlugin>(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<ModelPlugin>(PluginType.Model)

View File

@ -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<ModelPlugin>(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)
}
)

View File

@ -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<Record<string, ChatMessage[]>>({})
export const chatMessages = atom<Record<string, ThreadMessage[]>>({})
/**
* Return the chat messages for the current active conversation
*/
export const getCurrentChatMessagesAtom = atom<ChatMessage[]>((get) => {
export const getCurrentChatMessagesAtom = atom<ThreadMessage[]>((get) => {
const activeConversationId = get(getActiveConvoIdAtom)
if (!activeConversationId) return []
const messages = get(chatMessages)[activeConversationId]
@ -21,11 +24,11 @@ export const getCurrentChatMessagesAtom = atom<ChatMessage[]>((get) => {
export const setCurrentChatMessagesAtom = atom(
null,
(get, set, messages: ChatMessage[]) => {
(get, set, messages: ThreadMessage[]) => {
const currentConvoId = get(getActiveConvoIdAtom)
if (!currentConvoId) return
const newData: Record<string, ChatMessage[]> = {
const newData: Record<string, ThreadMessage[]> = {
...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<string, ChatMessage[]> = {
(get, set, messages: ThreadMessage[], convoId: string) => {
const newData: Record<string, ThreadMessage[]> = {
...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<string, ChatMessage[]> = {
const newData: Record<string, ThreadMessage[]> = {
...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<string, ChatMessage[]> = {
const newData: Record<string, ThreadMessage[]> = {
...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<string, ChatMessage[]> = {
const newData: Record<string, ThreadMessage[]> = {
...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<string, ChatMessage[]> = {
const newData: Record<string, ThreadMessage[]> = {
...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<string, ChatMessage[]> = {
const newData: Record<string, ThreadMessage[]> = {
...get(chatMessages),
}
newData[currentConvoId] = currentMessages

View File

@ -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<boolean | undefined>(undefined)
/**
* Stores all conversation states for the current user
*/
export const conversationStatesAtom = atom<Record<string, ConversationState>>(
{}
)
export const currentConvoStateAtom = atom<ConversationState | undefined>(
(get) => {
const activeConvoId = get(activeConversationIdAtom)
if (!activeConvoId) {
console.debug('Active convo id is undefined')
return undefined
}
return get(conversationStatesAtom)[activeConvoId]
export const conversationStatesAtom = atom<Record<string, ThreadState>>({})
export const currentConvoStateAtom = atom<ThreadState | undefined>((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<Conversation[]>([])
export const currentConversationAtom = atom<Conversation | undefined>((get) =>
get(userConversationsAtom).find((c) => c._id === get(getActiveConvoIdAtom))
export const userConversationsAtom = atom<Thread[]>([])
export const currentConversationAtom = atom<Thread | undefined>((get) =>
get(userConversationsAtom).find((c) => c.id === get(getActiveConvoIdAtom))
)

View File

@ -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)

View File

@ -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<ConversationalPlugin>(PluginType.Conversational)
?.saveConversation({
...mappedConvo,
name: mappedConvo.name ?? '',
messages: [],
})
?.saveConversation(mappedConvo)
setUserConversations([mappedConvo, ...userConversations])
setActiveConvoId(mappedConvo._id)
setActiveConvoId(mappedConvo.id)
}
return {

View File

@ -41,7 +41,7 @@ export default function useDeleteConversation() {
.get<ConversationalPlugin>(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)
}

View File

@ -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<ModelPlugin>(PluginType.Model)
?.deleteModel(model._id)
const path = join('models', model.name, model.id)
await pluginManager.get<ModelPlugin>(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.`,
})
}

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