Merge branch 'main' into docs/openai-api
11
.github/scripts/auto-sign.sh
vendored
@ -1,5 +1,10 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
APP_PATH=${APP_PATH}
|
# Check if both APP_PATH and DEVELOPER_ID environment variables are set
|
||||||
DEVELOPER_ID=${DEVELOPER_ID}
|
if [[ -z "$APP_PATH" ]] || [[ -z "$DEVELOPER_ID" ]]; then
|
||||||
find $APP_PATH \( -type f -perm +111 -o -name "*.node" \) -exec codesign -s "$DEVELOPER_ID" --options=runtime {} \;
|
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 {} \;
|
||||||
|
|||||||
12
.github/workflows/jan-electron-build.yml
vendored
@ -60,14 +60,14 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
yarn build:core
|
yarn build:core
|
||||||
yarn install
|
yarn install
|
||||||
yarn build:plugins-darwin
|
yarn build:plugins
|
||||||
env:
|
env:
|
||||||
APP_PATH: "."
|
APP_PATH: "."
|
||||||
DEVELOPER_ID: ${{ secrets.DEVELOPER_ID }}
|
DEVELOPER_ID: ${{ secrets.DEVELOPER_ID }}
|
||||||
|
|
||||||
- name: Build and publish app
|
- name: Build and publish app
|
||||||
run: |
|
run: |
|
||||||
yarn build:publish-darwin
|
yarn build:publish
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
CSC_LINK: "/tmp/codesign.p12"
|
CSC_LINK: "/tmp/codesign.p12"
|
||||||
@ -122,11 +122,11 @@ jobs:
|
|||||||
yarn build:core
|
yarn build:core
|
||||||
yarn install
|
yarn install
|
||||||
$env:NITRO_VERSION = Get-Content .\plugins\inference-plugin\nitro\version.txt; echo $env:NITRO_VERSION
|
$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
|
- name: Build and publish app
|
||||||
run: |
|
run: |
|
||||||
yarn build:publish-win32
|
yarn build:publish
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
@ -178,11 +178,11 @@ jobs:
|
|||||||
yarn config set network-timeout 300000
|
yarn config set network-timeout 300000
|
||||||
yarn build:core
|
yarn build:core
|
||||||
yarn install
|
yarn install
|
||||||
yarn build:plugins-linux
|
yarn build:plugins
|
||||||
|
|
||||||
- name: Build and publish app
|
- name: Build and publish app
|
||||||
run: |
|
run: |
|
||||||
yarn build:publish-linux
|
yarn build:publish
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
|||||||
@ -95,8 +95,8 @@ jobs:
|
|||||||
yarn build:core
|
yarn build:core
|
||||||
yarn install
|
yarn install
|
||||||
$env:NITRO_VERSION = Get-Content .\plugins\inference-plugin\nitro\version.txt; echo $env:NITRO_VERSION
|
$env:NITRO_VERSION = Get-Content .\plugins\inference-plugin\nitro\version.txt; echo $env:NITRO_VERSION
|
||||||
yarn build:plugins-win32
|
yarn build:plugins
|
||||||
yarn build:test-win32
|
yarn build:test
|
||||||
$env:CI="e2e"
|
$env:CI="e2e"
|
||||||
yarn test
|
yarn test
|
||||||
|
|
||||||
@ -131,6 +131,6 @@ jobs:
|
|||||||
yarn config set network-timeout 300000
|
yarn config set network-timeout 300000
|
||||||
yarn build:core
|
yarn build:core
|
||||||
yarn install
|
yarn install
|
||||||
yarn build:plugins-linux
|
yarn build:plugins
|
||||||
yarn build:test-linux
|
yarn build:test
|
||||||
yarn test
|
yarn test
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# Jan - Personal AI
|
# Jan - Own Your AI
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
<!-- 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 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:
|
**Jan runs on any hardware.** From PCs to multi-GPU clusters, Jan supports universal architectures:
|
||||||
|
|
||||||
|
|||||||
@ -12,44 +12,16 @@ export enum EventName {
|
|||||||
OnDownloadError = "onDownloadError",
|
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.
|
* Adds an observer for an event.
|
||||||
*
|
*
|
||||||
* @param eventName The name of the event to observe.
|
* @param eventName The name of the event to observe.
|
||||||
* @param handler The handler function to call when the event is observed.
|
* @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);
|
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 eventName The name of the event to stop observing.
|
||||||
* @param handler The handler function to call when the event is observed.
|
* @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);
|
window.corePlugin?.events?.off(eventName, handler);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,21 @@ const writeFile: (path: string, data: string) => Promise<any> = (path, data) =>
|
|||||||
window.coreAPI?.writeFile(path, data) ??
|
window.coreAPI?.writeFile(path, data) ??
|
||||||
window.electronAPI?.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.
|
* Reads the contents of a file at the specified path.
|
||||||
* @param {string} path - The path of the file to read.
|
* @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);
|
window.coreAPI?.deleteFile(path) ?? window.electronAPI?.deleteFile(path);
|
||||||
|
|
||||||
export const fs = {
|
export const fs = {
|
||||||
|
isDirectory,
|
||||||
|
getUserSpace,
|
||||||
writeFile,
|
writeFile,
|
||||||
readFile,
|
readFile,
|
||||||
listFiles,
|
listFiles,
|
||||||
|
|||||||
@ -20,12 +20,9 @@ export { events } from "./events";
|
|||||||
* Events types exports.
|
* Events types exports.
|
||||||
* @module
|
* @module
|
||||||
*/
|
*/
|
||||||
export {
|
export * from "./events";
|
||||||
EventName,
|
|
||||||
NewMessageRequest,
|
export * from "./types/index";
|
||||||
NewMessageResponse,
|
|
||||||
MessageHistory,
|
|
||||||
} from "./events";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filesystem module exports.
|
* Filesystem module exports.
|
||||||
@ -37,4 +34,4 @@ export { fs } from "./fs";
|
|||||||
* Plugin base module export.
|
* Plugin base module export.
|
||||||
* @module
|
* @module
|
||||||
*/
|
*/
|
||||||
export { JanPlugin, PluginType } from "./plugin";
|
export * from "./plugin";
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
|
import { Thread } from "../index";
|
||||||
import { JanPlugin } from "../plugin";
|
import { JanPlugin } from "../plugin";
|
||||||
import { Conversation } from "../types/index";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract class for conversational plugins.
|
* Abstract class for conversational plugins.
|
||||||
@ -17,10 +17,10 @@ export abstract class ConversationalPlugin extends JanPlugin {
|
|||||||
/**
|
/**
|
||||||
* Saves a conversation.
|
* Saves a conversation.
|
||||||
* @abstract
|
* @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.
|
* @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.
|
* Deletes a conversation.
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { NewMessageRequest } from "../events";
|
import { MessageRequest } from "../index";
|
||||||
import { JanPlugin } from "../plugin";
|
import { JanPlugin } from "../plugin";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -21,5 +21,5 @@ export abstract class InferencePlugin extends JanPlugin {
|
|||||||
* @param data - The data for the inference request.
|
* @param data - The data for the inference request.
|
||||||
* @returns The result of the inference request.
|
* @returns The result of the inference request.
|
||||||
*/
|
*/
|
||||||
abstract inferenceRequest(data: NewMessageRequest): Promise<any>;
|
abstract inferenceRequest(data: MessageRequest): Promise<any>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,91 +1,183 @@
|
|||||||
export interface Conversation {
|
/**
|
||||||
_id: string;
|
* Message Request and Response
|
||||||
modelId?: string;
|
* ============================
|
||||||
botId?: string;
|
* */
|
||||||
name: string;
|
|
||||||
message?: string;
|
/**
|
||||||
summary?: string;
|
* The role of the author of this message.
|
||||||
createdAt?: string;
|
* @data_transfer_object
|
||||||
updatedAt?: string;
|
*/
|
||||||
messages: Message[];
|
export enum ChatCompletionRole {
|
||||||
}
|
System = "system",
|
||||||
export interface Message {
|
Assistant = "assistant",
|
||||||
message?: string;
|
User = "user",
|
||||||
user?: string;
|
|
||||||
_id: string;
|
|
||||||
createdAt?: string;
|
|
||||||
updatedAt?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
export interface Model {
|
||||||
/**
|
/** Combination of owner and model name.*/
|
||||||
* Combination of owner and model name.
|
id: string;
|
||||||
* Being used as file name. MUST be unique.
|
/** The name of the model.*/
|
||||||
*/
|
|
||||||
_id: string;
|
|
||||||
name: string;
|
name: string;
|
||||||
quantMethod: string;
|
/** Quantization method name.*/
|
||||||
|
quantizationName: string;
|
||||||
|
/** The the number of bits represents a number.*/
|
||||||
bits: number;
|
bits: number;
|
||||||
|
/** The size of the model file in bytes.*/
|
||||||
size: number;
|
size: number;
|
||||||
|
/** The maximum RAM required to run the model in bytes.*/
|
||||||
maxRamRequired: number;
|
maxRamRequired: number;
|
||||||
|
/** The use case of the model.*/
|
||||||
usecase: string;
|
usecase: string;
|
||||||
|
/** The download link of the model.*/
|
||||||
downloadLink: string;
|
downloadLink: string;
|
||||||
modelFile?: string;
|
/** The short description of the model.*/
|
||||||
/**
|
|
||||||
* For tracking download info
|
|
||||||
*/
|
|
||||||
startDownloadAt?: number;
|
|
||||||
finishDownloadAt?: number;
|
|
||||||
productId: string;
|
|
||||||
productName: string;
|
|
||||||
shortDescription: string;
|
shortDescription: string;
|
||||||
|
/** The long description of the model.*/
|
||||||
longDescription: string;
|
longDescription: string;
|
||||||
|
/** The avatar url of the model.*/
|
||||||
avatarUrl: string;
|
avatarUrl: string;
|
||||||
|
/** The author name of the model.*/
|
||||||
author: string;
|
author: string;
|
||||||
|
/** The version of the model.*/
|
||||||
version: string;
|
version: string;
|
||||||
|
/** The origin url of the model repo.*/
|
||||||
modelUrl: string;
|
modelUrl: string;
|
||||||
createdAt: number;
|
/** The timestamp indicating when this model was released.*/
|
||||||
updatedAt?: number;
|
|
||||||
status: string;
|
|
||||||
releaseDate: number;
|
releaseDate: number;
|
||||||
|
/** The tags attached to the model description */
|
||||||
tags: string[];
|
tags: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model type of the presentation object which will be presented to the user
|
||||||
|
* @data_transfer_object
|
||||||
|
*/
|
||||||
export interface ModelCatalog {
|
export interface ModelCatalog {
|
||||||
_id: string;
|
/** The unique id of the model.*/
|
||||||
|
id: string;
|
||||||
|
/** The name of the model.*/
|
||||||
name: string;
|
name: string;
|
||||||
shortDescription: string;
|
/** The avatar url of the model.*/
|
||||||
avatarUrl: string;
|
avatarUrl: string;
|
||||||
|
/** The short description of the model.*/
|
||||||
|
shortDescription: string;
|
||||||
|
/** The long description of the model.*/
|
||||||
longDescription: string;
|
longDescription: string;
|
||||||
|
/** The author name of the model.*/
|
||||||
author: string;
|
author: string;
|
||||||
|
/** The version of the model.*/
|
||||||
version: string;
|
version: string;
|
||||||
|
/** The origin url of the model repo.*/
|
||||||
modelUrl: string;
|
modelUrl: string;
|
||||||
createdAt: number;
|
/** The timestamp indicating when this model was released.*/
|
||||||
updatedAt?: number;
|
|
||||||
status: string;
|
|
||||||
releaseDate: number;
|
releaseDate: number;
|
||||||
|
/** The tags attached to the model description **/
|
||||||
tags: string[];
|
tags: string[];
|
||||||
|
|
||||||
|
/** The available versions of this model to download. */
|
||||||
availableVersions: ModelVersion[];
|
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 = {
|
export type ModelVersion = {
|
||||||
/**
|
/** The name of this model version.*/
|
||||||
* Combination of owner and model name.
|
|
||||||
* Being used as file name. Should be unique.
|
|
||||||
*/
|
|
||||||
_id: string;
|
|
||||||
name: string;
|
name: string;
|
||||||
quantMethod: string;
|
/** The quantization method name.*/
|
||||||
|
quantizationName: string;
|
||||||
|
/** The the number of bits represents a number.*/
|
||||||
bits: number;
|
bits: number;
|
||||||
|
/** The size of the model file in bytes.*/
|
||||||
size: number;
|
size: number;
|
||||||
|
/** The maximum RAM required to run the model in bytes.*/
|
||||||
maxRamRequired: number;
|
maxRamRequired: number;
|
||||||
|
/** The use case of the model.*/
|
||||||
usecase: string;
|
usecase: string;
|
||||||
|
/** The download link of the model.*/
|
||||||
downloadLink: string;
|
downloadLink: string;
|
||||||
productId: string;
|
|
||||||
/**
|
|
||||||
* For tracking download state
|
|
||||||
*/
|
|
||||||
startDownloadAt?: number;
|
|
||||||
finishDownloadAt?: number;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,11 +2,11 @@
|
|||||||
title: About Jan
|
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.
|
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?
|
## Why does Jan Exist?
|
||||||
|
|
||||||
|
|||||||
@ -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
|
|
||||||
@ -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
|
|
||||||
```
|
|
||||||
@ -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
|
|
||||||
```
|
|
||||||
@ -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
|
|
||||||
```
|
|
||||||
@ -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
|
|
||||||
```
|
|
||||||
40
docs/docs/docs/specs/architecture.md
Normal 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
|
||||||
|
```
|
||||||
240
docs/docs/docs/specs/assistants.md
Normal 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
|
||||||
|
```
|
||||||
16
docs/docs/docs/specs/chats.md
Normal 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
|
||||||
@ -2,6 +2,12 @@
|
|||||||
title: "Files"
|
title: "Files"
|
||||||
---
|
---
|
||||||
|
|
||||||
|
:::warning
|
||||||
|
|
||||||
|
Draft Specification: functionality has not been implemented yet.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
Files can be used by `threads`, `assistants` and `fine-tuning`
|
Files can be used by `threads`, `assistants` and `fine-tuning`
|
||||||
|
|
||||||
> Equivalent to: https://platform.openai.com/docs/api-reference/files
|
> 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
|
## 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
|
## Files Filesystem
|
||||||
|
|
||||||
@ -39,5 +59,4 @@ Files can be used by `threads`, `assistants` and `fine-tuning`
|
|||||||
/threads
|
/threads
|
||||||
/jan-12938912
|
/jan-12938912
|
||||||
/files # thread-specific files
|
/files # thread-specific files
|
||||||
|
|
||||||
```
|
```
|
||||||
4
docs/docs/docs/specs/fine-tuning.md
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Fine tuning"
|
||||||
|
---
|
||||||
|
Todo: @hiro
|
||||||
178
docs/docs/docs/specs/messages.md
Normal 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"
|
||||||
|
}
|
||||||
|
```
|
||||||
372
docs/docs/docs/specs/models.md
Normal 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"
|
||||||
|
}
|
||||||
|
```
|
||||||
193
docs/docs/docs/specs/threads.md
Normal 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
|
||||||
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 186 KiB After Width: | Height: | Size: 186 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
@ -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
|
|
||||||
@ -138,13 +138,7 @@ const config = {
|
|||||||
src: "img/logo.svg",
|
src: "img/logo.svg",
|
||||||
},
|
},
|
||||||
items: [
|
items: [
|
||||||
// Navbar left
|
// Navbar Left
|
||||||
{
|
|
||||||
type: "docSidebar",
|
|
||||||
sidebarId: "guidesSidebar",
|
|
||||||
position: "left",
|
|
||||||
label: "User Guide",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
type: "docSidebar",
|
type: "docSidebar",
|
||||||
sidebarId: "docsSidebar",
|
sidebarId: "docsSidebar",
|
||||||
|
|||||||
@ -29,39 +29,52 @@ const sidebars = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
guidesSidebar: [
|
docsSidebar: [
|
||||||
"guides/overview",
|
{
|
||||||
|
type: "category",
|
||||||
|
label: "Getting Started",
|
||||||
|
collapsible: false,
|
||||||
|
collapsed: false,
|
||||||
|
items: [
|
||||||
|
"docs/introduction",
|
||||||
{
|
{
|
||||||
type: "category",
|
type: "category",
|
||||||
label: "Installation",
|
label: "Installation",
|
||||||
collapsible: true,
|
collapsible: true,
|
||||||
collapsed: false,
|
collapsed: true,
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
type: "autogenerated",
|
type: "autogenerated",
|
||||||
dirName: "guides/install",
|
dirName: "getting-started/install",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
"guides/troubleshooting",
|
|
||||||
],
|
|
||||||
|
|
||||||
docsSidebar: [
|
|
||||||
"docs/introduction",
|
|
||||||
"docs/quickstart",
|
"docs/quickstart",
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: "category",
|
type: "category",
|
||||||
label: "Modules",
|
label: "Building Jan",
|
||||||
collapsible: true,
|
collapsible: false,
|
||||||
collapsed: false,
|
collapsed: false,
|
||||||
items: [
|
items: [
|
||||||
|
"docs/user-interface",
|
||||||
{
|
{
|
||||||
type: "autogenerated",
|
type: "category",
|
||||||
dirName: "docs/modules",
|
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: [
|
apiSidebar: [
|
||||||
|
|||||||
127
docs/src/components/Elements/downloadLink.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -76,6 +76,7 @@ export default function Dropdown() {
|
|||||||
setDefaultSystem(systems[1]);
|
setDefaultSystem(systems[1]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const updateDownloadLinks = async () => {
|
const updateDownloadLinks = async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -5,41 +5,12 @@ import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
|
|||||||
import useBaseUrl from "@docusaurus/useBaseUrl";
|
import useBaseUrl from "@docusaurus/useBaseUrl";
|
||||||
import Layout from "@theme/Layout";
|
import Layout from "@theme/Layout";
|
||||||
import AnnoncementBanner from "@site/src/components/Announcement";
|
import AnnoncementBanner from "@site/src/components/Announcement";
|
||||||
import {
|
|
||||||
CloudArrowUpIcon,
|
import { AiOutlineGithub } from "react-icons/ai";
|
||||||
CursorArrowRaysIcon,
|
|
||||||
ShieldCheckIcon,
|
|
||||||
CpuChipIcon,
|
|
||||||
ClipboardDocumentIcon,
|
|
||||||
CubeTransparentIcon,
|
|
||||||
ComputerDesktopIcon,
|
|
||||||
FolderPlusIcon,
|
|
||||||
} from "@heroicons/react/24/outline";
|
|
||||||
|
|
||||||
import ThemedImage from "@theme/ThemedImage";
|
import ThemedImage from "@theme/ThemedImage";
|
||||||
|
|
||||||
const features = [
|
import DownloadLink from "@site/src/components/Elements/downloadLink";
|
||||||
{
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const { siteConfig } = useDocusaurusContext();
|
const { siteConfig } = useDocusaurusContext();
|
||||||
@ -48,8 +19,7 @@ export default function Home() {
|
|||||||
<AnnoncementBanner />
|
<AnnoncementBanner />
|
||||||
<Layout
|
<Layout
|
||||||
title={`${siteConfig.tagline}`}
|
title={`${siteConfig.tagline}`}
|
||||||
description="Jan runs Large Language Models locally on Windows, Mac and Linux.
|
description="Jan runs Large Language Models locally on Windows, Mac and Linux. Available on Desktop and Cloud-Native."
|
||||||
Available on Desktop and Cloud-Native."
|
|
||||||
>
|
>
|
||||||
<main className="bg-gray-50 dark:bg-gray-950/95 relative">
|
<main className="bg-gray-50 dark:bg-gray-950/95 relative">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
@ -75,7 +45,6 @@ export default function Home() {
|
|||||||
</p>
|
</p>
|
||||||
</a>
|
</a>
|
||||||
</div> */}
|
</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">
|
<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
|
Own your AI
|
||||||
</h1>
|
</h1>
|
||||||
@ -111,7 +80,6 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-center relative ">
|
<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">
|
<div className="p-3 border dark:border-gray-500 border-gray-400 inline-block rounded-lg">
|
||||||
<ThemedImage
|
<ThemedImage
|
||||||
alt="App screenshot"
|
alt="App screenshot"
|
||||||
@ -127,28 +95,188 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="container mt-10 mb-10 lg:mb-20 text-center">
|
||||||
<div className="container mt-10 mb-20 text-center">
|
|
||||||
<h2>AI that you control</h2>
|
<h2>AI that you control</h2>
|
||||||
<p className="text-base mt-2 w-full lg:w-2/5 mx-auto leading-relaxed">
|
<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.
|
Private. Local. Infinitely Customizable.
|
||||||
Available on Desktop and Cloud-Native.
|
|
||||||
</p>
|
</p>
|
||||||
<div className="grid text-left lg:grid-cols-2 lg:px-48 mt-16 gap-16">
|
<div className="grid text-left lg:grid-cols-2 mt-16 gap-4">
|
||||||
{features.map((feat, i) => {
|
<div className="card relative min-h-[380px] lg:min-h-[460px]">
|
||||||
return (
|
<img
|
||||||
<div className="flex gap-x-4" key={i}>
|
src="/img/card-element.png"
|
||||||
<feat.icon
|
alt="Element"
|
||||||
className="h-6 w-6 text-indigo-600 dark:text-indigo-400 flex-shrink-0"
|
className="absolute w-full bottom-0 left-0"
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
/>
|
||||||
|
<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>
|
<div>
|
||||||
<h6>{feat.name}</h6>
|
<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">
|
||||||
<p className="mt-2">{feat.desc}</p>
|
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>
|
||||||
|
<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>
|
||||||
})}
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
31
docs/src/styles/card.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,3 +6,4 @@
|
|||||||
@import "./tweaks.scss";
|
@import "./tweaks.scss";
|
||||||
@import "./base.scss";
|
@import "./base.scss";
|
||||||
@import "./components.scss";
|
@import "./components.scss";
|
||||||
|
@import "./card.scss";
|
||||||
|
|||||||
BIN
docs/static/img/card-element.png
vendored
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
docs/static/img/card-framework-dark.png
vendored
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
docs/static/img/card-framework-light.png
vendored
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
docs/static/img/card-free-dark.png
vendored
Normal file
|
After Width: | Height: | Size: 236 KiB |
BIN
docs/static/img/card-free-light.png
vendored
Normal file
|
After Width: | Height: | Size: 181 KiB |
BIN
docs/static/img/card-nitro-dark.png
vendored
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
docs/static/img/card-nitro-light.png
vendored
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
docs/static/img/discord-element-dark.png
vendored
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
docs/static/img/discord-element-light.png
vendored
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
BIN
docs/static/img/discord-logo.png
vendored
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
docs/static/img/github-element-dark.png
vendored
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
docs/static/img/group-chat-dark.png
vendored
Normal file
|
After Width: | Height: | Size: 145 KiB |
BIN
docs/static/img/group-chat-light.png
vendored
Normal file
|
After Width: | Height: | Size: 139 KiB |
4
docs/static/img/ic-baseline-control-camera.svg
vendored
Normal 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 |
10
docs/static/img/ic-park-solid-unlock.svg
vendored
Normal 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
|
After Width: | Height: | Size: 28 KiB |
BIN
docs/static/img/jan-icon-light.png
vendored
Normal file
|
After Width: | Height: | Size: 30 KiB |
@ -13,6 +13,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: [
|
sans: [
|
||||||
|
"Inter",
|
||||||
"-apple-system",
|
"-apple-system",
|
||||||
"BlinkMacSystemFont",
|
"BlinkMacSystemFont",
|
||||||
"Segoe UI",
|
"Segoe UI",
|
||||||
|
|||||||
8
electron/.prettierrc
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"quoteProps": "consistent",
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"endOfLine": "auto",
|
||||||
|
"plugins": ["prettier-plugin-tailwindcss"]
|
||||||
|
}
|
||||||
@ -1,10 +1,10 @@
|
|||||||
import { app, ipcMain } from "electron";
|
import { app, ipcMain } from 'electron'
|
||||||
import { DownloadManager } from "../managers/download";
|
import { DownloadManager } from '../managers/download'
|
||||||
import { resolve, join } from "path";
|
import { resolve, join } from 'path'
|
||||||
import { WindowManager } from "../managers/window";
|
import { WindowManager } from '../managers/window'
|
||||||
import request from "request";
|
import request from 'request'
|
||||||
import { createWriteStream, unlink } from "fs";
|
import { createWriteStream, unlink } from 'fs'
|
||||||
const progress = require("request-progress");
|
const progress = require('request-progress')
|
||||||
|
|
||||||
export function handleDownloaderIPCs() {
|
export function handleDownloaderIPCs() {
|
||||||
/**
|
/**
|
||||||
@ -12,18 +12,18 @@ export function handleDownloaderIPCs() {
|
|||||||
* @param _event - The IPC event object.
|
* @param _event - The IPC event object.
|
||||||
* @param fileName - The name of the file being downloaded.
|
* @param fileName - The name of the file being downloaded.
|
||||||
*/
|
*/
|
||||||
ipcMain.handle("pauseDownload", async (_event, fileName) => {
|
ipcMain.handle('pauseDownload', async (_event, fileName) => {
|
||||||
DownloadManager.instance.networkRequests[fileName]?.pause();
|
DownloadManager.instance.networkRequests[fileName]?.pause()
|
||||||
});
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the "resumeDownload" IPC message by resuming the download associated with the provided fileName.
|
* Handles the "resumeDownload" IPC message by resuming the download associated with the provided fileName.
|
||||||
* @param _event - The IPC event object.
|
* @param _event - The IPC event object.
|
||||||
* @param fileName - The name of the file being downloaded.
|
* @param fileName - The name of the file being downloaded.
|
||||||
*/
|
*/
|
||||||
ipcMain.handle("resumeDownload", async (_event, fileName) => {
|
ipcMain.handle('resumeDownload', async (_event, fileName) => {
|
||||||
DownloadManager.instance.networkRequests[fileName]?.resume();
|
DownloadManager.instance.networkRequests[fileName]?.resume()
|
||||||
});
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the "abortDownload" IPC message by aborting the download associated with the provided fileName.
|
* 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 _event - The IPC event object.
|
||||||
* @param fileName - The name of the file being downloaded.
|
* @param fileName - The name of the file being downloaded.
|
||||||
*/
|
*/
|
||||||
ipcMain.handle("abortDownload", async (_event, fileName) => {
|
ipcMain.handle('abortDownload', async (_event, fileName) => {
|
||||||
const rq = DownloadManager.instance.networkRequests[fileName];
|
const rq = DownloadManager.instance.networkRequests[fileName]
|
||||||
DownloadManager.instance.networkRequests[fileName] = undefined;
|
DownloadManager.instance.networkRequests[fileName] = undefined
|
||||||
const userDataPath = app.getPath("userData");
|
const userDataPath = app.getPath('userData')
|
||||||
const fullPath = join(userDataPath, fileName);
|
const fullPath = join(userDataPath, fileName)
|
||||||
rq?.abort();
|
rq?.abort()
|
||||||
let result = "NULL";
|
let result = 'NULL'
|
||||||
unlink(fullPath, function (err) {
|
unlink(fullPath, function (err) {
|
||||||
if (err && err.code == "ENOENT") {
|
if (err && err.code == 'ENOENT') {
|
||||||
result = `File not exist: ${err}`;
|
result = `File not exist: ${err}`
|
||||||
} else if (err) {
|
} else if (err) {
|
||||||
result = `File delete error: ${err}`;
|
result = `File delete error: ${err}`
|
||||||
} else {
|
} 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.
|
* Downloads a file from a given URL.
|
||||||
@ -56,51 +58,51 @@ export function handleDownloaderIPCs() {
|
|||||||
* @param url - The URL to download the file from.
|
* @param url - The URL to download the file from.
|
||||||
* @param fileName - The name to give the downloaded file.
|
* @param fileName - The name to give the downloaded file.
|
||||||
*/
|
*/
|
||||||
ipcMain.handle("downloadFile", async (_event, url, fileName) => {
|
ipcMain.handle('downloadFile', async (_event, url, fileName) => {
|
||||||
const userDataPath = app.getPath("userData");
|
const userDataPath = join(app.getPath('home'), 'jan')
|
||||||
const destination = resolve(userDataPath, fileName);
|
const destination = resolve(userDataPath, fileName)
|
||||||
const rq = request(url);
|
const rq = request(url)
|
||||||
|
|
||||||
progress(rq, {})
|
progress(rq, {})
|
||||||
.on("progress", function (state: any) {
|
.on('progress', function (state: any) {
|
||||||
WindowManager?.instance.currentWindow?.webContents.send(
|
WindowManager?.instance.currentWindow?.webContents.send(
|
||||||
"FILE_DOWNLOAD_UPDATE",
|
'FILE_DOWNLOAD_UPDATE',
|
||||||
{
|
{
|
||||||
...state,
|
...state,
|
||||||
fileName,
|
fileName,
|
||||||
}
|
}
|
||||||
);
|
)
|
||||||
})
|
})
|
||||||
.on("error", function (err: Error) {
|
.on('error', function (err: Error) {
|
||||||
WindowManager?.instance.currentWindow?.webContents.send(
|
WindowManager?.instance.currentWindow?.webContents.send(
|
||||||
"FILE_DOWNLOAD_ERROR",
|
'FILE_DOWNLOAD_ERROR',
|
||||||
{
|
{
|
||||||
fileName,
|
fileName,
|
||||||
err,
|
err,
|
||||||
}
|
}
|
||||||
);
|
)
|
||||||
})
|
})
|
||||||
.on("end", function () {
|
.on('end', function () {
|
||||||
if (DownloadManager.instance.networkRequests[fileName]) {
|
if (DownloadManager.instance.networkRequests[fileName]) {
|
||||||
WindowManager?.instance.currentWindow?.webContents.send(
|
WindowManager?.instance.currentWindow?.webContents.send(
|
||||||
"FILE_DOWNLOAD_COMPLETE",
|
'FILE_DOWNLOAD_COMPLETE',
|
||||||
{
|
{
|
||||||
fileName,
|
fileName,
|
||||||
}
|
}
|
||||||
);
|
)
|
||||||
DownloadManager.instance.setRequest(fileName, undefined);
|
DownloadManager.instance.setRequest(fileName, undefined)
|
||||||
} else {
|
} else {
|
||||||
WindowManager?.instance.currentWindow?.webContents.send(
|
WindowManager?.instance.currentWindow?.webContents.send(
|
||||||
"FILE_DOWNLOAD_ERROR",
|
'FILE_DOWNLOAD_ERROR',
|
||||||
{
|
{
|
||||||
fileName,
|
fileName,
|
||||||
err: "Download cancelled",
|
err: 'Download cancelled',
|
||||||
}
|
}
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.pipe(createWriteStream(destination));
|
.pipe(createWriteStream(destination))
|
||||||
|
|
||||||
DownloadManager.instance.setRequest(fileName, rq);
|
DownloadManager.instance.setRequest(fileName, rq)
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,28 +1,53 @@
|
|||||||
import { app, ipcMain } from "electron";
|
import { app, ipcMain } from 'electron'
|
||||||
import * as fs from "fs";
|
import * as fs from 'fs'
|
||||||
import { join } from "path";
|
import { join } from 'path'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles file system operations.
|
* Handles file system operations.
|
||||||
*/
|
*/
|
||||||
export function handleFsIPCs() {
|
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.
|
* Reads a file from the user data directory.
|
||||||
* @param event - The event object.
|
* @param event - The event object.
|
||||||
* @param path - The path of the file to read.
|
* @param path - The path of the file to read.
|
||||||
* @returns A promise that resolves with the contents of the file.
|
* @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) => {
|
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) {
|
if (err) {
|
||||||
reject(err);
|
reject(err)
|
||||||
} else {
|
} else {
|
||||||
resolve(data);
|
resolve(data)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes data to a file in the user data directory.
|
* 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.
|
* @returns A promise that resolves when the file has been written.
|
||||||
*/
|
*/
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
"writeFile",
|
'writeFile',
|
||||||
async (event, path: string, data: string): Promise<void> => {
|
async (event, path: string, data: string): Promise<void> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
fs.writeFile(
|
fs.writeFile(join(userSpacePath, path), data, 'utf8', (err) => {
|
||||||
join(app.getPath("userData"), path),
|
|
||||||
data,
|
|
||||||
"utf8",
|
|
||||||
(err) => {
|
|
||||||
if (err) {
|
if (err) {
|
||||||
reject(err);
|
reject(err)
|
||||||
} else {
|
} else {
|
||||||
resolve();
|
resolve()
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
);
|
)
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a directory in the user data directory.
|
* Creates a directory in the user data directory.
|
||||||
@ -57,21 +77,17 @@ export function handleFsIPCs() {
|
|||||||
* @param path - The path of the directory to create.
|
* @param path - The path of the directory to create.
|
||||||
* @returns A promise that resolves when the directory has been created.
|
* @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) => {
|
return new Promise((resolve, reject) => {
|
||||||
fs.mkdir(
|
fs.mkdir(join(userSpacePath, path), { recursive: true }, (err) => {
|
||||||
join(app.getPath("userData"), path),
|
|
||||||
{ recursive: true },
|
|
||||||
(err) => {
|
|
||||||
if (err) {
|
if (err) {
|
||||||
reject(err);
|
reject(err)
|
||||||
} else {
|
} else {
|
||||||
resolve();
|
resolve()
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
);
|
})
|
||||||
});
|
})
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes a directory in the user data directory.
|
* Removes a directory in the user data directory.
|
||||||
@ -79,21 +95,17 @@ export function handleFsIPCs() {
|
|||||||
* @param path - The path of the directory to remove.
|
* @param path - The path of the directory to remove.
|
||||||
* @returns A promise that resolves when the directory is removed successfully.
|
* @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) => {
|
return new Promise((resolve, reject) => {
|
||||||
fs.rmdir(
|
fs.rmdir(join(userSpacePath, path), { recursive: true }, (err) => {
|
||||||
join(app.getPath("userData"), path),
|
|
||||||
{ recursive: true },
|
|
||||||
(err) => {
|
|
||||||
if (err) {
|
if (err) {
|
||||||
reject(err);
|
reject(err)
|
||||||
} else {
|
} else {
|
||||||
resolve();
|
resolve()
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
);
|
})
|
||||||
});
|
})
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lists the files in a directory in the user data directory.
|
* 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.
|
* @returns A promise that resolves with an array of file names.
|
||||||
*/
|
*/
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
"listFiles",
|
'listFiles',
|
||||||
async (event, path: string): Promise<string[]> => {
|
async (event, path: string): Promise<string[]> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
fs.readdir(join(app.getPath("userData"), path), (err, files) => {
|
fs.readdir(join(userSpacePath, path), (err, files) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
reject(err);
|
reject(err)
|
||||||
} else {
|
} else {
|
||||||
resolve(files);
|
resolve(files)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
);
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes a file from the user data folder.
|
* Deletes a file from the user data folder.
|
||||||
@ -122,22 +134,23 @@ export function handleFsIPCs() {
|
|||||||
* @param filePath - The path to the file to delete.
|
* @param filePath - The path to the file to delete.
|
||||||
* @returns A string indicating the result of the operation.
|
* @returns A string indicating the result of the operation.
|
||||||
*/
|
*/
|
||||||
ipcMain.handle("deleteFile", async (_event, filePath) => {
|
ipcMain.handle('deleteFile', async (_event, filePath) => {
|
||||||
const userDataPath = app.getPath("userData");
|
const fullPath = join(userSpacePath, filePath)
|
||||||
const fullPath = join(userDataPath, filePath);
|
|
||||||
|
|
||||||
let result = "NULL";
|
let result = 'NULL'
|
||||||
fs.unlink(fullPath, function (err) {
|
fs.unlink(fullPath, function (err) {
|
||||||
if (err && err.code == "ENOENT") {
|
if (err && err.code == 'ENOENT') {
|
||||||
result = `File not exist: ${err}`;
|
result = `File not exist: ${err}`
|
||||||
} else if (err) {
|
} else if (err) {
|
||||||
result = `File delete error: ${err}`;
|
result = `File delete error: ${err}`
|
||||||
} else {
|
} 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
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,7 +30,7 @@ export function handlePluginIPCs() {
|
|||||||
if (typeof module[method] === "function") {
|
if (typeof module[method] === "function") {
|
||||||
return module[method](...args);
|
return module[method](...args);
|
||||||
} else {
|
} else {
|
||||||
console.log(module[method]);
|
console.debug(module[method]);
|
||||||
console.error(`Function "${method}" does not exist in the module.`);
|
console.error(`Function "${method}" does not exist in the module.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -75,7 +75,7 @@ export function handlePluginIPCs() {
|
|||||||
const fullPath = join(userDataPath, "plugins");
|
const fullPath = join(userDataPath, "plugins");
|
||||||
|
|
||||||
rmdir(fullPath, { recursive: true }, function (err) {
|
rmdir(fullPath, { recursive: true }, function (err) {
|
||||||
if (err) console.log(err);
|
if (err) console.error(err);
|
||||||
ModuleManager.instance.clearImportedModules();
|
ModuleManager.instance.clearImportedModules();
|
||||||
|
|
||||||
// just relaunch if packaged, should launch manually in development mode
|
// just relaunch if packaged, should launch manually in development mode
|
||||||
|
|||||||
@ -42,7 +42,7 @@ export function handleAppUpdates() {
|
|||||||
|
|
||||||
/* App Update Progress */
|
/* App Update Progress */
|
||||||
autoUpdater.on("download-progress", (progress: any) => {
|
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(
|
WindowManager.instance.currentWindow?.webContents.send(
|
||||||
"APP_UPDATE_PROGRESS",
|
"APP_UPDATE_PROGRESS",
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,23 +1,23 @@
|
|||||||
import { app, BrowserWindow } from "electron";
|
import { app, BrowserWindow } from 'electron'
|
||||||
import { join } from "path";
|
import { join } from 'path'
|
||||||
import { setupMenu } from "./utils/menu";
|
import { setupMenu } from './utils/menu'
|
||||||
import { handleFsIPCs } from "./handlers/fs";
|
import { handleFsIPCs } from './handlers/fs'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Managers
|
* Managers
|
||||||
**/
|
**/
|
||||||
import { WindowManager } from "./managers/window";
|
import { WindowManager } from './managers/window'
|
||||||
import { ModuleManager } from "./managers/module";
|
import { ModuleManager } from './managers/module'
|
||||||
import { PluginManager } from "./managers/plugin";
|
import { PluginManager } from './managers/plugin'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* IPC Handlers
|
* IPC Handlers
|
||||||
**/
|
**/
|
||||||
import { handleDownloaderIPCs } from "./handlers/download";
|
import { handleDownloaderIPCs } from './handlers/download'
|
||||||
import { handleThemesIPCs } from "./handlers/theme";
|
import { handleThemesIPCs } from './handlers/theme'
|
||||||
import { handlePluginIPCs } from "./handlers/plugin";
|
import { handlePluginIPCs } from './handlers/plugin'
|
||||||
import { handleAppIPCs } from "./handlers/app";
|
import { handleAppIPCs } from './handlers/app'
|
||||||
import { handleAppUpdates } from "./handlers/update";
|
import { handleAppUpdates } from './handlers/update'
|
||||||
|
|
||||||
app
|
app
|
||||||
.whenReady()
|
.whenReady()
|
||||||
@ -28,56 +28,56 @@ app
|
|||||||
.then(handleAppUpdates)
|
.then(handleAppUpdates)
|
||||||
.then(createMainWindow)
|
.then(createMainWindow)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
app.on("activate", () => {
|
app.on('activate', () => {
|
||||||
if (!BrowserWindow.getAllWindows().length) {
|
if (!BrowserWindow.getAllWindows().length) {
|
||||||
createMainWindow();
|
createMainWindow()
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
|
|
||||||
app.on("window-all-closed", () => {
|
app.on('window-all-closed', () => {
|
||||||
ModuleManager.instance.clearImportedModules();
|
ModuleManager.instance.clearImportedModules()
|
||||||
app.quit();
|
app.quit()
|
||||||
});
|
})
|
||||||
|
|
||||||
app.on("quit", () => {
|
app.on('quit', () => {
|
||||||
ModuleManager.instance.clearImportedModules();
|
ModuleManager.instance.clearImportedModules()
|
||||||
app.quit();
|
app.quit()
|
||||||
});
|
})
|
||||||
|
|
||||||
function createMainWindow() {
|
function createMainWindow() {
|
||||||
/* Create main window */
|
/* Create main window */
|
||||||
const mainWindow = WindowManager.instance.createWindow({
|
const mainWindow = WindowManager.instance.createWindow({
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: true,
|
nodeIntegration: true,
|
||||||
preload: join(__dirname, "preload.js"),
|
preload: join(__dirname, 'preload.js'),
|
||||||
webSecurity: false,
|
webSecurity: false,
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
const startURL = app.isPackaged
|
const startURL = app.isPackaged
|
||||||
? `file://${join(__dirname, "../renderer/index.html")}`
|
? `file://${join(__dirname, '../renderer/index.html')}`
|
||||||
: "http://localhost:3000";
|
: 'http://localhost:3000'
|
||||||
|
|
||||||
/* Load frontend app to the window */
|
/* Load frontend app to the window */
|
||||||
mainWindow.loadURL(startURL);
|
mainWindow.loadURL(startURL)
|
||||||
|
|
||||||
mainWindow.once("ready-to-show", () => mainWindow?.show());
|
mainWindow.once('ready-to-show', () => mainWindow?.show())
|
||||||
mainWindow.on("closed", () => {
|
mainWindow.on('closed', () => {
|
||||||
if (process.platform !== "darwin") app.quit();
|
if (process.platform !== 'darwin') app.quit()
|
||||||
});
|
})
|
||||||
|
|
||||||
/* Enable dev tools for development */
|
/* 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.
|
* Handles various IPC messages from the renderer process.
|
||||||
*/
|
*/
|
||||||
function handleIPCs() {
|
function handleIPCs() {
|
||||||
handleFsIPCs();
|
handleFsIPCs()
|
||||||
handleDownloaderIPCs();
|
handleDownloaderIPCs()
|
||||||
handleThemesIPCs();
|
handleThemesIPCs()
|
||||||
handlePluginIPCs();
|
handlePluginIPCs()
|
||||||
handleAppIPCs();
|
handleAppIPCs()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,14 +42,14 @@ export class PluginManager {
|
|||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const store = new Store();
|
const store = new Store();
|
||||||
if (store.get("migrated_version") !== app.getVersion()) {
|
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 userDataPath = app.getPath("userData");
|
||||||
const fullPath = join(userDataPath, "plugins");
|
const fullPath = join(userDataPath, "plugins");
|
||||||
|
|
||||||
rmdir(fullPath, { recursive: true }, function (err) {
|
rmdir(fullPath, { recursive: true }, function (err) {
|
||||||
if (err) console.log(err);
|
if (err) console.error(err);
|
||||||
store.set("migrated_version", app.getVersion());
|
store.set("migrated_version", app.getVersion());
|
||||||
console.log("migrate plugins done");
|
console.debug("migrate plugins done");
|
||||||
resolve(undefined);
|
resolve(undefined);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -52,18 +52,18 @@
|
|||||||
"lint": "eslint . --ext \".js,.jsx,.ts,.tsx\"",
|
"lint": "eslint . --ext \".js,.jsx,.ts,.tsx\"",
|
||||||
"test:e2e": "playwright test --workers=1",
|
"test:e2e": "playwright test --workers=1",
|
||||||
"dev": "tsc -p . && electron .",
|
"dev": "tsc -p . && electron .",
|
||||||
"build": "tsc -p . && electron-builder -p never -m",
|
"build": "run-script-os",
|
||||||
"build:test": "tsc -p . && electron-builder --dir -p never -m",
|
"build:test": "run-script-os",
|
||||||
"build:test-darwin": "tsc -p . && electron-builder -p never -m --x64 --arm64 --dir",
|
"build:test:darwin": "tsc -p . && electron-builder -p never -m --dir",
|
||||||
"build:test-win32": "tsc -p . && electron-builder -p never -w --dir",
|
"build:test:win32": "tsc -p . && electron-builder -p never -w --dir",
|
||||||
"build:test-linux": "tsc -p . && electron-builder -p never -l --dir",
|
"build:test:linux": "tsc -p . && electron-builder -p never -l --dir",
|
||||||
"build:darwin": "tsc -p . && electron-builder -p never -m --x64 --arm64",
|
"build:darwin": "tsc -p . && electron-builder -p never -m",
|
||||||
"build:win32": "tsc -p . && electron-builder -p never -w",
|
"build:win32": "tsc -p . && electron-builder -p never -w",
|
||||||
"build:linux": "tsc -p . && electron-builder -p never --linux deb",
|
"build:linux": "tsc -p . && electron-builder -p never --linux deb",
|
||||||
"build:publish": "tsc -p . && electron-builder -p onTagOrDraft -m",
|
"build:publish": "run-script-os",
|
||||||
"build:publish-darwin": "tsc -p . && electron-builder -p onTagOrDraft -m --x64 --arm64",
|
"build:publish:darwin": "tsc -p . && electron-builder -p onTagOrDraft -m --x64 --arm64",
|
||||||
"build:publish-win32": "tsc -p . && electron-builder -p onTagOrDraft -w",
|
"build:publish:win32": "tsc -p . && electron-builder -p onTagOrDraft -w",
|
||||||
"build:publish-linux": "tsc -p . && electron-builder -p onTagOrDraft --linux deb "
|
"build:publish:linux": "tsc -p . && electron-builder -p onTagOrDraft -l deb"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@npmcli/arborist": "^7.1.0",
|
"@npmcli/arborist": "^7.1.0",
|
||||||
@ -86,7 +86,8 @@
|
|||||||
"electron": "26.2.1",
|
"electron": "26.2.1",
|
||||||
"electron-builder": "^24.6.4",
|
"electron-builder": "^24.6.4",
|
||||||
"electron-playwright-helpers": "^1.6.0",
|
"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": {
|
"installConfig": {
|
||||||
"hoistingLimits": "workspaces"
|
"hoistingLimits": "workspaces"
|
||||||
|
|||||||
@ -33,6 +33,8 @@
|
|||||||
* @property {Function} relaunch - Relaunches the app.
|
* @property {Function} relaunch - Relaunches the app.
|
||||||
* @property {Function} openAppDirectory - Opens the app directory.
|
* @property {Function} openAppDirectory - Opens the app directory.
|
||||||
* @property {Function} deleteFile - Deletes the file at the given path.
|
* @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} readFile - Reads the file at the given path.
|
||||||
* @property {Function} writeFile - Writes the given data to 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.
|
* @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
|
// 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[]) =>
|
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) =>
|
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) =>
|
installRemotePlugin: (pluginName: string) =>
|
||||||
ipcRenderer.invoke("installRemotePlugin", pluginName),
|
ipcRenderer.invoke('installRemotePlugin', pluginName),
|
||||||
|
|
||||||
downloadFile: (url: string, path: string) =>
|
downloadFile: (url: string, path: string) =>
|
||||||
ipcRenderer.invoke("downloadFile", url, path),
|
ipcRenderer.invoke('downloadFile', url, path),
|
||||||
|
|
||||||
pauseDownload: (fileName: string) =>
|
pauseDownload: (fileName: string) =>
|
||||||
ipcRenderer.invoke("pauseDownload", fileName),
|
ipcRenderer.invoke('pauseDownload', fileName),
|
||||||
|
|
||||||
resumeDownload: (fileName: string) =>
|
resumeDownload: (fileName: string) =>
|
||||||
ipcRenderer.invoke("resumeDownload", fileName),
|
ipcRenderer.invoke('resumeDownload', fileName),
|
||||||
|
|
||||||
abortDownload: (fileName: string) =>
|
abortDownload: (fileName: string) =>
|
||||||
ipcRenderer.invoke("abortDownload", fileName),
|
ipcRenderer.invoke('abortDownload', fileName),
|
||||||
|
|
||||||
onFileDownloadUpdate: (callback: any) =>
|
onFileDownloadUpdate: (callback: any) =>
|
||||||
ipcRenderer.on("FILE_DOWNLOAD_UPDATE", callback),
|
ipcRenderer.on('FILE_DOWNLOAD_UPDATE', callback),
|
||||||
|
|
||||||
onFileDownloadError: (callback: any) =>
|
onFileDownloadError: (callback: any) =>
|
||||||
ipcRenderer.on("FILE_DOWNLOAD_ERROR", callback),
|
ipcRenderer.on('FILE_DOWNLOAD_ERROR', callback),
|
||||||
|
|
||||||
onFileDownloadSuccess: (callback: any) =>
|
onFileDownloadSuccess: (callback: any) =>
|
||||||
ipcRenderer.on("FILE_DOWNLOAD_COMPLETE", callback),
|
ipcRenderer.on('FILE_DOWNLOAD_COMPLETE', callback),
|
||||||
|
|
||||||
onAppUpdateDownloadUpdate: (callback: any) =>
|
onAppUpdateDownloadUpdate: (callback: any) =>
|
||||||
ipcRenderer.on("APP_UPDATE_PROGRESS", callback),
|
ipcRenderer.on('APP_UPDATE_PROGRESS', callback),
|
||||||
|
|
||||||
onAppUpdateDownloadError: (callback: any) =>
|
onAppUpdateDownloadError: (callback: any) =>
|
||||||
ipcRenderer.on("APP_UPDATE_ERROR", callback),
|
ipcRenderer.on('APP_UPDATE_ERROR', callback),
|
||||||
|
|
||||||
onAppUpdateDownloadSuccess: (callback: any) =>
|
onAppUpdateDownloadSuccess: (callback: any) =>
|
||||||
ipcRenderer.on("APP_UPDATE_COMPLETE", callback),
|
ipcRenderer.on('APP_UPDATE_COMPLETE', callback),
|
||||||
});
|
})
|
||||||
|
|||||||
21
package.json
@ -36,27 +36,16 @@
|
|||||||
"build:electron": "yarn workspace jan build",
|
"build:electron": "yarn workspace jan build",
|
||||||
"build:electron:test": "yarn workspace jan build:test",
|
"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": "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:test": "yarn build:web && yarn workspace jan build:test",
|
||||||
"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": "yarn build:web && yarn workspace jan build",
|
||||||
"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:publish": "yarn build:web && yarn workspace jan 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"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"concurrently": "^8.2.1",
|
"concurrently": "^8.2.1",
|
||||||
"cpx": "^1.5.0",
|
"cpx": "^1.5.0",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"wait-on": "^7.0.1"
|
"wait-on": "^7.0.1",
|
||||||
|
"run-script-os": "^1.1.6"
|
||||||
},
|
},
|
||||||
"version": "0.0.0"
|
"version": "0.0.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,6 +24,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@janhq/core": "file:../../core",
|
"@janhq/core": "file:../../core",
|
||||||
|
"path-browserify": "^1.0.1",
|
||||||
"ts-loader": "^9.5.0"
|
"ts-loader": "^9.5.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@ -1,12 +1,15 @@
|
|||||||
import { PluginType, fs } from '@janhq/core'
|
import { PluginType, fs } from '@janhq/core'
|
||||||
import { ConversationalPlugin } from '@janhq/core/lib/plugins'
|
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
|
* JSONConversationalPlugin is a ConversationalPlugin implementation that provides
|
||||||
* functionality for managing conversations.
|
* functionality for managing conversations.
|
||||||
*/
|
*/
|
||||||
export default class JSONConversationalPlugin implements ConversationalPlugin {
|
export default class JSONConversationalPlugin implements ConversationalPlugin {
|
||||||
|
private static readonly _homeDir = 'threads'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the type of the plugin.
|
* Returns the type of the plugin.
|
||||||
*/
|
*/
|
||||||
@ -18,7 +21,7 @@ export default class JSONConversationalPlugin implements ConversationalPlugin {
|
|||||||
* Called when the plugin is loaded.
|
* Called when the plugin is loaded.
|
||||||
*/
|
*/
|
||||||
onLoad() {
|
onLoad() {
|
||||||
fs.mkdir('conversations')
|
fs.mkdir(JSONConversationalPlugin._homeDir)
|
||||||
console.debug('JSONConversationalPlugin loaded')
|
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.
|
* Returns a Promise that resolves to an array of Conversation objects.
|
||||||
*/
|
*/
|
||||||
async getConversations(): Promise<Conversation[]> {
|
async getConversations(): Promise<Thread[]> {
|
||||||
try {
|
try {
|
||||||
const convoIds = await this.getConversationDocs()
|
const convoIds = await this.getConversationDocs()
|
||||||
|
|
||||||
@ -43,7 +46,7 @@ export default class JSONConversationalPlugin implements ConversationalPlugin {
|
|||||||
const convos = promiseResults
|
const convos = promiseResults
|
||||||
.map((result) => {
|
.map((result) => {
|
||||||
if (result.status === 'fulfilled') {
|
if (result.status === 'fulfilled') {
|
||||||
return JSON.parse(result.value) as Conversation
|
return JSON.parse(result.value) as Thread
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.filter((convo) => convo != null)
|
.filter((convo) => convo != null)
|
||||||
@ -63,12 +66,16 @@ export default class JSONConversationalPlugin implements ConversationalPlugin {
|
|||||||
* Saves a Conversation object to a Markdown file.
|
* Saves a Conversation object to a Markdown file.
|
||||||
* @param conversation The Conversation object to save.
|
* @param conversation The Conversation object to save.
|
||||||
*/
|
*/
|
||||||
saveConversation(conversation: Conversation): Promise<void> {
|
saveConversation(conversation: Thread): Promise<void> {
|
||||||
return fs
|
return fs
|
||||||
.mkdir(`conversations/${conversation._id}`)
|
.mkdir(`${JSONConversationalPlugin._homeDir}/${conversation.id}`)
|
||||||
.then(() =>
|
.then(() =>
|
||||||
fs.writeFile(
|
fs.writeFile(
|
||||||
`conversations/${conversation._id}/${conversation._id}.json`,
|
join(
|
||||||
|
JSONConversationalPlugin._homeDir,
|
||||||
|
conversation.id,
|
||||||
|
`${conversation.id}.json`
|
||||||
|
),
|
||||||
JSON.stringify(conversation)
|
JSON.stringify(conversation)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -79,7 +86,9 @@ export default class JSONConversationalPlugin implements ConversationalPlugin {
|
|||||||
* @param conversationId The ID of the conversation to delete.
|
* @param conversationId The ID of the conversation to delete.
|
||||||
*/
|
*/
|
||||||
deleteConversation(conversationId: string): Promise<void> {
|
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
|
* @returns data of the conversation
|
||||||
*/
|
*/
|
||||||
private async readConvo(convoId: string): Promise<any> {
|
private async readConvo(convoId: string): Promise<any> {
|
||||||
return fs.readFile(`conversations/${convoId}/${convoId}.json`)
|
return fs.readFile(
|
||||||
|
join(JSONConversationalPlugin._homeDir, convoId, `${convoId}.json`)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -97,7 +108,9 @@ export default class JSONConversationalPlugin implements ConversationalPlugin {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private async getConversationDocs(): Promise<string[]> {
|
private async getConversationDocs(): Promise<string[]> {
|
||||||
return fs.listFiles(`conversations`).then((files: string[]) => {
|
return fs
|
||||||
|
.listFiles(JSONConversationalPlugin._homeDir)
|
||||||
|
.then((files: string[]) => {
|
||||||
return Promise.all(files.filter((file) => file.startsWith('jan-')))
|
return Promise.all(files.filter((file) => file.startsWith('jan-')))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,6 +22,9 @@ module.exports = {
|
|||||||
plugins: [new webpack.DefinePlugin({})],
|
plugins: [new webpack.DefinePlugin({})],
|
||||||
resolve: {
|
resolve: {
|
||||||
extensions: [".ts", ".js"],
|
extensions: [".ts", ".js"],
|
||||||
|
fallback: {
|
||||||
|
path: require.resolve('path-browserify'),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
// Do not minify the output, otherwise it breaks the class registration
|
// Do not minify the output, otherwise it breaks the class registration
|
||||||
optimization: {
|
optimization: {
|
||||||
|
|||||||
@ -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": []
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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"]
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
};
|
|
||||||
@ -13,18 +13,14 @@
|
|||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc -b . && webpack --config webpack.config.js",
|
"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": "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: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": "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: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: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: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": "run-script-os",
|
||||||
"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",
|
"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",
|
||||||
"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",
|
"build:publish:win32": "rimraf *.tgz --glob && npm run build && npm run downloadnitro && cpx \"nitro/**\" \"dist/nitro\" && npm pack && cpx *.tgz ../../electron/core/pre-install",
|
||||||
"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:linux": "rimraf *.tgz --glob && npm run build && npm run downloadnitro && cpx \"nitro/**\" \"dist/nitro\" && npm pack && cpx *.tgz ../../electron/core/pre-install",
|
||||||
"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": "run-script-os"
|
||||||
"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"
|
|
||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./dist/index.js",
|
".": "./dist/index.js",
|
||||||
@ -33,6 +29,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"cpx": "^1.5.0",
|
"cpx": "^1.5.0",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
|
"run-script-os": "^1.1.6",
|
||||||
"webpack": "^5.88.2",
|
"webpack": "^5.88.2",
|
||||||
"webpack-cli": "^5.1.4"
|
"webpack-cli": "^5.1.4"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -7,10 +7,13 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
ChatCompletionMessage,
|
||||||
|
ChatCompletionRole,
|
||||||
EventName,
|
EventName,
|
||||||
MessageHistory,
|
MessageRequest,
|
||||||
NewMessageRequest,
|
MessageStatus,
|
||||||
PluginType,
|
PluginType,
|
||||||
|
ThreadMessage,
|
||||||
events,
|
events,
|
||||||
executeOnMain,
|
executeOnMain,
|
||||||
} from "@janhq/core";
|
} from "@janhq/core";
|
||||||
@ -18,7 +21,7 @@ import { InferencePlugin } from "@janhq/core/lib/plugins";
|
|||||||
import { requestInference } from "./helpers/sse";
|
import { requestInference } from "./helpers/sse";
|
||||||
import { ulid } from "ulid";
|
import { ulid } from "ulid";
|
||||||
import { join } from "path";
|
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.
|
* 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.
|
* @returns {Promise<void>} A promise that resolves when the model is initialized.
|
||||||
*/
|
*/
|
||||||
async initModel(modelFileName: string): Promise<void> {
|
async initModel(modelFileName: string): Promise<void> {
|
||||||
const appPath = await appDataPath();
|
const userSpacePath = await fs.getUserSpace();
|
||||||
return executeOnMain(MODULE, "initModel", join(appPath, modelFileName));
|
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.
|
* 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.
|
* @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 = {
|
const message = {
|
||||||
...data,
|
...data,
|
||||||
message: "",
|
message: "",
|
||||||
user: "assistant",
|
user: "assistant",
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
const prompts: [MessageHistory] = [
|
|
||||||
{
|
|
||||||
role: "user",
|
|
||||||
content: data.message,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const recentMessages = await (data.history ?? prompts);
|
|
||||||
|
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
requestInference([
|
requestInference(data.messages ?? []).subscribe({
|
||||||
...recentMessages,
|
|
||||||
{ role: "user", content: data.message },
|
|
||||||
]).subscribe({
|
|
||||||
next: (content) => {
|
next: (content) => {
|
||||||
message.message = 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.
|
* 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) {
|
private async handleMessageRequest(data: MessageRequest) {
|
||||||
const prompts: [MessageHistory] = [
|
const message: ThreadMessage = {
|
||||||
{
|
threadId: data.threadId,
|
||||||
role: "user",
|
content: "",
|
||||||
content: data.message,
|
role: ChatCompletionRole.Assistant,
|
||||||
},
|
|
||||||
];
|
|
||||||
const recentMessages = data.history ?? prompts;
|
|
||||||
const message = {
|
|
||||||
...data,
|
|
||||||
message: "",
|
|
||||||
user: "assistant",
|
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
_id: ulid(),
|
id: ulid(),
|
||||||
|
status: MessageStatus.Pending,
|
||||||
};
|
};
|
||||||
events.emit(EventName.OnNewMessageResponse, message);
|
events.emit(EventName.OnNewMessageResponse, message);
|
||||||
|
|
||||||
requestInference(recentMessages).subscribe({
|
requestInference(data.messages).subscribe({
|
||||||
next: (content) => {
|
next: (content) => {
|
||||||
message.message = content;
|
message.content = content;
|
||||||
events.emit(EventName.OnMessageResponseUpdate, message);
|
events.emit(EventName.OnMessageResponseUpdate, message);
|
||||||
},
|
},
|
||||||
complete: async () => {
|
complete: async () => {
|
||||||
message.message = message.message.trim();
|
message.content = message.content.trim();
|
||||||
|
message.status = MessageStatus.Ready;
|
||||||
events.emit(EventName.OnMessageResponseFinished, message);
|
events.emit(EventName.OnMessageResponseFinished, message);
|
||||||
},
|
},
|
||||||
error: async (err) => {
|
error: async (err) => {
|
||||||
message.message =
|
message.content =
|
||||||
message.message.trim() + "\n" + "Error occurred: " + err.message;
|
message.content.trim() + "\n" + "Error occurred: " + err.message;
|
||||||
|
message.status = MessageStatus.Ready;
|
||||||
events.emit(EventName.OnMessageResponseUpdate, message);
|
events.emit(EventName.OnMessageResponseUpdate, message);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -124,7 +124,7 @@ function killSubprocess(): Promise<void> {
|
|||||||
if (subprocess) {
|
if (subprocess) {
|
||||||
subprocess.kill();
|
subprocess.kill();
|
||||||
subprocess = null;
|
subprocess = null;
|
||||||
console.log("Subprocess terminated.");
|
console.debug("Subprocess terminated.");
|
||||||
} else {
|
} else {
|
||||||
return kill(PORT, "tcp").then(console.log).catch(console.log);
|
return kill(PORT, "tcp").then(console.log).catch(console.log);
|
||||||
}
|
}
|
||||||
|
|||||||
8
plugins/model-plugin/.prettierrc
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"quoteProps": "consistent",
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"endOfLine": "auto",
|
||||||
|
"plugins": ["prettier-plugin-tailwindcss"]
|
||||||
|
}
|
||||||
@ -29,6 +29,7 @@
|
|||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@janhq/core": "file:../../core",
|
"@janhq/core": "file:../../core",
|
||||||
|
"path-browserify": "^1.0.1",
|
||||||
"ts-loader": "^9.5.0"
|
"ts-loader": "^9.5.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -1,8 +1,8 @@
|
|||||||
export const parseToModel = (model) => {
|
export const parseToModel = (model) => {
|
||||||
const modelVersions = [];
|
const modelVersions = []
|
||||||
model.versions.forEach((v) => {
|
model.versions.forEach((v) => {
|
||||||
const version = {
|
const version = {
|
||||||
_id: `${model.author}-${v.name}`,
|
id: `${model.author}-${v.name}`,
|
||||||
name: v.name,
|
name: v.name,
|
||||||
quantMethod: v.quantMethod,
|
quantMethod: v.quantMethod,
|
||||||
bits: v.bits,
|
bits: v.bits,
|
||||||
@ -11,12 +11,12 @@ export const parseToModel = (model) => {
|
|||||||
usecase: v.usecase,
|
usecase: v.usecase,
|
||||||
downloadLink: v.downloadLink,
|
downloadLink: v.downloadLink,
|
||||||
productId: model.id,
|
productId: model.id,
|
||||||
};
|
}
|
||||||
modelVersions.push(version);
|
modelVersions.push(version)
|
||||||
});
|
})
|
||||||
|
|
||||||
const product = {
|
const product = {
|
||||||
_id: model.id,
|
id: model.id,
|
||||||
name: model.name,
|
name: model.name,
|
||||||
shortDescription: model.shortDescription,
|
shortDescription: model.shortDescription,
|
||||||
avatarUrl: model.avatarUrl,
|
avatarUrl: model.avatarUrl,
|
||||||
@ -29,9 +29,9 @@ export const parseToModel = (model) => {
|
|||||||
type: model.type,
|
type: model.type,
|
||||||
createdAt: model.createdAt,
|
createdAt: model.createdAt,
|
||||||
longDescription: model.longDescription,
|
longDescription: model.longDescription,
|
||||||
status: "Downloadable",
|
status: 'Downloadable',
|
||||||
releaseDate: 0,
|
releaseDate: 0,
|
||||||
availableVersions: modelVersions,
|
availableVersions: modelVersions,
|
||||||
};
|
}
|
||||||
return product;
|
return product
|
||||||
};
|
}
|
||||||
|
|||||||
@ -1,20 +1,21 @@
|
|||||||
import { PluginType, fs, downloadFile } from "@janhq/core";
|
import { PluginType, fs, downloadFile } from '@janhq/core'
|
||||||
import { ModelPlugin } from "@janhq/core/lib/plugins";
|
import { ModelPlugin } from '@janhq/core/lib/plugins'
|
||||||
import { Model, ModelCatalog } from "@janhq/core/lib/types";
|
import { Model, ModelCatalog } from '@janhq/core/lib/types'
|
||||||
import { pollDownloadProgress } from "./helpers/cloudNative";
|
import { parseToModel } from './helpers/modelParser'
|
||||||
import { parseToModel } from "./helpers/modelParser";
|
import { join } from 'path'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A plugin for managing machine learning models.
|
* A plugin for managing machine learning models.
|
||||||
*/
|
*/
|
||||||
export default class JanModelPlugin implements ModelPlugin {
|
export default class JanModelPlugin implements ModelPlugin {
|
||||||
|
private static readonly _homeDir = 'models'
|
||||||
/**
|
/**
|
||||||
* Implements type from JanPlugin.
|
* Implements type from JanPlugin.
|
||||||
* @override
|
* @override
|
||||||
* @returns The type of the plugin.
|
* @returns The type of the plugin.
|
||||||
*/
|
*/
|
||||||
type(): PluginType {
|
type(): PluginType {
|
||||||
return PluginType.Model;
|
return PluginType.Model
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -25,6 +26,7 @@ export default class JanModelPlugin implements ModelPlugin {
|
|||||||
/** Cloud Native
|
/** Cloud Native
|
||||||
* TODO: Fetch all downloading progresses?
|
* 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.
|
* @returns A Promise that resolves when the model is downloaded.
|
||||||
*/
|
*/
|
||||||
async downloadModel(model: Model): Promise<void> {
|
async downloadModel(model: Model): Promise<void> {
|
||||||
await fs.mkdir("models");
|
// create corresponding directory
|
||||||
downloadFile(model.downloadLink, `models/${model._id}`);
|
const directoryPath = join(JanModelPlugin._homeDir, model.name)
|
||||||
/** Cloud Native
|
await fs.mkdir(directoryPath)
|
||||||
* MARK: Poll Downloading Progress
|
|
||||||
**/
|
// path to model binary
|
||||||
pollDownloadProgress(model._id);
|
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.
|
* @param filePath - The path to the model file to delete.
|
||||||
* @returns A Promise that resolves when the model is deleted.
|
* @returns A Promise that resolves when the model is deleted.
|
||||||
*/
|
*/
|
||||||
deleteModel(filePath: string): Promise<void> {
|
async deleteModel(filePath: string): Promise<void> {
|
||||||
return fs
|
try {
|
||||||
.deleteFile(`models/${filePath}`)
|
await Promise.allSettled([
|
||||||
.then(() => fs.deleteFile(`models/m-${filePath}.json`));
|
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.
|
* @returns A Promise that resolves when the model is saved.
|
||||||
*/
|
*/
|
||||||
async saveModel(model: Model): Promise<void> {
|
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.
|
* Gets all downloaded models.
|
||||||
* @returns A Promise that resolves with an array of all models.
|
* @returns A Promise that resolves with an array of all models.
|
||||||
*/
|
*/
|
||||||
getDownloadedModels(): Promise<Model[]> {
|
async getDownloadedModels(): Promise<Model[]> {
|
||||||
return fs
|
const results: Model[] = []
|
||||||
.listFiles("models")
|
const allDirs: string[] = await fs.listFiles(JanModelPlugin._homeDir)
|
||||||
.then((files: string[]) => {
|
for (const dir of allDirs) {
|
||||||
return Promise.all(
|
const modelDirPath = join(JanModelPlugin._homeDir, dir)
|
||||||
files
|
const isModelDir = await fs.isDirectory(modelDirPath)
|
||||||
.filter((file) => /^m-.*\.json$/.test(file))
|
if (!isModelDir) {
|
||||||
.map(async (file) => {
|
// if not a directory, ignore
|
||||||
const model: Model = JSON.parse(
|
continue
|
||||||
await fs.readFile(`models/${file}`)
|
|
||||||
);
|
|
||||||
return model;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.catch((e) => fs.mkdir("models").then(() => []));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.
|
* Gets all available models.
|
||||||
* @returns A Promise that resolves with an array of all 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
|
// Add a timestamp to the URL to prevent caching
|
||||||
return import(
|
return import(
|
||||||
/* webpackIgnore: true */ MODEL_CATALOG_URL + `?t=${Date.now()}`
|
/* webpackIgnore: true */ MODEL_CATALOG_URL + `?t=${Date.now()}`
|
||||||
).then((module) =>
|
).then((module) => module.default.map((e) => parseToModel(e)))
|
||||||
module.default.map((e) => {
|
|
||||||
return parseToModel(e);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,16 @@
|
|||||||
const path = require("path");
|
const path = require('path')
|
||||||
const webpack = require("webpack");
|
const webpack = require('webpack')
|
||||||
const packageJson = require("./package.json");
|
const packageJson = require('./package.json')
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
experiments: { outputModule: true },
|
experiments: { outputModule: true },
|
||||||
entry: "./src/index.ts", // Adjust the entry point to match your project's main file
|
entry: './src/index.ts', // Adjust the entry point to match your project's main file
|
||||||
mode: "production",
|
mode: 'production',
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
test: /\.tsx?$/,
|
test: /\.tsx?$/,
|
||||||
use: "ts-loader",
|
use: 'ts-loader',
|
||||||
exclude: /node_modules/,
|
exclude: /node_modules/,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -20,20 +20,23 @@ module.exports = {
|
|||||||
PLUGIN_NAME: JSON.stringify(packageJson.name),
|
PLUGIN_NAME: JSON.stringify(packageJson.name),
|
||||||
MODULE_PATH: JSON.stringify(`${packageJson.name}/${packageJson.module}`),
|
MODULE_PATH: JSON.stringify(`${packageJson.name}/${packageJson.module}`),
|
||||||
MODEL_CATALOG_URL: JSON.stringify(
|
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: {
|
output: {
|
||||||
filename: "index.js", // Adjust the output file name as needed
|
filename: 'index.js', // Adjust the output file name as needed
|
||||||
path: path.resolve(__dirname, "dist"),
|
path: path.resolve(__dirname, 'dist'),
|
||||||
library: { type: "module" }, // Specify ESM output format
|
library: { type: 'module' }, // Specify ESM output format
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
extensions: [".ts", ".js"],
|
extensions: ['.ts', '.js'],
|
||||||
|
fallback: {
|
||||||
|
path: require.resolve('path-browserify'),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
optimization: {
|
optimization: {
|
||||||
minimize: false,
|
minimize: false,
|
||||||
},
|
},
|
||||||
// Add loaders and other configuration as needed for your project
|
// Add loaders and other configuration as needed for your project
|
||||||
};
|
}
|
||||||
|
|||||||
@ -46,7 +46,7 @@ const BottomBar = () => {
|
|||||||
<SystemItem
|
<SystemItem
|
||||||
name="Active model:"
|
name="Active model:"
|
||||||
value={
|
value={
|
||||||
activeModel?._id || (
|
activeModel?.id || (
|
||||||
<Badge themes="secondary">⌘e to show your model</Badge>
|
<Badge themes="secondary">⌘e to show your model</Badge>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,7 +24,7 @@ export default function CommandListDownloadedModel() {
|
|||||||
const { activeModel, startModel, stopModel } = useActiveModel()
|
const { activeModel, startModel, stopModel } = useActiveModel()
|
||||||
|
|
||||||
const onModelActionClick = (modelId: string) => {
|
const onModelActionClick = (modelId: string) => {
|
||||||
if (activeModel && activeModel._id === modelId) {
|
if (activeModel && activeModel.id === modelId) {
|
||||||
stopModel(modelId)
|
stopModel(modelId)
|
||||||
} else {
|
} else {
|
||||||
startModel(modelId)
|
startModel(modelId)
|
||||||
@ -62,7 +62,7 @@ export default function CommandListDownloadedModel() {
|
|||||||
<CommandItem
|
<CommandItem
|
||||||
key={i}
|
key={i}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
onModelActionClick(model._id)
|
onModelActionClick(model.id)
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -72,7 +72,7 @@ export default function CommandListDownloadedModel() {
|
|||||||
/>
|
/>
|
||||||
<div className="flex w-full items-center justify-between">
|
<div className="flex w-full items-center justify-between">
|
||||||
<span>{model.name}</span>
|
<span>{model.name}</span>
|
||||||
{activeModel && activeModel._id === model._id && (
|
{activeModel && activeModel.id === model.id && (
|
||||||
<Badge themes="secondary">Active</Badge>
|
<Badge themes="secondary">Active</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -32,9 +32,9 @@ export default function ModalCancelDownload({
|
|||||||
const { modelDownloadStateAtom } = useDownloadState()
|
const { modelDownloadStateAtom } = useDownloadState()
|
||||||
useGetPerformanceTag()
|
useGetPerformanceTag()
|
||||||
const downloadAtom = useMemo(
|
const downloadAtom = useMemo(
|
||||||
() => atom((get) => get(modelDownloadStateAtom)[suitableModel._id]),
|
() => atom((get) => get(modelDownloadStateAtom)[suitableModel.name]),
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
[suitableModel._id]
|
[suitableModel.name]
|
||||||
)
|
)
|
||||||
const downloadState = useAtomValue(downloadAtom)
|
const downloadState = useAtomValue(downloadAtom)
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,14 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { ReactNode, useEffect, useRef } from 'react'
|
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 { ConversationalPlugin, ModelPlugin } from '@janhq/core/lib/plugins'
|
||||||
import { Message } from '@janhq/core/lib/types'
|
|
||||||
import { useAtomValue, useSetAtom } from 'jotai'
|
import { useAtomValue, useSetAtom } from 'jotai'
|
||||||
|
|
||||||
import { useDownloadState } from '@/hooks/useDownloadState'
|
import { useDownloadState } from '@/hooks/useDownloadState'
|
||||||
@ -16,22 +20,15 @@ import {
|
|||||||
updateMessageAtom,
|
updateMessageAtom,
|
||||||
} from '@/helpers/atoms/ChatMessage.atom'
|
} from '@/helpers/atoms/ChatMessage.atom'
|
||||||
import {
|
import {
|
||||||
updateConversationAtom,
|
|
||||||
updateConversationWaitingForResponseAtom,
|
updateConversationWaitingForResponseAtom,
|
||||||
userConversationsAtom,
|
userConversationsAtom,
|
||||||
} from '@/helpers/atoms/Conversation.atom'
|
} from '@/helpers/atoms/Conversation.atom'
|
||||||
|
|
||||||
import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom'
|
import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom'
|
||||||
import { MessageStatus, toChatMessage } from '@/models/ChatMessage'
|
|
||||||
import { pluginManager } from '@/plugin'
|
import { pluginManager } from '@/plugin'
|
||||||
import { ChatMessage, Conversation } from '@/types/chatMessage'
|
|
||||||
|
|
||||||
let currentConversation: Conversation | undefined = undefined
|
|
||||||
|
|
||||||
export default function EventHandler({ children }: { children: ReactNode }) {
|
export default function EventHandler({ children }: { children: ReactNode }) {
|
||||||
const addNewMessage = useSetAtom(addNewMessageAtom)
|
const addNewMessage = useSetAtom(addNewMessageAtom)
|
||||||
const updateMessage = useSetAtom(updateMessageAtom)
|
const updateMessage = useSetAtom(updateMessageAtom)
|
||||||
const updateConversation = useSetAtom(updateConversationAtom)
|
|
||||||
|
|
||||||
const { setDownloadState, setDownloadStateSuccess } = useDownloadState()
|
const { setDownloadState, setDownloadStateSuccess } = useDownloadState()
|
||||||
const { downloadedModels, setDownloadedModels } = useGetDownloadedModels()
|
const { downloadedModels, setDownloadedModels } = useGetDownloadedModels()
|
||||||
@ -48,98 +45,55 @@ export default function EventHandler({ children }: { children: ReactNode }) {
|
|||||||
convoRef.current = conversations
|
convoRef.current = conversations
|
||||||
}, [messages, conversations])
|
}, [messages, conversations])
|
||||||
|
|
||||||
async function handleNewMessageResponse(message: NewMessageResponse) {
|
async function handleNewMessageResponse(message: ThreadMessage) {
|
||||||
if (message.conversationId) {
|
if (message.threadId) {
|
||||||
const convo = convoRef.current.find(
|
const convo = convoRef.current.find((e) => e.id == message.threadId)
|
||||||
(e) => e._id == message.conversationId
|
|
||||||
)
|
|
||||||
if (!convo) return
|
if (!convo) return
|
||||||
const newResponse = toChatMessage(message)
|
addNewMessage(message)
|
||||||
addNewMessage(newResponse)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function handleMessageResponseUpdate(
|
async function handleMessageResponseUpdate(messageResponse: ThreadMessage) {
|
||||||
messageResponse: NewMessageResponse
|
|
||||||
) {
|
|
||||||
if (
|
if (
|
||||||
messageResponse.conversationId &&
|
messageResponse.threadId &&
|
||||||
messageResponse._id &&
|
messageResponse.id &&
|
||||||
messageResponse.message
|
messageResponse.content
|
||||||
) {
|
) {
|
||||||
updateMessage(
|
updateMessage(
|
||||||
messageResponse._id,
|
messageResponse.id,
|
||||||
messageResponse.conversationId,
|
messageResponse.threadId,
|
||||||
messageResponse.message,
|
messageResponse.content,
|
||||||
MessageStatus.Pending
|
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) {
|
async function handleMessageResponseFinished(messageResponse: ThreadMessage) {
|
||||||
const updatedConv: Conversation = {
|
if (!messageResponse.threadId || !convoRef.current) return
|
||||||
...currentConversation,
|
updateConvWaiting(messageResponse.threadId, false)
|
||||||
lastMessage: messageResponse.message,
|
|
||||||
}
|
|
||||||
|
|
||||||
updateConversation(updatedConv)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleMessageResponseFinished(
|
|
||||||
messageResponse: NewMessageResponse
|
|
||||||
) {
|
|
||||||
if (!messageResponse.conversationId || !convoRef.current) return
|
|
||||||
updateConvWaiting(messageResponse.conversationId, false)
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
messageResponse.conversationId &&
|
messageResponse.threadId &&
|
||||||
messageResponse._id &&
|
messageResponse.id &&
|
||||||
messageResponse.message
|
messageResponse.content
|
||||||
) {
|
) {
|
||||||
updateMessage(
|
updateMessage(
|
||||||
messageResponse._id,
|
messageResponse.id,
|
||||||
messageResponse.conversationId,
|
messageResponse.threadId,
|
||||||
messageResponse.message,
|
messageResponse.content,
|
||||||
MessageStatus.Ready
|
MessageStatus.Ready
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const convo = convoRef.current.find(
|
const thread = convoRef.current.find(
|
||||||
(e) => e._id == messageResponse.conversationId
|
(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
|
pluginManager
|
||||||
.get<ConversationalPlugin>(PluginType.Conversational)
|
.get<ConversationalPlugin>(PluginType.Conversational)
|
||||||
?.saveConversation({
|
?.saveConversation({
|
||||||
...convo,
|
...thread,
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
id: thread.id ?? '',
|
||||||
_id: convo._id ?? '',
|
messages: messagesRef.current[thread.id] ?? [],
|
||||||
name: convo.name ?? '',
|
|
||||||
message: convo.lastMessage ?? '',
|
|
||||||
messages: messagesData,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -151,9 +105,9 @@ export default function EventHandler({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
function handleDownloadSuccess(state: any) {
|
function handleDownloadSuccess(state: any) {
|
||||||
if (state && state.fileName && state.success === true) {
|
if (state && state.fileName && state.success === true) {
|
||||||
state.fileName = state.fileName.replace('models/', '')
|
state.fileName = state.fileName.split('/').pop() ?? ''
|
||||||
setDownloadStateSuccess(state.fileName)
|
setDownloadStateSuccess(state.fileName)
|
||||||
const model = models.find((e) => e._id === state.fileName)
|
const model = models.find((e) => e.id === state.fileName)
|
||||||
if (model)
|
if (model)
|
||||||
pluginManager
|
pluginManager
|
||||||
.get<ModelPlugin>(PluginType.Model)
|
.get<ModelPlugin>(PluginType.Model)
|
||||||
|
|||||||
@ -27,20 +27,28 @@ export default function EventListenerWrapper({ children }: PropsWithChildren) {
|
|||||||
const { setDownloadedModels, downloadedModels } = useGetDownloadedModels()
|
const { setDownloadedModels, downloadedModels } = useGetDownloadedModels()
|
||||||
const { setDownloadState, setDownloadStateSuccess, setDownloadStateFailed } =
|
const { setDownloadState, setDownloadStateSuccess, setDownloadStateFailed } =
|
||||||
useDownloadState()
|
useDownloadState()
|
||||||
|
const downloadedModelRef = useRef(downloadedModels)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
downloadedModelRef.current = downloadedModels
|
||||||
|
}, [downloadedModels])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (window && window.electronAPI) {
|
if (window && window.electronAPI) {
|
||||||
window.electronAPI.onFileDownloadUpdate(
|
window.electronAPI.onFileDownloadUpdate(
|
||||||
(_event: string, state: DownloadState | undefined) => {
|
(_event: string, state: DownloadState | undefined) => {
|
||||||
if (!state) return
|
if (!state) return
|
||||||
setDownloadState(state)
|
setDownloadState({
|
||||||
|
...state,
|
||||||
|
fileName: state.fileName.split('/').pop() ?? '',
|
||||||
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
window.electronAPI.onFileDownloadError(
|
window.electronAPI.onFileDownloadError(
|
||||||
(_event: string, callback: any) => {
|
(_event: string, callback: any) => {
|
||||||
console.log('Download error', callback)
|
console.error('Download error', callback)
|
||||||
const fileName = callback.fileName.replace('models/', '')
|
const fileName = callback.fileName.split('/').pop() ?? ''
|
||||||
setDownloadStateFailed(fileName)
|
setDownloadStateFailed(fileName)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -48,16 +56,16 @@ export default function EventListenerWrapper({ children }: PropsWithChildren) {
|
|||||||
window.electronAPI.onFileDownloadSuccess(
|
window.electronAPI.onFileDownloadSuccess(
|
||||||
(_event: string, callback: any) => {
|
(_event: string, callback: any) => {
|
||||||
if (callback && callback.fileName) {
|
if (callback && callback.fileName) {
|
||||||
const fileName = callback.fileName.replace('models/', '')
|
const fileName = callback.fileName.split('/').pop() ?? ''
|
||||||
setDownloadStateSuccess(fileName)
|
setDownloadStateSuccess(fileName)
|
||||||
|
|
||||||
const model = modelsRef.current.find((e) => e._id === fileName)
|
const model = modelsRef.current.find((e) => e.id === fileName)
|
||||||
if (model)
|
if (model)
|
||||||
pluginManager
|
pluginManager
|
||||||
.get<ModelPlugin>(PluginType.Model)
|
.get<ModelPlugin>(PluginType.Model)
|
||||||
?.saveModel(model)
|
?.saveModel(model)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setDownloadedModels([...downloadedModels, model])
|
setDownloadedModels([...downloadedModelRef.current, model])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -66,13 +74,13 @@ export default function EventListenerWrapper({ children }: PropsWithChildren) {
|
|||||||
window.electronAPI.onAppUpdateDownloadUpdate(
|
window.electronAPI.onAppUpdateDownloadUpdate(
|
||||||
(_event: string, progress: any) => {
|
(_event: string, progress: any) => {
|
||||||
setProgress(progress.percent)
|
setProgress(progress.percent)
|
||||||
console.log('app update progress:', progress.percent)
|
console.debug('app update progress:', progress.percent)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
window.electronAPI.onAppUpdateDownloadError(
|
window.electronAPI.onAppUpdateDownloadError(
|
||||||
(_event: string, callback: any) => {
|
(_event: string, callback: any) => {
|
||||||
console.log('Download error', callback)
|
console.error('Download error', callback)
|
||||||
setProgress(-1)
|
setProgress(-1)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,18 +1,21 @@
|
|||||||
|
import { MessageStatus, ThreadMessage } from '@janhq/core'
|
||||||
import { atom } from 'jotai'
|
import { atom } from 'jotai'
|
||||||
|
|
||||||
import { getActiveConvoIdAtom } from './Conversation.atom'
|
import {
|
||||||
|
conversationStatesAtom,
|
||||||
import { ChatMessage, MessageStatus } from '@/models/ChatMessage'
|
getActiveConvoIdAtom,
|
||||||
|
updateThreadStateLastMessageAtom,
|
||||||
|
} from './Conversation.atom'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores all chat messages for all conversations
|
* 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
|
* 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)
|
const activeConversationId = get(getActiveConvoIdAtom)
|
||||||
if (!activeConversationId) return []
|
if (!activeConversationId) return []
|
||||||
const messages = get(chatMessages)[activeConversationId]
|
const messages = get(chatMessages)[activeConversationId]
|
||||||
@ -21,11 +24,11 @@ export const getCurrentChatMessagesAtom = atom<ChatMessage[]>((get) => {
|
|||||||
|
|
||||||
export const setCurrentChatMessagesAtom = atom(
|
export const setCurrentChatMessagesAtom = atom(
|
||||||
null,
|
null,
|
||||||
(get, set, messages: ChatMessage[]) => {
|
(get, set, messages: ThreadMessage[]) => {
|
||||||
const currentConvoId = get(getActiveConvoIdAtom)
|
const currentConvoId = get(getActiveConvoIdAtom)
|
||||||
if (!currentConvoId) return
|
if (!currentConvoId) return
|
||||||
|
|
||||||
const newData: Record<string, ChatMessage[]> = {
|
const newData: Record<string, ThreadMessage[]> = {
|
||||||
...get(chatMessages),
|
...get(chatMessages),
|
||||||
}
|
}
|
||||||
newData[currentConvoId] = messages
|
newData[currentConvoId] = messages
|
||||||
@ -35,8 +38,8 @@ export const setCurrentChatMessagesAtom = atom(
|
|||||||
|
|
||||||
export const setConvoMessagesAtom = atom(
|
export const setConvoMessagesAtom = atom(
|
||||||
null,
|
null,
|
||||||
(get, set, messages: ChatMessage[], convoId: string) => {
|
(get, set, messages: ThreadMessage[], convoId: string) => {
|
||||||
const newData: Record<string, ChatMessage[]> = {
|
const newData: Record<string, ThreadMessage[]> = {
|
||||||
...get(chatMessages),
|
...get(chatMessages),
|
||||||
}
|
}
|
||||||
newData[convoId] = messages
|
newData[convoId] = messages
|
||||||
@ -49,14 +52,14 @@ export const setConvoMessagesAtom = atom(
|
|||||||
*/
|
*/
|
||||||
export const addOldMessagesAtom = atom(
|
export const addOldMessagesAtom = atom(
|
||||||
null,
|
null,
|
||||||
(get, set, newMessages: ChatMessage[]) => {
|
(get, set, newMessages: ThreadMessage[]) => {
|
||||||
const currentConvoId = get(getActiveConvoIdAtom)
|
const currentConvoId = get(getActiveConvoIdAtom)
|
||||||
if (!currentConvoId) return
|
if (!currentConvoId) return
|
||||||
|
|
||||||
const currentMessages = get(chatMessages)[currentConvoId] ?? []
|
const currentMessages = get(chatMessages)[currentConvoId] ?? []
|
||||||
const updatedMessages = [...currentMessages, ...newMessages]
|
const updatedMessages = [...currentMessages, ...newMessages]
|
||||||
|
|
||||||
const newData: Record<string, ChatMessage[]> = {
|
const newData: Record<string, ThreadMessage[]> = {
|
||||||
...get(chatMessages),
|
...get(chatMessages),
|
||||||
}
|
}
|
||||||
newData[currentConvoId] = updatedMessages
|
newData[currentConvoId] = updatedMessages
|
||||||
@ -66,23 +69,25 @@ export const addOldMessagesAtom = atom(
|
|||||||
|
|
||||||
export const addNewMessageAtom = atom(
|
export const addNewMessageAtom = atom(
|
||||||
null,
|
null,
|
||||||
(get, set, newMessage: ChatMessage) => {
|
(get, set, newMessage: ThreadMessage) => {
|
||||||
const currentConvoId = get(getActiveConvoIdAtom)
|
const currentConvoId = get(getActiveConvoIdAtom)
|
||||||
if (!currentConvoId) return
|
if (!currentConvoId) return
|
||||||
|
|
||||||
const currentMessages = get(chatMessages)[currentConvoId] ?? []
|
const currentMessages = get(chatMessages)[currentConvoId] ?? []
|
||||||
const updatedMessages = [newMessage, ...currentMessages]
|
const updatedMessages = [newMessage, ...currentMessages]
|
||||||
|
|
||||||
const newData: Record<string, ChatMessage[]> = {
|
const newData: Record<string, ThreadMessage[]> = {
|
||||||
...get(chatMessages),
|
...get(chatMessages),
|
||||||
}
|
}
|
||||||
newData[currentConvoId] = updatedMessages
|
newData[currentConvoId] = updatedMessages
|
||||||
set(chatMessages, newData)
|
set(chatMessages, newData)
|
||||||
|
// Update thread last message
|
||||||
|
set(updateThreadStateLastMessageAtom, currentConvoId, newMessage.content)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export const deleteConversationMessage = atom(null, (get, set, id: string) => {
|
export const deleteConversationMessage = atom(null, (get, set, id: string) => {
|
||||||
const newData: Record<string, ChatMessage[]> = {
|
const newData: Record<string, ThreadMessage[]> = {
|
||||||
...get(chatMessages),
|
...get(chatMessages),
|
||||||
}
|
}
|
||||||
newData[id] = []
|
newData[id] = []
|
||||||
@ -102,15 +107,17 @@ export const updateMessageAtom = atom(
|
|||||||
const messages = get(chatMessages)[conversationId] ?? []
|
const messages = get(chatMessages)[conversationId] ?? []
|
||||||
const message = messages.find((e) => e.id === id)
|
const message = messages.find((e) => e.id === id)
|
||||||
if (message) {
|
if (message) {
|
||||||
message.text = text
|
message.content = text
|
||||||
message.status = status
|
message.status = status
|
||||||
const updatedMessages = [...messages]
|
const updatedMessages = [...messages]
|
||||||
|
|
||||||
const newData: Record<string, ChatMessage[]> = {
|
const newData: Record<string, ThreadMessage[]> = {
|
||||||
...get(chatMessages),
|
...get(chatMessages),
|
||||||
}
|
}
|
||||||
newData[conversationId] = updatedMessages
|
newData[conversationId] = updatedMessages
|
||||||
set(chatMessages, newData)
|
set(chatMessages, newData)
|
||||||
|
// Update thread last message
|
||||||
|
set(updateThreadStateLastMessageAtom, conversationId, text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -131,14 +138,14 @@ export const updateLastMessageAsReadyAtom = atom(
|
|||||||
if (!messageToUpdate) return
|
if (!messageToUpdate) return
|
||||||
|
|
||||||
const index = currentMessages.indexOf(messageToUpdate)
|
const index = currentMessages.indexOf(messageToUpdate)
|
||||||
const updatedMsg: ChatMessage = {
|
const updatedMsg: ThreadMessage = {
|
||||||
...messageToUpdate,
|
...messageToUpdate,
|
||||||
status: MessageStatus.Ready,
|
status: MessageStatus.Ready,
|
||||||
text: text,
|
content: text,
|
||||||
}
|
}
|
||||||
|
|
||||||
currentMessages[index] = updatedMsg
|
currentMessages[index] = updatedMsg
|
||||||
const newData: Record<string, ChatMessage[]> = {
|
const newData: Record<string, ThreadMessage[]> = {
|
||||||
...get(chatMessages),
|
...get(chatMessages),
|
||||||
}
|
}
|
||||||
newData[currentConvoId] = currentMessages
|
newData[currentConvoId] = currentMessages
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { Conversation, ConversationState } from '@/types/chatMessage'
|
import { Thread } from '@janhq/core'
|
||||||
import { atom } from 'jotai'
|
import { atom } from 'jotai'
|
||||||
|
|
||||||
// import { MainViewState, setMainViewStateAtom } from './MainView.atom'
|
import { ThreadState } from '@/types/conversation'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores the current active conversation id.
|
* Stores the current active conversation id.
|
||||||
@ -21,11 +21,8 @@ export const waitingToSendMessage = atom<boolean | undefined>(undefined)
|
|||||||
/**
|
/**
|
||||||
* Stores all conversation states for the current user
|
* Stores all conversation states for the current user
|
||||||
*/
|
*/
|
||||||
export const conversationStatesAtom = atom<Record<string, ConversationState>>(
|
export const conversationStatesAtom = atom<Record<string, ThreadState>>({})
|
||||||
{}
|
export const currentConvoStateAtom = atom<ThreadState | undefined>((get) => {
|
||||||
)
|
|
||||||
export const currentConvoStateAtom = atom<ConversationState | undefined>(
|
|
||||||
(get) => {
|
|
||||||
const activeConvoId = get(activeConversationIdAtom)
|
const activeConvoId = get(activeConversationIdAtom)
|
||||||
if (!activeConvoId) {
|
if (!activeConvoId) {
|
||||||
console.debug('Active convo id is undefined')
|
console.debug('Active convo id is undefined')
|
||||||
@ -33,11 +30,10 @@ export const currentConvoStateAtom = atom<ConversationState | undefined>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return get(conversationStatesAtom)[activeConvoId]
|
return get(conversationStatesAtom)[activeConvoId]
|
||||||
}
|
})
|
||||||
)
|
|
||||||
export const addNewConversationStateAtom = atom(
|
export const addNewConversationStateAtom = atom(
|
||||||
null,
|
null,
|
||||||
(get, set, conversationId: string, state: ConversationState) => {
|
(get, set, conversationId: string, state: ThreadState) => {
|
||||||
const currentState = { ...get(conversationStatesAtom) }
|
const currentState = { ...get(conversationStatesAtom) }
|
||||||
currentState[conversationId] = state
|
currentState[conversationId] = state
|
||||||
set(conversationStatesAtom, currentState)
|
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(
|
export const updateConversationAtom = atom(
|
||||||
null,
|
null,
|
||||||
(get, set, conversation: Conversation) => {
|
(get, set, conversation: Thread) => {
|
||||||
const id = conversation._id
|
const id = conversation.id
|
||||||
if (!id) return
|
if (!id) return
|
||||||
const convo = get(userConversationsAtom).find((c) => c._id === id)
|
const convo = get(userConversationsAtom).find((c) => c.id === id)
|
||||||
if (!convo) return
|
if (!convo) return
|
||||||
|
|
||||||
const newConversations: Conversation[] = get(userConversationsAtom).map(
|
const newConversations: Thread[] = get(userConversationsAtom).map((c) =>
|
||||||
(c) => (c._id === id ? conversation : c)
|
c.id === id ? conversation : c
|
||||||
)
|
)
|
||||||
|
|
||||||
// sort new conversations based on updated at
|
// sort new conversations based on updated at
|
||||||
@ -101,7 +109,7 @@ export const updateConversationAtom = atom(
|
|||||||
/**
|
/**
|
||||||
* Stores all conversations for the current user
|
* Stores all conversations for the current user
|
||||||
*/
|
*/
|
||||||
export const userConversationsAtom = atom<Conversation[]>([])
|
export const userConversationsAtom = atom<Thread[]>([])
|
||||||
export const currentConversationAtom = atom<Conversation | undefined>((get) =>
|
export const currentConversationAtom = atom<Thread | undefined>((get) =>
|
||||||
get(userConversationsAtom).find((c) => c._id === get(getActiveConvoIdAtom))
|
get(userConversationsAtom).find((c) => c.id === get(getActiveConvoIdAtom))
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import { join } from 'path'
|
||||||
|
|
||||||
import { PluginType } from '@janhq/core'
|
import { PluginType } from '@janhq/core'
|
||||||
import { InferencePlugin } from '@janhq/core/lib/plugins'
|
import { InferencePlugin } from '@janhq/core/lib/plugins'
|
||||||
import { Model } from '@janhq/core/lib/types'
|
import { Model } from '@janhq/core/lib/types'
|
||||||
@ -21,16 +23,16 @@ export function useActiveModel() {
|
|||||||
const { downloadedModels } = useGetDownloadedModels()
|
const { downloadedModels } = useGetDownloadedModels()
|
||||||
|
|
||||||
const startModel = async (modelId: string) => {
|
const startModel = async (modelId: string) => {
|
||||||
if (activeModel && activeModel._id === modelId) {
|
if (activeModel && activeModel.id === modelId) {
|
||||||
console.debug(`Model ${modelId} is already init. Ignore..`)
|
console.debug(`Model ${modelId} is already init. Ignore..`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setStateModel({ state: 'start', loading: true, model: modelId })
|
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.`)
|
alert(`Model ${modelId} not found! Please re-download the model first.`)
|
||||||
setStateModel(() => ({
|
setStateModel(() => ({
|
||||||
state: 'start',
|
state: 'start',
|
||||||
@ -42,8 +44,8 @@ export function useActiveModel() {
|
|||||||
|
|
||||||
const currentTime = Date.now()
|
const currentTime = Date.now()
|
||||||
console.debug('Init model: ', modelId)
|
console.debug('Init model: ', modelId)
|
||||||
|
const path = join('models', model.name, modelId)
|
||||||
const res = await initModel(`models/${modelId}`)
|
const res = await initModel(path)
|
||||||
if (res?.error) {
|
if (res?.error) {
|
||||||
const errorMessage = `${res.error}`
|
const errorMessage = `${res.error}`
|
||||||
alert(errorMessage)
|
alert(errorMessage)
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { PluginType } from '@janhq/core'
|
import { PluginType } from '@janhq/core'
|
||||||
|
import { Thread, Model } from '@janhq/core'
|
||||||
import { ConversationalPlugin } from '@janhq/core/lib/plugins'
|
import { ConversationalPlugin } from '@janhq/core/lib/plugins'
|
||||||
import { Model } from '@janhq/core/lib/types'
|
|
||||||
|
|
||||||
import { useAtom, useSetAtom } from 'jotai'
|
import { useAtom, useSetAtom } from 'jotai'
|
||||||
|
|
||||||
import { generateConversationId } from '@/utils/conversation'
|
import { generateConversationId } from '@/utils/conversation'
|
||||||
@ -12,7 +11,6 @@ import {
|
|||||||
addNewConversationStateAtom,
|
addNewConversationStateAtom,
|
||||||
} from '@/helpers/atoms/Conversation.atom'
|
} from '@/helpers/atoms/Conversation.atom'
|
||||||
import { pluginManager } from '@/plugin'
|
import { pluginManager } from '@/plugin'
|
||||||
import { Conversation } from '@/types/chatMessage'
|
|
||||||
|
|
||||||
export const useCreateConversation = () => {
|
export const useCreateConversation = () => {
|
||||||
const [userConversations, setUserConversations] = useAtom(
|
const [userConversations, setUserConversations] = useAtom(
|
||||||
@ -22,30 +20,26 @@ export const useCreateConversation = () => {
|
|||||||
const addNewConvoState = useSetAtom(addNewConversationStateAtom)
|
const addNewConvoState = useSetAtom(addNewConversationStateAtom)
|
||||||
|
|
||||||
const requestCreateConvo = async (model: Model) => {
|
const requestCreateConvo = async (model: Model) => {
|
||||||
const conversationName = model.name
|
const summary = model.name
|
||||||
const mappedConvo: Conversation = {
|
const mappedConvo: Thread = {
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
id: generateConversationId(),
|
||||||
_id: generateConversationId(),
|
modelId: model.id,
|
||||||
modelId: model._id,
|
summary,
|
||||||
name: conversationName,
|
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
|
messages: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
addNewConvoState(mappedConvo._id, {
|
addNewConvoState(mappedConvo.id, {
|
||||||
hasMore: true,
|
hasMore: true,
|
||||||
waitingForResponse: false,
|
waitingForResponse: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
pluginManager
|
pluginManager
|
||||||
.get<ConversationalPlugin>(PluginType.Conversational)
|
.get<ConversationalPlugin>(PluginType.Conversational)
|
||||||
?.saveConversation({
|
?.saveConversation(mappedConvo)
|
||||||
...mappedConvo,
|
|
||||||
name: mappedConvo.name ?? '',
|
|
||||||
messages: [],
|
|
||||||
})
|
|
||||||
setUserConversations([mappedConvo, ...userConversations])
|
setUserConversations([mappedConvo, ...userConversations])
|
||||||
setActiveConvoId(mappedConvo._id)
|
setActiveConvoId(mappedConvo.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -41,7 +41,7 @@ export default function useDeleteConversation() {
|
|||||||
.get<ConversationalPlugin>(PluginType.Conversational)
|
.get<ConversationalPlugin>(PluginType.Conversational)
|
||||||
?.deleteConversation(activeConvoId)
|
?.deleteConversation(activeConvoId)
|
||||||
const currentConversations = userConversations.filter(
|
const currentConversations = userConversations.filter(
|
||||||
(c) => c._id !== activeConvoId
|
(c) => c.id !== activeConvoId
|
||||||
)
|
)
|
||||||
setUserConversations(currentConversations)
|
setUserConversations(currentConversations)
|
||||||
deleteMessages(activeConvoId)
|
deleteMessages(activeConvoId)
|
||||||
@ -50,7 +50,7 @@ export default function useDeleteConversation() {
|
|||||||
description: `Delete chat with ${activeModel?.name} has been completed`,
|
description: `Delete chat with ${activeModel?.name} has been completed`,
|
||||||
})
|
})
|
||||||
if (currentConversations.length > 0) {
|
if (currentConversations.length > 0) {
|
||||||
setActiveConvoId(currentConversations[0]._id)
|
setActiveConvoId(currentConversations[0].id)
|
||||||
} else {
|
} else {
|
||||||
setActiveConvoId(undefined)
|
setActiveConvoId(undefined)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { join } from 'path'
|
||||||
|
|
||||||
import { PluginType } from '@janhq/core'
|
import { PluginType } from '@janhq/core'
|
||||||
import { ModelPlugin } from '@janhq/core/lib/plugins'
|
import { ModelPlugin } from '@janhq/core/lib/plugins'
|
||||||
import { Model } from '@janhq/core/lib/types'
|
import { Model } from '@janhq/core/lib/types'
|
||||||
@ -12,15 +14,14 @@ export default function useDeleteModel() {
|
|||||||
const { setDownloadedModels, downloadedModels } = useGetDownloadedModels()
|
const { setDownloadedModels, downloadedModels } = useGetDownloadedModels()
|
||||||
|
|
||||||
const deleteModel = async (model: Model) => {
|
const deleteModel = async (model: Model) => {
|
||||||
await pluginManager
|
const path = join('models', model.name, model.id)
|
||||||
.get<ModelPlugin>(PluginType.Model)
|
await pluginManager.get<ModelPlugin>(PluginType.Model)?.deleteModel(path)
|
||||||
?.deleteModel(model._id)
|
|
||||||
|
|
||||||
// reload models
|
// reload models
|
||||||
setDownloadedModels(downloadedModels.filter((e) => e._id !== model._id))
|
setDownloadedModels(downloadedModels.filter((e) => e.id !== model.id))
|
||||||
toaster({
|
toaster({
|
||||||
title: 'Delete a Model',
|
title: 'Delete a Model',
|
||||||
description: `Model ${model._id} has been deleted.`,
|
description: `Model ${model.id} has been deleted.`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||