Merge branch 'main' into feat_specs_model
@ -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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -34,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,140 +1,183 @@
|
|||||||
export interface Conversation {
|
|
||||||
id: string;
|
|
||||||
modelId?: string;
|
|
||||||
botId?: string;
|
|
||||||
name: string;
|
|
||||||
message?: string;
|
|
||||||
summary?: string;
|
|
||||||
createdAt?: string;
|
|
||||||
updatedAt?: string;
|
|
||||||
messages: Message[];
|
|
||||||
lastMessage?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Message {
|
|
||||||
id: string;
|
|
||||||
message?: string;
|
|
||||||
user?: string;
|
|
||||||
createdAt?: string;
|
|
||||||
updatedAt?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RawMessage {
|
|
||||||
id?: string;
|
|
||||||
conversationId?: string;
|
|
||||||
user?: string;
|
|
||||||
avatar?: string;
|
|
||||||
message?: string;
|
|
||||||
createdAt?: string;
|
|
||||||
updatedAt?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Model {
|
|
||||||
/**
|
|
||||||
* Combination of owner and model name.
|
|
||||||
* Being used as file name. MUST be unique.
|
|
||||||
*/
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
quantMethod: string;
|
|
||||||
bits: number;
|
|
||||||
size: number;
|
|
||||||
maxRamRequired: number;
|
|
||||||
usecase: string;
|
|
||||||
downloadLink: string;
|
|
||||||
modelFile?: string;
|
|
||||||
/**
|
|
||||||
* For tracking download info
|
|
||||||
*/
|
|
||||||
startDownloadAt?: number;
|
|
||||||
finishDownloadAt?: number;
|
|
||||||
productId: string;
|
|
||||||
productName: string;
|
|
||||||
shortDescription: string;
|
|
||||||
longDescription: string;
|
|
||||||
avatarUrl: string;
|
|
||||||
author: string;
|
|
||||||
version: string;
|
|
||||||
modelUrl: string;
|
|
||||||
createdAt: number;
|
|
||||||
updatedAt?: number;
|
|
||||||
status: string;
|
|
||||||
releaseDate: number;
|
|
||||||
tags: string[];
|
|
||||||
}
|
|
||||||
export interface ModelCatalog {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
shortDescription: string;
|
|
||||||
avatarUrl: string;
|
|
||||||
longDescription: string;
|
|
||||||
author: string;
|
|
||||||
version: string;
|
|
||||||
modelUrl: string;
|
|
||||||
createdAt: number;
|
|
||||||
updatedAt?: number;
|
|
||||||
status: string;
|
|
||||||
releaseDate: number;
|
|
||||||
tags: string[];
|
|
||||||
availableVersions: ModelVersion[];
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
* Model type which will be stored in the database
|
* Message Request and Response
|
||||||
|
* ============================
|
||||||
|
* */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The role of the author of this message.
|
||||||
|
* @data_transfer_object
|
||||||
*/
|
*/
|
||||||
export type ModelVersion = {
|
export enum ChatCompletionRole {
|
||||||
/**
|
System = "system",
|
||||||
* Combination of owner and model name.
|
Assistant = "assistant",
|
||||||
* Being used as file name. Should be unique.
|
|
||||||
*/
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
quantMethod: string;
|
|
||||||
bits: number;
|
|
||||||
size: number;
|
|
||||||
maxRamRequired: number;
|
|
||||||
usecase: string;
|
|
||||||
downloadLink: string;
|
|
||||||
productId: string;
|
|
||||||
/**
|
|
||||||
* For tracking download state
|
|
||||||
*/
|
|
||||||
startDownloadAt?: number;
|
|
||||||
finishDownloadAt?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface ChatMessage {
|
|
||||||
id: string;
|
|
||||||
conversationId: string;
|
|
||||||
messageType: MessageType;
|
|
||||||
messageSenderType: MessageSenderType;
|
|
||||||
senderUid: string;
|
|
||||||
senderName: string;
|
|
||||||
senderAvatarUrl: string;
|
|
||||||
text: string | undefined;
|
|
||||||
imageUrls?: string[] | undefined;
|
|
||||||
createdAt: number;
|
|
||||||
status: MessageStatus;
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum MessageType {
|
|
||||||
Text = "Text",
|
|
||||||
Image = "Image",
|
|
||||||
ImageWithText = "ImageWithText",
|
|
||||||
Error = "Error",
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum MessageSenderType {
|
|
||||||
Ai = "assistant",
|
|
||||||
User = "user",
|
User = "user",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `MessageRequest` type defines the shape of a new message request object.
|
||||||
|
* @data_transfer_object
|
||||||
|
*/
|
||||||
|
export type ChatCompletionMessage = {
|
||||||
|
/** The contents of the message. **/
|
||||||
|
content?: string;
|
||||||
|
/** The role of the author of this message. **/
|
||||||
|
role: ChatCompletionRole;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `MessageRequest` type defines the shape of a new message request object.
|
||||||
|
* @data_transfer_object
|
||||||
|
*/
|
||||||
|
export type MessageRequest = {
|
||||||
|
id?: string;
|
||||||
|
/** The thread id of the message request. **/
|
||||||
|
threadId?: string;
|
||||||
|
/** Messages for constructing a chat completion request **/
|
||||||
|
messages?: ChatCompletionMessage[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thread and Message
|
||||||
|
* ========================
|
||||||
|
* */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The status of the message.
|
||||||
|
* @data_transfer_object
|
||||||
|
*/
|
||||||
export enum MessageStatus {
|
export enum MessageStatus {
|
||||||
|
/** Message is fully loaded. **/
|
||||||
Ready = "ready",
|
Ready = "ready",
|
||||||
|
/** Message is not fully loaded. **/
|
||||||
Pending = "pending",
|
Pending = "pending",
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
export type ConversationState = {
|
* The `ThreadMessage` type defines the shape of a thread's message object.
|
||||||
hasMore: boolean;
|
* @stored
|
||||||
waitingForResponse: boolean;
|
*/
|
||||||
error?: Error;
|
export type ThreadMessage = {
|
||||||
|
/** Unique identifier for the message, generated by default using the ULID method. **/
|
||||||
|
id?: string;
|
||||||
|
/** Thread id, default is a ulid. **/
|
||||||
|
threadId?: string;
|
||||||
|
/** The role of the author of this message. **/
|
||||||
|
role?: ChatCompletionRole;
|
||||||
|
/** The content of this message. **/
|
||||||
|
content?: string;
|
||||||
|
/** The status of this message. **/
|
||||||
|
status: MessageStatus;
|
||||||
|
/** The timestamp indicating when this message was created, represented in ISO 8601 format. **/
|
||||||
|
createdAt?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `Thread` type defines the shape of a thread object.
|
||||||
|
* @stored
|
||||||
|
*/
|
||||||
|
export interface Thread {
|
||||||
|
/** Unique identifier for the thread, generated by default using the ULID method. **/
|
||||||
|
id: string;
|
||||||
|
/** The summary of this thread. **/
|
||||||
|
summary?: string;
|
||||||
|
/** The messages of this thread. **/
|
||||||
|
messages: ThreadMessage[];
|
||||||
|
/** The timestamp indicating when this thread was created, represented in ISO 8601 format. **/
|
||||||
|
createdAt?: string;
|
||||||
|
/** The timestamp indicating when this thread was updated, represented in ISO 8601 format. **/
|
||||||
|
updatedAt?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated This field is deprecated and should not be used.
|
||||||
|
* Read from model file instead.
|
||||||
|
*/
|
||||||
|
modelId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model type defines the shape of a model object.
|
||||||
|
* @stored
|
||||||
|
*/
|
||||||
|
export interface Model {
|
||||||
|
/** Combination of owner and model name.*/
|
||||||
|
id: string;
|
||||||
|
/** The name of the model.*/
|
||||||
|
name: string;
|
||||||
|
/** Quantization method name.*/
|
||||||
|
quantizationName: string;
|
||||||
|
/** The the number of bits represents a number.*/
|
||||||
|
bits: number;
|
||||||
|
/** The size of the model file in bytes.*/
|
||||||
|
size: number;
|
||||||
|
/** The maximum RAM required to run the model in bytes.*/
|
||||||
|
maxRamRequired: number;
|
||||||
|
/** The use case of the model.*/
|
||||||
|
usecase: string;
|
||||||
|
/** The download link of the model.*/
|
||||||
|
downloadLink: string;
|
||||||
|
/** The short description of the model.*/
|
||||||
|
shortDescription: string;
|
||||||
|
/** The long description of the model.*/
|
||||||
|
longDescription: string;
|
||||||
|
/** The avatar url of the model.*/
|
||||||
|
avatarUrl: string;
|
||||||
|
/** The author name of the model.*/
|
||||||
|
author: string;
|
||||||
|
/** The version of the model.*/
|
||||||
|
version: string;
|
||||||
|
/** The origin url of the model repo.*/
|
||||||
|
modelUrl: string;
|
||||||
|
/** The timestamp indicating when this model was released.*/
|
||||||
|
releaseDate: number;
|
||||||
|
/** The tags attached to the model description */
|
||||||
|
tags: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model type of the presentation object which will be presented to the user
|
||||||
|
* @data_transfer_object
|
||||||
|
*/
|
||||||
|
export interface ModelCatalog {
|
||||||
|
/** The unique id of the model.*/
|
||||||
|
id: string;
|
||||||
|
/** The name of the model.*/
|
||||||
|
name: string;
|
||||||
|
/** The avatar url of the model.*/
|
||||||
|
avatarUrl: string;
|
||||||
|
/** The short description of the model.*/
|
||||||
|
shortDescription: string;
|
||||||
|
/** The long description of the model.*/
|
||||||
|
longDescription: string;
|
||||||
|
/** The author name of the model.*/
|
||||||
|
author: string;
|
||||||
|
/** The version of the model.*/
|
||||||
|
version: string;
|
||||||
|
/** The origin url of the model repo.*/
|
||||||
|
modelUrl: string;
|
||||||
|
/** The timestamp indicating when this model was released.*/
|
||||||
|
releaseDate: number;
|
||||||
|
/** The tags attached to the model description **/
|
||||||
|
tags: string[];
|
||||||
|
|
||||||
|
/** The available versions of this model to download. */
|
||||||
|
availableVersions: ModelVersion[];
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Model type which will be present a version of ModelCatalog
|
||||||
|
* @data_transfer_object
|
||||||
|
*/
|
||||||
|
export type ModelVersion = {
|
||||||
|
/** The name of this model version.*/
|
||||||
|
name: string;
|
||||||
|
/** The quantization method name.*/
|
||||||
|
quantizationName: string;
|
||||||
|
/** The the number of bits represents a number.*/
|
||||||
|
bits: number;
|
||||||
|
/** The size of the model file in bytes.*/
|
||||||
|
size: number;
|
||||||
|
/** The maximum RAM required to run the model in bytes.*/
|
||||||
|
maxRamRequired: number;
|
||||||
|
/** The use case of the model.*/
|
||||||
|
usecase: string;
|
||||||
|
/** The download link of the model.*/
|
||||||
|
downloadLink: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,79 +2,188 @@
|
|||||||
title: "Assistants"
|
title: "Assistants"
|
||||||
---
|
---
|
||||||
|
|
||||||
Assistants can use models and tools.
|
:::warning
|
||||||
|
|
||||||
- Jan's `Assistants` are even more powerful than OpenAI due to customizable code in `index.js`
|
Draft Specification: functionality has not been implemented yet.
|
||||||
|
|
||||||
> OpenAI Equivalent: https://platform.openai.com/docs/api-reference/assistants
|
Feedback: [HackMD: Assistants Spec](https://hackmd.io/KKAznzZvS668R6Vmyf8fCg)
|
||||||
|
|
||||||
## Assistant Object
|
:::
|
||||||
|
|
||||||
- `assistant.json`
|
|
||||||
- Equivalent to: https://platform.openai.com/docs/api-reference/assistants/object
|
## User Stories
|
||||||
|
|
||||||
|
_Users can chat with an assistant_
|
||||||
|
|
||||||
|
- [Wireframes - show asst object properties]
|
||||||
|
- See [Threads Spec](https://hackmd.io/BM_8o_OCQ-iLCYhunn2Aug)
|
||||||
|
|
||||||
|
_Users can use Jan - the default assistant_
|
||||||
|
|
||||||
|
- [Wireframes here - show model picker]
|
||||||
|
- See [Default Jan Object](#Default-Jan-Example)
|
||||||
|
|
||||||
|
_Users can create an assistant from scratch_
|
||||||
|
|
||||||
|
- [Wireframes here - show create asst flow]
|
||||||
|
- Users can select any model for an assistant. See Model Spec
|
||||||
|
|
||||||
|
_Users can create an assistant from an existing assistant_
|
||||||
|
|
||||||
|
- [Wireframes showing asst edit mode]
|
||||||
|
|
||||||
|
## Jan Assistant Object
|
||||||
|
|
||||||
|
- A `Jan Assistant Object` is a "representation of an assistant"
|
||||||
|
- Objects are defined by `assistant-uuid.json` files in `json` format
|
||||||
|
- Objects are designed to be compatible with `OpenAI Assistant Objects` with additional properties needed to run on our infrastructure.
|
||||||
|
- ALL object properties are optional, i.e. users should be able to use an assistant declared by an empty `json` file.
|
||||||
|
|
||||||
|
| Property | Type | Description | Validation |
|
||||||
|
| ------------- | ----------------------------------------------- | ---------------------------------------------------------------------------------------------- | ------------------------------- |
|
||||||
|
| `object` | enum: `model`, `assistant`, `thread`, `message` | The Jan Object type | Defaults to `assistant` |
|
||||||
|
| `name` | string | A vanity name. | Defaults to filename |
|
||||||
|
| `description` | string | A vanity description. | Max `n` chars. Defaults to `""` |
|
||||||
|
| `models` | array | A list of Model Objects that the assistant can use. | Defaults to ALL models |
|
||||||
|
| `metadata` | map | This can be useful for storing additional information about the object in a structured format. | Defaults to `{}` |
|
||||||
|
| `tools` | array | TBA. | TBA |
|
||||||
|
| `files` | array | TBA. | TBA |
|
||||||
|
|
||||||
|
### Generic Example
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
// janroot/assistants/example/example.json
|
||||||
// Jan specific properties
|
"name": "Homework Helper",
|
||||||
"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
|
// Option 1 (default): all models in janroot/models are available via Model Picker
|
||||||
"id": "asst_abc123",
|
"models": [],
|
||||||
"object": "assistant",
|
|
||||||
"created_at": 1698984975,
|
// Option 2: creator can configure custom parameters on existing models in `janroot/models` &&
|
||||||
"name": "Math Tutor",
|
// Option 3: creator can package a custom model with the assistant
|
||||||
"description": null,
|
"models": [{ ...modelObject1 }, { ...modelObject2 }],
|
||||||
"model": reference model.json,
|
|
||||||
"instructions": reference model.json,
|
|
||||||
"tools": [
|
|
||||||
{
|
|
||||||
"type": "rag"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"file_ids": [],
|
|
||||||
"metadata": {}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Assistants API
|
### Default Jan Example
|
||||||
|
|
||||||
- _TODO_: What would modifying Assistant do? (doesn't mutate `index.js`?)
|
- Every user install has a default "Jan Assistant" declared below.
|
||||||
|
> Q: can we omit most properties in `jan.json`? It's all defaults anyway.
|
||||||
|
|
||||||
|
```json
|
||||||
|
// janroot/assistants/jan/jan.json
|
||||||
|
"description": "Use Jan to chat with all models",
|
||||||
|
```
|
||||||
|
|
||||||
|
## Filesystem
|
||||||
|
|
||||||
|
- Everything needed to represent & run an assistant is packaged into an `Assistant folder`.
|
||||||
|
- The folder is standalone and can be easily zipped, imported, and exported, e.g. to Github.
|
||||||
|
- The folder always contains an `Assistant Object`, declared in an `assistant-uuid.json`.
|
||||||
|
- The folder and file must share the same name: `assistant-uuid`
|
||||||
|
- In the future, the folder will contain all of the resources an assistant needs to run, e.g. custom model binaries, pdf files, custom code, etc.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
GET https://api.openai.com/v1/assistants # List
|
janroot/
|
||||||
POST https://api.openai.com/v1/assistants # C
|
assistants/
|
||||||
GET https://api.openai.com/v1/assistants/{assistant_id} # R
|
jan/ # Assistant Folder
|
||||||
POST https://api.openai.com/v1/assistants/{assistant_id} # U
|
jan.json # Assistant Object
|
||||||
DELETE https://api.openai.com/v1/assistants/{assistant_id} # D
|
homework-helper/ # Assistant Folder
|
||||||
|
homework-helper.json # Assistant Object
|
||||||
```
|
```
|
||||||
|
|
||||||
## Assistants Filesystem
|
### Custom Code
|
||||||
|
|
||||||
|
> Not in scope yet. Sharing as a preview only.
|
||||||
|
|
||||||
|
- Assistants can call custom code in the future
|
||||||
|
- Custom code extends beyond `function calling` to any features that can be implemented in `/src`
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
/assistants
|
example/ # Assistant Folder
|
||||||
/jan
|
example.json # Assistant Object
|
||||||
assistant.json # Assistant configs (see below)
|
package.json
|
||||||
|
src/
|
||||||
# For any custom code
|
index.ts
|
||||||
package.json # Import npm modules
|
helpers.ts
|
||||||
# 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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Knowledge Files
|
||||||
|
|
||||||
|
> Not in scope yet. Sharing as a preview only
|
||||||
|
|
||||||
|
- Assistants can do `retrieval` in future
|
||||||
|
|
||||||
|
```sh
|
||||||
|
|
||||||
|
example/ # Assistant Folder
|
||||||
|
example.json # Assistant Object
|
||||||
|
files/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Jan API
|
||||||
|
|
||||||
|
### Assistant API Object
|
||||||
|
|
||||||
|
#### `GET /v1/assistants/{assistant_id}`
|
||||||
|
|
||||||
|
- The `Jan Assistant Object` maps into the `OpenAI Assistant Object`.
|
||||||
|
- Properties marked with `*` are compatible with the [OpenAI `assistant` object](https://platform.openai.com/docs/api-reference/assistants)
|
||||||
|
- Note: The `Jan Assistant Object` has additional properties when retrieved via its API endpoint.
|
||||||
|
- https://platform.openai.com/docs/api-reference/assistants/getAssistant
|
||||||
|
|
||||||
|
| Property | Type | Public Description | Jan Assistant Object (`a`) Property |
|
||||||
|
| ---------------- | -------------- | ------------------------------------------------------------------------- | ----------------------------------- |
|
||||||
|
| `id`\* | string | Assistant uuid, also the name of the Jan Assistant Object file: `id.json` | `json` filename |
|
||||||
|
| `object`\* | string | Always "assistant" | `a.object` |
|
||||||
|
| `created_at`\* | integer | Timestamp when assistant was created. | `a.json` creation time |
|
||||||
|
| `name`\* | string or null | A display name | `a.name` or `id` |
|
||||||
|
| `description`\* | string or null | A description | `a.description` |
|
||||||
|
| `model`\* | string | Text | `a.models[0].name` |
|
||||||
|
| `instructions`\* | string or null | Text | `a.models[0].parameters.prompt` |
|
||||||
|
| `tools`\* | array | TBA | `a.tools` |
|
||||||
|
| `file_ids`\* | array | TBA | `a.files` |
|
||||||
|
| `metadata`\* | map | TBA | `a.metadata` |
|
||||||
|
| `models` | array | TBA | `a.models` |
|
||||||
|
|
||||||
|
### Create Assistant
|
||||||
|
|
||||||
|
#### `POST /v1/assistants`
|
||||||
|
|
||||||
|
- https://platform.openai.com/docs/api-reference/assistants/createAssistant
|
||||||
|
|
||||||
|
### Retrieve Assistant
|
||||||
|
|
||||||
|
#### `GET v1/assistants/{assistant_id}`
|
||||||
|
|
||||||
|
- https://platform.openai.com/docs/api-reference/assistants/getAssistant
|
||||||
|
|
||||||
|
### Modify Assistant
|
||||||
|
|
||||||
|
#### `POST v1/assistants/{assistant_id}`
|
||||||
|
|
||||||
|
- https://platform.openai.com/docs/api-reference/assistants/modifyAssistant
|
||||||
|
|
||||||
|
### Delete Assistant
|
||||||
|
|
||||||
|
#### `DELETE v1/assistants/{assistant_id}`
|
||||||
|
|
||||||
|
- https://platform.openai.com/docs/api-reference/assistants/deleteAssistant
|
||||||
|
|
||||||
|
### List Assistants
|
||||||
|
|
||||||
|
#### `GET v1/assistants`
|
||||||
|
|
||||||
|
- https://platform.openai.com/docs/api-reference/assistants/listAssistants
|
||||||
|
|
||||||
|
### CRUD Assistant.Models
|
||||||
|
|
||||||
|
- This is a Jan-only endpoint, since Jan supports the ModelPicker, i.e. an `assistant` can be created to run with many `models`.
|
||||||
|
|
||||||
|
#### `POST /v1/assistants/{assistant_id}/models`
|
||||||
|
|
||||||
|
#### `GET /v1/assistants/{assistant_id}/models`
|
||||||
|
|
||||||
|
#### `GET /v1/assistants/{assistant_id}/models/{model_id}`
|
||||||
|
|
||||||
|
#### `DELETE /v1/assistants/{assistant_id}/models`
|
||||||
|
|
||||||
|
Note: There's no need to implement `Modify Assistant.Models`
|
||||||
|
|||||||
@ -2,6 +2,12 @@
|
|||||||
title: "Chats"
|
title: "Chats"
|
||||||
---
|
---
|
||||||
|
|
||||||
|
:::warning
|
||||||
|
|
||||||
|
Draft Specification: functionality has not been implemented yet.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
Chats are essentially inference requests to a model
|
Chats are essentially inference requests to a model
|
||||||
|
|
||||||
> OpenAI Equivalent: https://platform.openai.com/docs/api-reference/chat
|
> OpenAI Equivalent: https://platform.openai.com/docs/api-reference/chat
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -2,6 +2,14 @@
|
|||||||
title: "Messages"
|
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.
|
Messages are within `threads` and capture additional metadata.
|
||||||
|
|
||||||
- Equivalent to: https://platform.openai.com/docs/api-reference/messages
|
- Equivalent to: https://platform.openai.com/docs/api-reference/messages
|
||||||
|
|||||||
@ -1,4 +1,16 @@
|
|||||||
# Model Specs
|
---
|
||||||
|
title: "Models"
|
||||||
|
---
|
||||||
|
|
||||||
|
:::warning
|
||||||
|
|
||||||
|
Draft Specification: functionality has not been implemented yet.
|
||||||
|
|
||||||
|
Feedback: [HackMD: Models Spec](https://hackmd.io/ulO3uB1AQCqLa5SAAMFOQw)
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
Models are AI models like Llama and Mistral
|
||||||
|
|
||||||
> OpenAI Equivalent: https://platform.openai.com/docs/api-reference/models
|
> OpenAI Equivalent: https://platform.openai.com/docs/api-reference/models
|
||||||
|
|
||||||
@ -18,7 +30,7 @@ _Users can configure model settings, like run parameters_
|
|||||||
|
|
||||||
_Users can override run settings at runtime_
|
_Users can override run settings at runtime_
|
||||||
|
|
||||||
- See [assistant]() and [thread]()
|
- See Assistant Spec and Thread
|
||||||
|
|
||||||
## Jan Model Object
|
## Jan Model Object
|
||||||
|
|
||||||
|
|||||||
@ -2,52 +2,133 @@
|
|||||||
title: "Threads"
|
title: "Threads"
|
||||||
---
|
---
|
||||||
|
|
||||||
Threads contain `messages` history with assistants. Messages in a thread share context.
|
:::warning
|
||||||
|
|
||||||
- Note: For now, threads "lock the model" after a `message` is sent
|
Draft Specification: functionality has not been implemented yet.
|
||||||
- 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
|
Feedback: [HackMD: Threads Spec](https://hackmd.io/BM_8o_OCQ-iLCYhunn2Aug)
|
||||||
|
|
||||||
## Thread Object
|
:::
|
||||||
|
|
||||||
- `thread.json`
|
## User Stories
|
||||||
- Equivalent to: https://platform.openai.com/docs/api-reference/threads/object
|
|
||||||
|
_Users can chat with an assistant in a thread_
|
||||||
|
|
||||||
|
- See [Messages Spec]
|
||||||
|
|
||||||
|
_Users can change model in a new thread_
|
||||||
|
|
||||||
|
- Wireframes here
|
||||||
|
|
||||||
|
_Users can change model parameters in a thread_
|
||||||
|
|
||||||
|
- Wireframes of
|
||||||
|
|
||||||
|
_Users can delete all thread history_
|
||||||
|
|
||||||
|
- Wireframes of settings page.
|
||||||
|
|
||||||
|
## Jan Thread Object
|
||||||
|
|
||||||
|
- A `Jan Thread Object` is a "representation of a conversation thread" between an `assistant` and the user
|
||||||
|
- Objects are defined by `thread-uuid.json` files in `json` format
|
||||||
|
- Objects are designed to be compatible with `OpenAI Thread Objects` with additional properties needed to run on our infrastructure.
|
||||||
|
- Objects contain a `models` field, to track when the user overrides the assistant's default model parameters.
|
||||||
|
|
||||||
|
| Property | Type | Description | Validation |
|
||||||
|
| ---------- | ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ |
|
||||||
|
| `object` | enum: `model`, `assistant`, `thread`, `message` | The Jan Object type | Defaults to `thread` |
|
||||||
|
| `models` | array | An array of Jan Model Objects. Threads can "override" an assistant's model run parameters. Thread-level model parameters are directly saved in the `thread.models` property! (see Models spec) | Defaults to `assistant.models` |
|
||||||
|
| `messages` | array | An array of Jan Message Objects. (see Messages spec) | Defaults to `[]` |
|
||||||
|
| `metadata` | map | Useful for storing additional information about the object in a structured format. | Defaults to `{}` |
|
||||||
|
|
||||||
|
### Generic Example
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
// janroot/threads/jan_1700123404.json
|
||||||
// Jan specific properties:
|
"messages": [
|
||||||
"summary": "HCMC restaurant recommendations",
|
{...message0}, {...message1}
|
||||||
"messages": {see below}
|
],
|
||||||
|
"metadata": {
|
||||||
// OpenAI compatible properties: https://platform.openai.com/docs/api-reference/threads)
|
"summary": "funny physics joke",
|
||||||
"id": "thread_abc123",
|
},
|
||||||
"object": "thread",
|
|
||||||
"created_at": 1698107661,
|
|
||||||
"metadata": {}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Threads API
|
## Filesystem
|
||||||
|
|
||||||
- Equivalent to: https://platform.openai.com/docs/api-reference/threads
|
- `Jan Thread Objects`' `json` files always has the naming schema: `assistant_uuid` + `unix_time_thread_created_at. See below.
|
||||||
|
- Threads are all saved in the `janroot/threads` folder in a flat folder structure.
|
||||||
```sh=
|
- The folder is standalone and can be easily zipped, exported, and cleared.
|
||||||
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
|
```sh
|
||||||
/assistants
|
janroot/
|
||||||
/homework-helper
|
threads/
|
||||||
/threads # context is "permanently remembered" by assistant in future conversations
|
jan_1700123404.json
|
||||||
/threads # context is only retained within a single thread
|
homework_helper_700120003.json
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Jan API
|
||||||
|
|
||||||
|
### Thread API Object
|
||||||
|
|
||||||
|
#### `GET /v1/threads/{thread_id}`
|
||||||
|
|
||||||
|
- The `Jan Thread Object` maps into the `OpenAI Thread Object`.
|
||||||
|
- Properties marked with `*` are compatible with the [OpenAI `thread` object](https://platform.openai.com/docs/api-reference/threads)
|
||||||
|
- Note: The `Jan Thread Object` has additional properties when retrieved via its API endpoint.
|
||||||
|
- https://platform.openai.com/docs/api-reference/threads/getThread
|
||||||
|
|
||||||
|
| Property | Type | Public Description | Jan Thread Object (`t`) Property |
|
||||||
|
| -------------- | ------- | ------------------------------------------------------------------- | -------------------------------- |
|
||||||
|
| `id`\* | string | Thread uuid, also the name of the Jan Thread Object file: `id.json` | `json` filename |
|
||||||
|
| `object`\* | string | Always "thread" | `t.object` |
|
||||||
|
| `created_at`\* | integer | | `json` file creation time |
|
||||||
|
| `metadata`\* | map | | `t.metadata` |
|
||||||
|
| `models` | array | | `t.models` |
|
||||||
|
| `messages` | array | | `t.messages` |
|
||||||
|
|
||||||
|
### Create Thread
|
||||||
|
|
||||||
|
#### `POST /v1/threads`
|
||||||
|
|
||||||
|
- https://platform.openai.com/docs/api-reference/threads/createThread
|
||||||
|
|
||||||
|
### Retrieve Thread
|
||||||
|
|
||||||
|
#### `GET v1/threads/{thread_id}`
|
||||||
|
|
||||||
|
- https://platform.openai.com/docs/api-reference/threads/getThread
|
||||||
|
|
||||||
|
### Modify Thread
|
||||||
|
|
||||||
|
#### `POST v1/threads/{thread_id}`
|
||||||
|
|
||||||
|
- https://platform.openai.com/docs/api-reference/threads/modifyThread
|
||||||
|
|
||||||
|
### Delete Thread
|
||||||
|
|
||||||
|
#### `DELETE v1/threads/{thread_id}`
|
||||||
|
|
||||||
|
- https://platform.openai.com/docs/api-reference/threads/deleteThread
|
||||||
|
|
||||||
|
### List Threads
|
||||||
|
|
||||||
|
> This is a Jan-only endpoint, not supported by OAI yet.
|
||||||
|
|
||||||
|
#### `GET v1/threads`
|
||||||
|
|
||||||
|
### Get & Modify `Thread.Models`
|
||||||
|
|
||||||
|
> This is a Jan-only endpoint, not supported by OAI yet.
|
||||||
|
|
||||||
|
#### `GET v1/threads/{thread_id}/models`
|
||||||
|
|
||||||
|
#### `POST v1/threads/{thread_id}/models/{model_id}`
|
||||||
|
|
||||||
|
- Since users can change model parameters in an existing thread
|
||||||
|
|
||||||
|
### List `Thread.Messages`
|
||||||
|
|
||||||
|
> This is a Jan-only endpoint, not supported by OAI yet.
|
||||||
|
|
||||||
|
#### `GET v1/threads/{thread_id}/messages`
|
||||||
|
|||||||
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>
|
||||||
|
<h1 className="bg-gradient-to-r dark:from-white from-black to-gray-500 dark:to-gray-400 bg-clip-text text-4xl lg:text-6xl font-bold leading-tight text-transparent dark:text-transparent lg:leading-tight">
|
||||||
|
Your AI, forever.
|
||||||
|
</h1>
|
||||||
|
<p className="text-lg lg:text-2xl mt-2">
|
||||||
|
Apps come and go, but your AI and data should last.{" "}
|
||||||
|
</p>
|
||||||
|
<div class="w-full lg:w-3/4 mt-8">
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-10 lg:gap-24">
|
||||||
<div>
|
<div>
|
||||||
<h6>{feat.name}</h6>
|
<img
|
||||||
<p className="mt-2">{feat.desc}</p>
|
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",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
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'
|
import { join } from 'path'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -35,7 +35,7 @@ export default class JSONConversationalPlugin implements ConversationalPlugin {
|
|||||||
/**
|
/**
|
||||||
* Returns a Promise that resolves to an array of Conversation objects.
|
* 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()
|
||||||
|
|
||||||
@ -46,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)
|
||||||
@ -66,7 +66,7 @@ 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(`${JSONConversationalPlugin._homeDir}/${conversation.id}`)
|
.mkdir(`${JSONConversationalPlugin._homeDir}/${conversation.id}`)
|
||||||
.then(() =>
|
.then(() =>
|
||||||
|
|||||||
@ -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
|
|
||||||
};
|
|
||||||
@ -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";
|
||||||
@ -70,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 = 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;
|
||||||
},
|
},
|
||||||
@ -108,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);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -42,7 +42,7 @@ export default class JanModelPlugin implements ModelPlugin {
|
|||||||
*/
|
*/
|
||||||
async downloadModel(model: Model): Promise<void> {
|
async downloadModel(model: Model): Promise<void> {
|
||||||
// create corresponding directory
|
// create corresponding directory
|
||||||
const directoryPath = join(JanModelPlugin._homeDir, model.productName)
|
const directoryPath = join(JanModelPlugin._homeDir, model.name)
|
||||||
await fs.mkdir(directoryPath)
|
await fs.mkdir(directoryPath)
|
||||||
|
|
||||||
// path to model binary
|
// path to model binary
|
||||||
@ -72,7 +72,7 @@ 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> {
|
||||||
const directoryPath = join(JanModelPlugin._homeDir, model.productName)
|
const directoryPath = join(JanModelPlugin._homeDir, model.name)
|
||||||
const jsonFilePath = join(directoryPath, `${model.id}.json`)
|
const jsonFilePath = join(directoryPath, `${model.id}.json`)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|||||||
@ -4,38 +4,31 @@ import { ReactNode, useEffect, useRef } from 'react'
|
|||||||
import {
|
import {
|
||||||
events,
|
events,
|
||||||
EventName,
|
EventName,
|
||||||
NewMessageResponse,
|
ThreadMessage,
|
||||||
PluginType,
|
PluginType,
|
||||||
ChatMessage,
|
MessageStatus,
|
||||||
} from '@janhq/core'
|
} from '@janhq/core'
|
||||||
import { Conversation, Message, MessageStatus } from '@janhq/core'
|
|
||||||
import { ConversationalPlugin, ModelPlugin } from '@janhq/core/lib/plugins'
|
import { ConversationalPlugin, ModelPlugin } from '@janhq/core/lib/plugins'
|
||||||
import { useAtomValue, useSetAtom } from 'jotai'
|
import { useAtomValue, useSetAtom } from 'jotai'
|
||||||
|
|
||||||
import { useDownloadState } from '@/hooks/useDownloadState'
|
import { useDownloadState } from '@/hooks/useDownloadState'
|
||||||
import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
|
import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
|
||||||
|
|
||||||
import { toChatMessage } from '@/utils/message'
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
addNewMessageAtom,
|
addNewMessageAtom,
|
||||||
chatMessages,
|
chatMessages,
|
||||||
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 { pluginManager } from '@/plugin'
|
import { pluginManager } from '@/plugin'
|
||||||
|
|
||||||
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()
|
||||||
@ -52,92 +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((e) => e.id == message.conversationId)
|
const convo = convoRef.current.find((e) => e.id == message.threadId)
|
||||||
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) {
|
|
||||||
const updatedConv: Conversation = {
|
|
||||||
...currentConversation,
|
|
||||||
lastMessage: messageResponse.message,
|
|
||||||
}
|
|
||||||
|
|
||||||
updateConversation(updatedConv)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleMessageResponseFinished(
|
async function handleMessageResponseFinished(messageResponse: ThreadMessage) {
|
||||||
messageResponse: NewMessageResponse
|
if (!messageResponse.threadId || !convoRef.current) return
|
||||||
) {
|
updateConvWaiting(messageResponse.threadId, false)
|
||||||
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) {
|
if (thread) {
|
||||||
const messagesData = (messagesRef.current ?? [])[convo.id].map<Message>(
|
|
||||||
(e: ChatMessage) => ({
|
|
||||||
id: e.id,
|
|
||||||
message: e.text,
|
|
||||||
user: e.senderUid,
|
|
||||||
updatedAt: new Date(e.createdAt).toISOString(),
|
|
||||||
createdAt: new Date(e.createdAt).toISOString(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
pluginManager
|
pluginManager
|
||||||
.get<ConversationalPlugin>(PluginType.Conversational)
|
.get<ConversationalPlugin>(PluginType.Conversational)
|
||||||
?.saveConversation({
|
?.saveConversation({
|
||||||
...convo,
|
...thread,
|
||||||
id: convo.id ?? '',
|
id: thread.id ?? '',
|
||||||
name: convo.name ?? '',
|
messages: messagesRef.current[thread.id] ?? [],
|
||||||
message: convo.lastMessage ?? '',
|
|
||||||
messages: messagesData,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -149,7 +105,7 @@ 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)
|
||||||
|
|||||||
@ -27,6 +27,11 @@ 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) {
|
||||||
@ -60,7 +65,7 @@ export default function EventListenerWrapper({ children }: PropsWithChildren) {
|
|||||||
.get<ModelPlugin>(PluginType.Model)
|
.get<ModelPlugin>(PluginType.Model)
|
||||||
?.saveModel(model)
|
?.saveModel(model)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setDownloadedModels([...downloadedModels, model])
|
setDownloadedModels([...downloadedModelRef.current, model])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,17 +1,21 @@
|
|||||||
import { ChatMessage, MessageStatus } from '@janhq/core'
|
import { MessageStatus, ThreadMessage } from '@janhq/core'
|
||||||
import { atom } from 'jotai'
|
import { atom } from 'jotai'
|
||||||
|
|
||||||
import { getActiveConvoIdAtom } from './Conversation.atom'
|
import {
|
||||||
|
conversationStatesAtom,
|
||||||
|
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]
|
||||||
@ -20,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
|
||||||
@ -34,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
|
||||||
@ -48,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
|
||||||
@ -65,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] = []
|
||||||
@ -101,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -130,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,6 +1,8 @@
|
|||||||
import { Conversation, ConversationState } from '@janhq/core'
|
import { Thread } from '@janhq/core'
|
||||||
import { atom } from 'jotai'
|
import { atom } from 'jotai'
|
||||||
|
|
||||||
|
import { ThreadState } from '@/types/conversation'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores the current active conversation id.
|
* Stores the current active conversation id.
|
||||||
*/
|
*/
|
||||||
@ -19,23 +21,19 @@ 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) => {
|
||||||
)
|
const activeConvoId = get(activeConversationIdAtom)
|
||||||
export const currentConvoStateAtom = atom<ConversationState | undefined>(
|
if (!activeConvoId) {
|
||||||
(get) => {
|
console.debug('Active convo id is undefined')
|
||||||
const activeConvoId = get(activeConversationIdAtom)
|
return undefined
|
||||||
if (!activeConvoId) {
|
|
||||||
console.debug('Active convo id is undefined')
|
|
||||||
return 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)
|
||||||
@ -73,16 +71,28 @@ export const updateConversationHasMoreAtom = atom(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const updateThreadStateLastMessageAtom = atom(
|
||||||
|
null,
|
||||||
|
(get, set, conversationId: string, lastMessage?: string) => {
|
||||||
|
const currentState = { ...get(conversationStatesAtom) }
|
||||||
|
currentState[conversationId] = {
|
||||||
|
...currentState[conversationId],
|
||||||
|
lastMessage,
|
||||||
|
}
|
||||||
|
set(conversationStatesAtom, currentState)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
export const updateConversationAtom = atom(
|
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
|
||||||
@ -99,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'
|
||||||
@ -10,7 +12,6 @@ import { toaster } from '@/containers/Toast'
|
|||||||
import { useGetDownloadedModels } from './useGetDownloadedModels'
|
import { useGetDownloadedModels } from './useGetDownloadedModels'
|
||||||
|
|
||||||
import { pluginManager } from '@/plugin'
|
import { pluginManager } from '@/plugin'
|
||||||
import { join } from 'path'
|
|
||||||
|
|
||||||
const activeAssistantModelAtom = atom<Model | undefined>(undefined)
|
const activeAssistantModelAtom = atom<Model | undefined>(undefined)
|
||||||
|
|
||||||
@ -43,7 +44,7 @@ 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.productName, modelId)
|
const path = join('models', model.name, modelId)
|
||||||
const res = await initModel(path)
|
const res = await initModel(path)
|
||||||
if (res?.error) {
|
if (res?.error) {
|
||||||
const errorMessage = `${res.error}`
|
const errorMessage = `${res.error}`
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { PluginType } from '@janhq/core'
|
import { PluginType } from '@janhq/core'
|
||||||
import { Conversation, Model } from '@janhq/core'
|
import { Thread, Model } from '@janhq/core'
|
||||||
import { ConversationalPlugin } from '@janhq/core/lib/plugins'
|
import { ConversationalPlugin } from '@janhq/core/lib/plugins'
|
||||||
import { useAtom, useSetAtom } from 'jotai'
|
import { useAtom, useSetAtom } from 'jotai'
|
||||||
|
|
||||||
@ -20,11 +20,11 @@ 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 = {
|
||||||
id: generateConversationId(),
|
id: generateConversationId(),
|
||||||
modelId: model.id,
|
modelId: model.id,
|
||||||
name: conversationName,
|
summary,
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
messages: [],
|
messages: [],
|
||||||
@ -37,11 +37,7 @@ export const useCreateConversation = () => {
|
|||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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'
|
||||||
@ -7,13 +9,12 @@ import { toaster } from '@/containers/Toast'
|
|||||||
import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
|
import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
|
||||||
|
|
||||||
import { pluginManager } from '@/plugin/PluginManager'
|
import { pluginManager } from '@/plugin/PluginManager'
|
||||||
import { join } from 'path'
|
|
||||||
|
|
||||||
export default function useDeleteModel() {
|
export default function useDeleteModel() {
|
||||||
const { setDownloadedModels, downloadedModels } = useGetDownloadedModels()
|
const { setDownloadedModels, downloadedModels } = useGetDownloadedModels()
|
||||||
|
|
||||||
const deleteModel = async (model: Model) => {
|
const deleteModel = async (model: Model) => {
|
||||||
const path = join('models', model.productName, model.id)
|
const path = join('models', model.name, model.id)
|
||||||
await pluginManager.get<ModelPlugin>(PluginType.Model)?.deleteModel(path)
|
await pluginManager.get<ModelPlugin>(PluginType.Model)?.deleteModel(path)
|
||||||
|
|
||||||
// reload models
|
// reload models
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { useAtom } from 'jotai'
|
|||||||
import { useDownloadState } from './useDownloadState'
|
import { useDownloadState } from './useDownloadState'
|
||||||
|
|
||||||
import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom'
|
import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom'
|
||||||
|
|
||||||
import { pluginManager } from '@/plugin/PluginManager'
|
import { pluginManager } from '@/plugin/PluginManager'
|
||||||
|
|
||||||
export default function useDownloadModel() {
|
export default function useDownloadModel() {
|
||||||
@ -20,28 +21,24 @@ export default function useDownloadModel() {
|
|||||||
modelVersion: ModelVersion
|
modelVersion: ModelVersion
|
||||||
): Model => {
|
): Model => {
|
||||||
return {
|
return {
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
/**
|
||||||
id: modelVersion.id,
|
* Id will be used for the model file name
|
||||||
name: modelVersion.name,
|
* Should be the version name
|
||||||
quantMethod: modelVersion.quantMethod,
|
*/
|
||||||
|
id: modelVersion.name,
|
||||||
|
name: model.name,
|
||||||
|
quantizationName: modelVersion.quantizationName,
|
||||||
bits: modelVersion.bits,
|
bits: modelVersion.bits,
|
||||||
size: modelVersion.size,
|
size: modelVersion.size,
|
||||||
maxRamRequired: modelVersion.maxRamRequired,
|
maxRamRequired: modelVersion.maxRamRequired,
|
||||||
usecase: modelVersion.usecase,
|
usecase: modelVersion.usecase,
|
||||||
downloadLink: modelVersion.downloadLink,
|
downloadLink: modelVersion.downloadLink,
|
||||||
startDownloadAt: modelVersion.startDownloadAt,
|
|
||||||
finishDownloadAt: modelVersion.finishDownloadAt,
|
|
||||||
productId: model.id,
|
|
||||||
productName: model.name,
|
|
||||||
shortDescription: model.shortDescription,
|
shortDescription: model.shortDescription,
|
||||||
longDescription: model.longDescription,
|
longDescription: model.longDescription,
|
||||||
avatarUrl: model.avatarUrl,
|
avatarUrl: model.avatarUrl,
|
||||||
author: model.author,
|
author: model.author,
|
||||||
version: model.version,
|
version: model.version,
|
||||||
modelUrl: model.modelUrl,
|
modelUrl: model.modelUrl,
|
||||||
createdAt: new Date(model.createdAt).getTime(),
|
|
||||||
updatedAt: new Date(model.updatedAt ?? '').getTime(),
|
|
||||||
status: '',
|
|
||||||
releaseDate: -1,
|
releaseDate: -1,
|
||||||
tags: model.tags,
|
tags: model.tags,
|
||||||
}
|
}
|
||||||
@ -53,7 +50,7 @@ export default function useDownloadModel() {
|
|||||||
) => {
|
) => {
|
||||||
// set an initial download state
|
// set an initial download state
|
||||||
setDownloadState({
|
setDownloadState({
|
||||||
modelId: modelVersion.id,
|
modelId: modelVersion.name,
|
||||||
time: {
|
time: {
|
||||||
elapsed: 0,
|
elapsed: 0,
|
||||||
remaining: 0,
|
remaining: 0,
|
||||||
@ -64,10 +61,9 @@ export default function useDownloadModel() {
|
|||||||
total: 0,
|
total: 0,
|
||||||
transferred: 0,
|
transferred: 0,
|
||||||
},
|
},
|
||||||
fileName: modelVersion.id,
|
fileName: modelVersion.name,
|
||||||
})
|
})
|
||||||
|
|
||||||
modelVersion.startDownloadAt = Date.now()
|
|
||||||
const assistantModel = assistanModel(model, modelVersion)
|
const assistantModel = assistanModel(model, modelVersion)
|
||||||
|
|
||||||
setDownloadingModels([...downloadingModels, assistantModel])
|
setDownloadingModels([...downloadingModels, assistantModel])
|
||||||
|
|||||||
@ -1,8 +1,11 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Model, Conversation } from '@janhq/core'
|
|
||||||
|
import { Model, Thread } from '@janhq/core'
|
||||||
import { useAtomValue } from 'jotai'
|
import { useAtomValue } from 'jotai'
|
||||||
|
|
||||||
import { useActiveModel } from './useActiveModel'
|
import { useActiveModel } from './useActiveModel'
|
||||||
import { useGetDownloadedModels } from './useGetDownloadedModels'
|
import { useGetDownloadedModels } from './useGetDownloadedModels'
|
||||||
|
|
||||||
import { currentConversationAtom } from '@/helpers/atoms/Conversation.atom'
|
import { currentConversationAtom } from '@/helpers/atoms/Conversation.atom'
|
||||||
|
|
||||||
export default function useGetInputState() {
|
export default function useGetInputState() {
|
||||||
@ -12,7 +15,7 @@ export default function useGetInputState() {
|
|||||||
const { downloadedModels } = useGetDownloadedModels()
|
const { downloadedModels } = useGetDownloadedModels()
|
||||||
|
|
||||||
const handleInputState = (
|
const handleInputState = (
|
||||||
convo: Conversation | undefined,
|
convo: Thread | undefined,
|
||||||
currentModel: Model | undefined
|
currentModel: Model | undefined
|
||||||
) => {
|
) => {
|
||||||
if (convo == null) return
|
if (convo == null) return
|
||||||
|
|||||||
@ -1,16 +1,14 @@
|
|||||||
import { PluginType, ChatMessage, ConversationState } from '@janhq/core'
|
import { PluginType, Thread } from '@janhq/core'
|
||||||
import { ConversationalPlugin } from '@janhq/core/lib/plugins'
|
import { ConversationalPlugin } from '@janhq/core/lib/plugins'
|
||||||
import { Conversation } from '@janhq/core/lib/types'
|
|
||||||
import { useSetAtom } from 'jotai'
|
import { useSetAtom } from 'jotai'
|
||||||
|
|
||||||
import { toChatMessage } from '@/utils/message'
|
|
||||||
|
|
||||||
import { setConvoMessagesAtom } from '@/helpers/atoms/ChatMessage.atom'
|
import { setConvoMessagesAtom } from '@/helpers/atoms/ChatMessage.atom'
|
||||||
import {
|
import {
|
||||||
conversationStatesAtom,
|
conversationStatesAtom,
|
||||||
userConversationsAtom,
|
userConversationsAtom,
|
||||||
} from '@/helpers/atoms/Conversation.atom'
|
} from '@/helpers/atoms/Conversation.atom'
|
||||||
import { pluginManager } from '@/plugin/PluginManager'
|
import { pluginManager } from '@/plugin/PluginManager'
|
||||||
|
import { ThreadState } from '@/types/conversation'
|
||||||
|
|
||||||
const useGetUserConversations = () => {
|
const useGetUserConversations = () => {
|
||||||
const setConversationStates = useSetAtom(conversationStatesAtom)
|
const setConversationStates = useSetAtom(conversationStatesAtom)
|
||||||
@ -19,19 +17,17 @@ const useGetUserConversations = () => {
|
|||||||
|
|
||||||
const getUserConversations = async () => {
|
const getUserConversations = async () => {
|
||||||
try {
|
try {
|
||||||
const convos: Conversation[] | undefined = await pluginManager
|
const convos: Thread[] | undefined = await pluginManager
|
||||||
.get<ConversationalPlugin>(PluginType.Conversational)
|
.get<ConversationalPlugin>(PluginType.Conversational)
|
||||||
?.getConversations()
|
?.getConversations()
|
||||||
const convoStates: Record<string, ConversationState> = {}
|
const convoStates: Record<string, ThreadState> = {}
|
||||||
convos?.forEach((convo) => {
|
convos?.forEach((convo) => {
|
||||||
convoStates[convo.id ?? ''] = {
|
convoStates[convo.id ?? ''] = {
|
||||||
hasMore: true,
|
hasMore: true,
|
||||||
waitingForResponse: false,
|
waitingForResponse: false,
|
||||||
|
lastMessage: convo.messages[0]?.content ?? '',
|
||||||
}
|
}
|
||||||
setConvoMessages(
|
setConvoMessages(convo.messages, convo.id ?? '')
|
||||||
convo.messages.map<ChatMessage>((msg) => toChatMessage(msg)),
|
|
||||||
convo.id ?? ''
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
setConversationStates(convoStates)
|
setConversationStates(convoStates)
|
||||||
setConversations(convos ?? [])
|
setConversations(convos ?? [])
|
||||||
|
|||||||
@ -1,18 +1,21 @@
|
|||||||
import {
|
import {
|
||||||
|
ChatCompletionMessage,
|
||||||
|
ChatCompletionRole,
|
||||||
EventName,
|
EventName,
|
||||||
MessageHistory,
|
MessageRequest,
|
||||||
NewMessageRequest,
|
MessageStatus,
|
||||||
PluginType,
|
PluginType,
|
||||||
|
Thread,
|
||||||
|
ThreadMessage,
|
||||||
events,
|
events,
|
||||||
ChatMessage,
|
|
||||||
Message,
|
|
||||||
Conversation,
|
|
||||||
MessageSenderType,
|
|
||||||
} from '@janhq/core'
|
} from '@janhq/core'
|
||||||
import { ConversationalPlugin, InferencePlugin } from '@janhq/core/lib/plugins'
|
import { ConversationalPlugin, InferencePlugin } from '@janhq/core/lib/plugins'
|
||||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
|
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
|
||||||
import { currentPromptAtom } from '@/containers/Providers/Jotai'
|
|
||||||
import { ulid } from 'ulid'
|
import { ulid } from 'ulid'
|
||||||
|
|
||||||
|
import { currentPromptAtom } from '@/containers/Providers/Jotai'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
addNewMessageAtom,
|
addNewMessageAtom,
|
||||||
getCurrentChatMessagesAtom,
|
getCurrentChatMessagesAtom,
|
||||||
@ -23,7 +26,6 @@ import {
|
|||||||
updateConversationWaitingForResponseAtom,
|
updateConversationWaitingForResponseAtom,
|
||||||
} from '@/helpers/atoms/Conversation.atom'
|
} from '@/helpers/atoms/Conversation.atom'
|
||||||
import { pluginManager } from '@/plugin/PluginManager'
|
import { pluginManager } from '@/plugin/PluginManager'
|
||||||
import { toChatMessage } from '@/utils/message'
|
|
||||||
|
|
||||||
export default function useSendChatMessage() {
|
export default function useSendChatMessage() {
|
||||||
const currentConvo = useAtomValue(currentConversationAtom)
|
const currentConvo = useAtomValue(currentConversationAtom)
|
||||||
@ -35,7 +37,7 @@ export default function useSendChatMessage() {
|
|||||||
|
|
||||||
let timeout: NodeJS.Timeout | undefined = undefined
|
let timeout: NodeJS.Timeout | undefined = undefined
|
||||||
|
|
||||||
function updateConvSummary(newMessage: NewMessageRequest) {
|
function updateConvSummary(newMessage: MessageRequest) {
|
||||||
if (timeout) {
|
if (timeout) {
|
||||||
clearTimeout(timeout)
|
clearTimeout(timeout)
|
||||||
}
|
}
|
||||||
@ -46,13 +48,19 @@ export default function useSendChatMessage() {
|
|||||||
currentConvo.summary === '' ||
|
currentConvo.summary === '' ||
|
||||||
currentConvo.summary.startsWith('Prompt:')
|
currentConvo.summary.startsWith('Prompt:')
|
||||||
) {
|
) {
|
||||||
|
const summaryMsg: ChatCompletionMessage = {
|
||||||
|
role: ChatCompletionRole.User,
|
||||||
|
content:
|
||||||
|
'summary this conversation in 5 words, the response should just include the summary',
|
||||||
|
}
|
||||||
// Request convo summary
|
// Request convo summary
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
newMessage.message =
|
|
||||||
'summary this conversation in 5 words, the response should just include the summary'
|
|
||||||
const result = await pluginManager
|
const result = await pluginManager
|
||||||
.get<InferencePlugin>(PluginType.Inference)
|
.get<InferencePlugin>(PluginType.Inference)
|
||||||
?.inferenceRequest(newMessage)
|
?.inferenceRequest({
|
||||||
|
...newMessage,
|
||||||
|
messages: newMessage.messages?.concat([summaryMsg]),
|
||||||
|
})
|
||||||
|
|
||||||
if (
|
if (
|
||||||
result?.message &&
|
result?.message &&
|
||||||
@ -68,15 +76,7 @@ export default function useSendChatMessage() {
|
|||||||
.get<ConversationalPlugin>(PluginType.Conversational)
|
.get<ConversationalPlugin>(PluginType.Conversational)
|
||||||
?.saveConversation({
|
?.saveConversation({
|
||||||
...updatedConv,
|
...updatedConv,
|
||||||
name: updatedConv.name ?? '',
|
messages: currentMessages,
|
||||||
message: updatedConv.lastMessage ?? '',
|
|
||||||
messages: currentMessages.map<Message>((e: ChatMessage) => ({
|
|
||||||
id: e.id,
|
|
||||||
message: e.text,
|
|
||||||
user: e.senderUid,
|
|
||||||
updatedAt: new Date(e.createdAt).toISOString(),
|
|
||||||
createdAt: new Date(e.createdAt).toISOString(),
|
|
||||||
})),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, 1000)
|
}, 1000)
|
||||||
@ -85,64 +85,60 @@ export default function useSendChatMessage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sendChatMessage = async () => {
|
const sendChatMessage = async () => {
|
||||||
const convoId = currentConvo?.id
|
const threadId = currentConvo?.id
|
||||||
if (!convoId) {
|
if (!threadId) {
|
||||||
console.error('No conversation id')
|
console.error('No conversation id')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setCurrentPrompt('')
|
setCurrentPrompt('')
|
||||||
updateConvWaiting(convoId, true)
|
updateConvWaiting(threadId, true)
|
||||||
|
|
||||||
const prompt = currentPrompt.trim()
|
const prompt = currentPrompt.trim()
|
||||||
const messageHistory: MessageHistory[] = currentMessages
|
const messages: ChatCompletionMessage[] = currentMessages
|
||||||
.map((msg) => ({
|
.map<ChatCompletionMessage>((msg) => ({
|
||||||
role: msg.senderUid,
|
role: msg.role ?? ChatCompletionRole.User,
|
||||||
content: msg.text ?? '',
|
content: msg.content ?? '',
|
||||||
}))
|
}))
|
||||||
.reverse()
|
.reverse()
|
||||||
.concat([
|
.concat([
|
||||||
{
|
{
|
||||||
role: MessageSenderType.User,
|
role: ChatCompletionRole.User,
|
||||||
content: prompt,
|
content: prompt,
|
||||||
} as MessageHistory,
|
} as ChatCompletionMessage,
|
||||||
])
|
])
|
||||||
const newMessage: NewMessageRequest = {
|
const messageRequest: MessageRequest = {
|
||||||
id: ulid(),
|
id: ulid(),
|
||||||
conversationId: convoId,
|
threadId: threadId,
|
||||||
message: prompt,
|
messages,
|
||||||
user: MessageSenderType.User,
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
history: messageHistory,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const newChatMessage = toChatMessage(newMessage)
|
const threadMessage: ThreadMessage = {
|
||||||
addNewMessage(newChatMessage)
|
id: messageRequest.id,
|
||||||
|
threadId: messageRequest.threadId,
|
||||||
|
content: prompt,
|
||||||
|
role: ChatCompletionRole.User,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
status: MessageStatus.Ready,
|
||||||
|
}
|
||||||
|
addNewMessage(threadMessage)
|
||||||
|
|
||||||
// delay randomly from 50 - 100ms
|
// delay randomly from 50 - 100ms
|
||||||
// to prevent duplicate message id
|
// to prevent duplicate message id
|
||||||
const delay = Math.floor(Math.random() * 50) + 50
|
const delay = Math.floor(Math.random() * 50) + 50
|
||||||
await new Promise((resolve) => setTimeout(resolve, delay))
|
await new Promise((resolve) => setTimeout(resolve, delay))
|
||||||
|
|
||||||
events.emit(EventName.OnNewMessageRequest, newMessage)
|
events.emit(EventName.OnNewMessageRequest, messageRequest)
|
||||||
if (!currentConvo?.summary && currentConvo) {
|
if (!currentConvo?.summary && currentConvo) {
|
||||||
const updatedConv: Conversation = {
|
const updatedConv: Thread = {
|
||||||
...currentConvo,
|
...currentConvo,
|
||||||
lastMessage: prompt,
|
|
||||||
summary: `Prompt: ${prompt}`,
|
summary: `Prompt: ${prompt}`,
|
||||||
}
|
}
|
||||||
|
|
||||||
updateConversation(updatedConv)
|
|
||||||
} else if (currentConvo) {
|
|
||||||
const updatedConv: Conversation = {
|
|
||||||
...currentConvo,
|
|
||||||
lastMessage: prompt,
|
|
||||||
}
|
|
||||||
|
|
||||||
updateConversation(updatedConv)
|
updateConversation(updatedConv)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateConvSummary(newMessage)
|
updateConvSummary(messageRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -10,7 +10,7 @@ const ChatBody: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col-reverse overflow-y-auto">
|
<div className="flex h-full w-full flex-col-reverse overflow-y-auto">
|
||||||
{messages.map((message) => (
|
{messages.map((message) => (
|
||||||
<ChatItem message={message} key={message.id} />
|
<ChatItem {...message} key={message.id} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,24 +1,14 @@
|
|||||||
import React, { forwardRef } from 'react'
|
import React, { forwardRef } from 'react'
|
||||||
import { ChatMessage } from '@janhq/core'
|
|
||||||
import SimpleTextMessage from '../SimpleTextMessage'
|
|
||||||
|
|
||||||
type Props = {
|
import { ThreadMessage } from '@janhq/core'
|
||||||
message: ChatMessage
|
|
||||||
}
|
import SimpleTextMessage from '../SimpleTextMessage'
|
||||||
|
|
||||||
type Ref = HTMLDivElement
|
type Ref = HTMLDivElement
|
||||||
|
|
||||||
const ChatItem = forwardRef<Ref, Props>(({ message }, ref) => (
|
const ChatItem = forwardRef<Ref, ThreadMessage>((message, ref) => (
|
||||||
<div ref={ref} className="py-4 even:bg-secondary dark:even:bg-secondary/20">
|
<div ref={ref} className="py-4 even:bg-secondary dark:even:bg-secondary/20">
|
||||||
<SimpleTextMessage
|
<SimpleTextMessage {...message} />
|
||||||
status={message.status}
|
|
||||||
key={message.id}
|
|
||||||
avatarUrl={message.senderAvatarUrl}
|
|
||||||
senderName={message.senderName}
|
|
||||||
createdAt={message.createdAt}
|
|
||||||
senderType={message.messageSenderType}
|
|
||||||
text={message.text}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
import { Conversation, Model } from '@janhq/core/lib/types'
|
import { Thread, Model } from '@janhq/core'
|
||||||
import { Button } from '@janhq/uikit'
|
import { Button } from '@janhq/uikit'
|
||||||
import { motion as m } from 'framer-motion'
|
import { motion as m } from 'framer-motion'
|
||||||
import { useAtomValue, useSetAtom } from 'jotai'
|
import { useAtomValue, useSetAtom } from 'jotai'
|
||||||
@ -17,6 +17,7 @@ import useGetUserConversations from '@/hooks/useGetUserConversations'
|
|||||||
import { displayDate } from '@/utils/datetime'
|
import { displayDate } from '@/utils/datetime'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
conversationStatesAtom,
|
||||||
getActiveConvoIdAtom,
|
getActiveConvoIdAtom,
|
||||||
setActiveConvoIdAtom,
|
setActiveConvoIdAtom,
|
||||||
userConversationsAtom,
|
userConversationsAtom,
|
||||||
@ -24,6 +25,7 @@ import {
|
|||||||
|
|
||||||
export default function HistoryList() {
|
export default function HistoryList() {
|
||||||
const conversations = useAtomValue(userConversationsAtom)
|
const conversations = useAtomValue(userConversationsAtom)
|
||||||
|
const threadStates = useAtomValue(conversationStatesAtom)
|
||||||
const { getUserConversations } = useGetUserConversations()
|
const { getUserConversations } = useGetUserConversations()
|
||||||
const { activeModel, startModel } = useActiveModel()
|
const { activeModel, startModel } = useActiveModel()
|
||||||
const { requestCreateConvo } = useCreateConversation()
|
const { requestCreateConvo } = useCreateConversation()
|
||||||
@ -41,7 +43,7 @@ export default function HistoryList() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleActiveModel = async (convo: Conversation) => {
|
const handleActiveModel = async (convo: Thread) => {
|
||||||
if (convo.modelId == null) {
|
if (convo.modelId == null) {
|
||||||
console.debug('modelId is undefined')
|
console.debug('modelId is undefined')
|
||||||
return
|
return
|
||||||
@ -83,6 +85,7 @@ export default function HistoryList() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
conversations.map((convo, i) => {
|
conversations.map((convo, i) => {
|
||||||
|
const lastMessage = threadStates[convo.id]?.lastMessage
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
@ -90,15 +93,18 @@ export default function HistoryList() {
|
|||||||
'relative flex cursor-pointer flex-col border-b border-border px-4 py-2 hover:bg-secondary/20',
|
'relative flex cursor-pointer flex-col border-b border-border px-4 py-2 hover:bg-secondary/20',
|
||||||
activeConvoId === convo.id && 'bg-secondary-10'
|
activeConvoId === convo.id && 'bg-secondary-10'
|
||||||
)}
|
)}
|
||||||
onClick={() => handleActiveModel(convo as Conversation)}
|
onClick={() => handleActiveModel(convo as Thread)}
|
||||||
>
|
>
|
||||||
<p className="mb-1 line-clamp-1 text-xs leading-5">
|
<p className="mb-1 line-clamp-1 text-xs leading-5">
|
||||||
{convo.updatedAt &&
|
{convo.updatedAt &&
|
||||||
displayDate(new Date(convo.updatedAt).getTime())}
|
displayDate(new Date(convo.updatedAt).getTime())}
|
||||||
</p>
|
</p>
|
||||||
<h2 className="line-clamp-1">{convo.summary ?? convo.name}</h2>
|
<h2 className="line-clamp-1">{convo.summary}</h2>
|
||||||
<p className="mt-1 line-clamp-2 text-xs">
|
<p className="mt-1 line-clamp-2 text-xs">
|
||||||
{convo?.lastMessage ?? 'No new message'}
|
{/* TODO: Check latest message update */}
|
||||||
|
{lastMessage && lastMessage.length > 0
|
||||||
|
? lastMessage
|
||||||
|
: 'No new message'}
|
||||||
</p>
|
</p>
|
||||||
{activeModel && activeConvoId === convo.id && (
|
{activeModel && activeConvoId === convo.id && (
|
||||||
<m.div
|
<m.div
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
import React, { useState } from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import { MessageSenderType, MessageStatus } from '@janhq/core'
|
import { ChatCompletionRole, ThreadMessage } from '@janhq/core'
|
||||||
import hljs from 'highlight.js'
|
import hljs from 'highlight.js'
|
||||||
|
|
||||||
import { Marked } from 'marked'
|
import { Marked } from 'marked'
|
||||||
@ -16,15 +16,6 @@ import BubbleLoader from '@/containers/Loader/Bubble'
|
|||||||
|
|
||||||
import { displayDate } from '@/utils/datetime'
|
import { displayDate } from '@/utils/datetime'
|
||||||
|
|
||||||
type Props = {
|
|
||||||
avatarUrl: string
|
|
||||||
senderName: string
|
|
||||||
createdAt: number
|
|
||||||
senderType: MessageSenderType
|
|
||||||
status: MessageStatus
|
|
||||||
text?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const marked = new Marked(
|
const marked = new Marked(
|
||||||
markedHighlight({
|
markedHighlight({
|
||||||
langPrefix: 'hljs',
|
langPrefix: 'hljs',
|
||||||
@ -50,16 +41,9 @@ const marked = new Marked(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const SimpleTextMessage: React.FC<Props> = ({
|
const SimpleTextMessage: React.FC<ThreadMessage> = (props) => {
|
||||||
senderName,
|
const parsedText = marked.parse(props.content ?? '')
|
||||||
senderType,
|
const isUser = props.role === ChatCompletionRole.User
|
||||||
createdAt,
|
|
||||||
// will use status as streaming text
|
|
||||||
// status,
|
|
||||||
text = '',
|
|
||||||
}) => {
|
|
||||||
const parsedText = marked.parse(text)
|
|
||||||
const isUser = senderType === 'user'
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto rounded-xl px-4 lg:w-3/4">
|
<div className="mx-auto rounded-xl px-4 lg:w-3/4">
|
||||||
@ -70,12 +54,12 @@ const SimpleTextMessage: React.FC<Props> = ({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{!isUser && <LogoMark width={20} />}
|
{!isUser && <LogoMark width={20} />}
|
||||||
<div className="text-sm font-extrabold ">{senderName}</div>
|
<div className="text-sm font-extrabold capitalize">{props.role}</div>
|
||||||
<p className="text-xs font-medium">{displayDate(createdAt)}</p>
|
<p className="text-xs font-medium">{displayDate(props.createdAt)}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={twMerge('w-full')}>
|
<div className={twMerge('w-full')}>
|
||||||
{text === '' ? (
|
{!props.content || props.content === '' ? (
|
||||||
<BubbleLoader />
|
<BubbleLoader />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -126,7 +126,7 @@ const ChatScreen = () => {
|
|||||||
{isEnableChat && currentConvo && (
|
{isEnableChat && currentConvo && (
|
||||||
<div className="h-[53px] flex-shrink-0 border-b border-border bg-background p-4">
|
<div className="h-[53px] flex-shrink-0 border-b border-border bg-background p-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span>{currentConvo?.name ?? ''}</span>
|
<span>{currentConvo?.summary ?? ''}</span>
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'flex items-center space-x-3',
|
'flex items-center space-x-3',
|
||||||
|
|||||||
@ -33,7 +33,7 @@ const ExploreModelItem = forwardRef<HTMLDivElement, Props>(({ model }, ref) => {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const { quantMethod, bits, maxRamRequired, usecase } = suitableModel
|
const { quantizationName, bits, maxRamRequired, usecase } = suitableModel
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -73,7 +73,9 @@ const ExploreModelItem = forwardRef<HTMLDivElement, Props>(({ model }, ref) => {
|
|||||||
<span className="font-semibold">Version</span>
|
<span className="font-semibold">Version</span>
|
||||||
<div className="mt-2 flex space-x-2">
|
<div className="mt-2 flex space-x-2">
|
||||||
<Badge themes="outline">v{model.version}</Badge>
|
<Badge themes="outline">v{model.version}</Badge>
|
||||||
{quantMethod && <Badge themes="outline">{quantMethod}</Badge>}
|
{quantizationName && (
|
||||||
|
<Badge themes="outline">{quantizationName}</Badge>
|
||||||
|
)}
|
||||||
<Badge themes="outline">{`${bits} Bits`}</Badge>
|
<Badge themes="outline">{`${bits} Bits`}</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -105,7 +107,7 @@ const ExploreModelItem = forwardRef<HTMLDivElement, Props>(({ model }, ref) => {
|
|||||||
<ModelVersionList
|
<ModelVersionList
|
||||||
model={model}
|
model={model}
|
||||||
versions={model.availableVersions}
|
versions={model.availableVersions}
|
||||||
recommendedVersion={suitableModel?.id ?? ''}
|
recommendedVersion={suitableModel?.name ?? ''}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -35,8 +35,8 @@ const ExploreModelItemHeader: React.FC<Props> = ({
|
|||||||
const { performanceTag, title, getPerformanceForModel } =
|
const { performanceTag, title, getPerformanceForModel } =
|
||||||
useGetPerformanceTag()
|
useGetPerformanceTag()
|
||||||
const downloadAtom = useMemo(
|
const downloadAtom = useMemo(
|
||||||
() => atom((get) => get(modelDownloadStateAtom)[suitableModel.id]),
|
() => atom((get) => get(modelDownloadStateAtom)[suitableModel.name]),
|
||||||
[suitableModel.id]
|
[suitableModel.name]
|
||||||
)
|
)
|
||||||
const downloadState = useAtomValue(downloadAtom)
|
const downloadState = useAtomValue(downloadAtom)
|
||||||
const { setMainViewState } = useMainViewState()
|
const { setMainViewState } = useMainViewState()
|
||||||
@ -51,8 +51,9 @@ const ExploreModelItemHeader: React.FC<Props> = ({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [exploreModel, suitableModel])
|
}, [exploreModel, suitableModel])
|
||||||
|
|
||||||
|
// TODO: Comparing between Model Id and Version Name?
|
||||||
const isDownloaded =
|
const isDownloaded =
|
||||||
downloadedModels.find((model) => model.id === suitableModel.id) != null
|
downloadedModels.find((model) => model.id === suitableModel.name) != null
|
||||||
|
|
||||||
let downloadButton = (
|
let downloadButton = (
|
||||||
<Button onClick={() => onDownloadClick()}>
|
<Button onClick={() => onDownloadClick()}>
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { ModelCatalog } from '@janhq/core/lib/types'
|
import { ModelCatalog } from '@janhq/core/lib/types'
|
||||||
|
|
||||||
import ExploreModelItem from '@/screens/ExploreModels/ExploreModelItem'
|
import ExploreModelItem from '@/screens/ExploreModels/ExploreModelItem'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -7,7 +8,9 @@ type Props = {
|
|||||||
|
|
||||||
const ExploreModelList: React.FC<Props> = ({ models }) => (
|
const ExploreModelList: React.FC<Props> = ({ models }) => (
|
||||||
<div className="relative h-full w-full flex-shrink-0">
|
<div className="relative h-full w-full flex-shrink-0">
|
||||||
{models?.map((item, i) => <ExploreModelItem key={item.id} model={item} />)}
|
{models?.map((item, i) => (
|
||||||
|
<ExploreModelItem key={item.name + '/' + item.id} model={item} />
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -1,15 +1,20 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import React, { useMemo } from 'react'
|
import React, { useMemo } from 'react'
|
||||||
|
|
||||||
import { ModelCatalog, ModelVersion } from '@janhq/core/lib/types'
|
import { ModelCatalog, ModelVersion } from '@janhq/core/lib/types'
|
||||||
import { Button } from '@janhq/uikit'
|
import { Button } from '@janhq/uikit'
|
||||||
import { Badge } from '@janhq/uikit'
|
import { Badge } from '@janhq/uikit'
|
||||||
import { atom, useAtomValue } from 'jotai'
|
import { atom, useAtomValue } from 'jotai'
|
||||||
|
|
||||||
import ModalCancelDownload from '@/containers/ModalCancelDownload'
|
import ModalCancelDownload from '@/containers/ModalCancelDownload'
|
||||||
|
|
||||||
import { MainViewState } from '@/constants/screens'
|
import { MainViewState } from '@/constants/screens'
|
||||||
|
|
||||||
import useDownloadModel from '@/hooks/useDownloadModel'
|
import useDownloadModel from '@/hooks/useDownloadModel'
|
||||||
import { useDownloadState } from '@/hooks/useDownloadState'
|
import { useDownloadState } from '@/hooks/useDownloadState'
|
||||||
import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
|
import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
|
||||||
import { useMainViewState } from '@/hooks/useMainViewState'
|
import { useMainViewState } from '@/hooks/useMainViewState'
|
||||||
|
|
||||||
import { toGigabytes } from '@/utils/converter'
|
import { toGigabytes } from '@/utils/converter'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -23,13 +28,13 @@ const ModelVersionItem: React.FC<Props> = ({ model, modelVersion }) => {
|
|||||||
const { downloadedModels } = useGetDownloadedModels()
|
const { downloadedModels } = useGetDownloadedModels()
|
||||||
const { setMainViewState } = useMainViewState()
|
const { setMainViewState } = useMainViewState()
|
||||||
const isDownloaded =
|
const isDownloaded =
|
||||||
downloadedModels.find((model) => model.id === modelVersion.id) != null
|
downloadedModels.find((model) => model.id === modelVersion.name) != null
|
||||||
|
|
||||||
const { modelDownloadStateAtom, downloadStates } = useDownloadState()
|
const { modelDownloadStateAtom, downloadStates } = useDownloadState()
|
||||||
|
|
||||||
const downloadAtom = useMemo(
|
const downloadAtom = useMemo(
|
||||||
() => atom((get) => get(modelDownloadStateAtom)[modelVersion.id ?? '']),
|
() => atom((get) => get(modelDownloadStateAtom)[modelVersion.name ?? '']),
|
||||||
[modelVersion.id]
|
[modelVersion.name]
|
||||||
)
|
)
|
||||||
const downloadState = useAtomValue(downloadAtom)
|
const downloadState = useAtomValue(downloadAtom)
|
||||||
|
|
||||||
|
|||||||
@ -17,10 +17,10 @@ export default function ModelVersionList({
|
|||||||
<div className="pt-4">
|
<div className="pt-4">
|
||||||
{versions.map((item) => (
|
{versions.map((item) => (
|
||||||
<ModelVersionItem
|
<ModelVersionItem
|
||||||
key={item.id}
|
key={item.name}
|
||||||
model={model}
|
model={model}
|
||||||
modelVersion={item}
|
modelVersion={item}
|
||||||
isRecommended={item.id === recommendedVersion}
|
isRecommended={item.name === recommendedVersion}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -76,7 +76,7 @@ const MyModelsScreen = () => {
|
|||||||
<h2 className="mb-1 font-medium capitalize">
|
<h2 className="mb-1 font-medium capitalize">
|
||||||
{model.author}
|
{model.author}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="line-clamp-1">{model.productName}</p>
|
<p className="line-clamp-1">{model.name}</p>
|
||||||
<div className="mt-2 flex items-center gap-2">
|
<div className="mt-2 flex items-center gap-2">
|
||||||
<Badge themes="secondary">v{model.version}</Badge>
|
<Badge themes="secondary">v{model.version}</Badge>
|
||||||
<Badge themes="outline">GGUF</Badge>
|
<Badge themes="outline">GGUF</Badge>
|
||||||
@ -101,7 +101,7 @@ const MyModelsScreen = () => {
|
|||||||
<ModalTitle>Are you sure?</ModalTitle>
|
<ModalTitle>Are you sure?</ModalTitle>
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<p className="leading-relaxed">
|
<p className="leading-relaxed">
|
||||||
Delete model {model.productName}, v{model.version},{' '}
|
Delete model {model.name}, v{model.version},{' '}
|
||||||
{toGigabytes(model.size)}.
|
{toGigabytes(model.size)}.
|
||||||
</p>
|
</p>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
|
|||||||
6
web/types/conversation.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export type ThreadState = {
|
||||||
|
hasMore: boolean
|
||||||
|
waitingForResponse: boolean
|
||||||
|
error?: Error
|
||||||
|
lastMessage?: string
|
||||||
|
}
|
||||||
53
web/types/searchModelParam.hf.d.ts
vendored
@ -1,53 +0,0 @@
|
|||||||
type Task =
|
|
||||||
| 'text-classification'
|
|
||||||
| 'token-classification'
|
|
||||||
| 'table-question-answering'
|
|
||||||
| 'question-answering'
|
|
||||||
| 'zero-shot-classification'
|
|
||||||
| 'translation'
|
|
||||||
| 'summarization'
|
|
||||||
| 'conversational'
|
|
||||||
| 'feature-extraction'
|
|
||||||
| 'text-generation'
|
|
||||||
| 'text2text-generation'
|
|
||||||
| 'fill-mask'
|
|
||||||
| 'sentence-similarity'
|
|
||||||
| 'text-to-speech'
|
|
||||||
| 'automatic-speech-recognition'
|
|
||||||
| 'audio-to-audio'
|
|
||||||
| 'audio-classification'
|
|
||||||
| 'voice-activity-detection'
|
|
||||||
| 'depth-estimation'
|
|
||||||
| 'image-classification'
|
|
||||||
| 'object-detection'
|
|
||||||
| 'image-segmentation'
|
|
||||||
| 'text-to-image'
|
|
||||||
| 'image-to-text'
|
|
||||||
| 'image-to-image'
|
|
||||||
| 'unconditional-image-generation'
|
|
||||||
| 'video-classification'
|
|
||||||
| 'reinforcement-learning'
|
|
||||||
| 'robotics'
|
|
||||||
| 'tabular-classification'
|
|
||||||
| 'tabular-regression'
|
|
||||||
| 'tabular-to-text'
|
|
||||||
| 'table-to-text'
|
|
||||||
| 'multiple-choice'
|
|
||||||
| 'text-retrieval'
|
|
||||||
| 'time-series-forecasting'
|
|
||||||
| 'visual-question-answering'
|
|
||||||
| 'document-question-answering'
|
|
||||||
| 'zero-shot-image-classification'
|
|
||||||
| 'graph-ml'
|
|
||||||
| 'other'
|
|
||||||
|
|
||||||
type SearchModelParamHf = {
|
|
||||||
search?: {
|
|
||||||
owner?: string
|
|
||||||
task?: Task
|
|
||||||
}
|
|
||||||
credentials?: {
|
|
||||||
accessToken: string
|
|
||||||
}
|
|
||||||
limit: number
|
|
||||||
}
|
|
||||||
11
web/types/users.d.ts
vendored
@ -1,11 +0,0 @@
|
|||||||
export enum Role {
|
|
||||||
User = 'user',
|
|
||||||
Assistant = 'assistant',
|
|
||||||
}
|
|
||||||
|
|
||||||
type User = {
|
|
||||||
id: string
|
|
||||||
displayName: string
|
|
||||||
avatarUrl: string
|
|
||||||
email?: string
|
|
||||||
}
|
|
||||||
@ -3,11 +3,11 @@ export const isToday = (timestamp: number) => {
|
|||||||
return today.setHours(0, 0, 0, 0) == new Date(timestamp).setHours(0, 0, 0, 0)
|
return today.setHours(0, 0, 0, 0) == new Date(timestamp).setHours(0, 0, 0, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const displayDate = (timestamp?: number) => {
|
export const displayDate = (timestamp?: string | number | Date) => {
|
||||||
if (!timestamp) return 'N/A'
|
if (!timestamp) return 'N/A'
|
||||||
|
|
||||||
let displayDate = new Date(timestamp).toLocaleString()
|
let displayDate = new Date(timestamp).toLocaleString()
|
||||||
if (isToday(timestamp)) {
|
if (typeof timestamp === 'number' && isToday(timestamp)) {
|
||||||
displayDate = new Date(timestamp).toLocaleTimeString(undefined, {
|
displayDate = new Date(timestamp).toLocaleTimeString(undefined, {
|
||||||
hour: '2-digit',
|
hour: '2-digit',
|
||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
|
|||||||
@ -7,18 +7,37 @@ export const dummyModel: ModelCatalog = {
|
|||||||
shortDescription: 'TinyLlama-1.1B-Chat-v0.3-GGUF',
|
shortDescription: 'TinyLlama-1.1B-Chat-v0.3-GGUF',
|
||||||
longDescription: 'https://huggingface.co/aladar/TinyLLama-v0-GGUF/tree/main',
|
longDescription: 'https://huggingface.co/aladar/TinyLLama-v0-GGUF/tree/main',
|
||||||
avatarUrl: '',
|
avatarUrl: '',
|
||||||
status: '',
|
|
||||||
releaseDate: Date.now(),
|
releaseDate: Date.now(),
|
||||||
author: 'aladar',
|
author: 'aladar',
|
||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
modelUrl: 'aladar/TinyLLama-v0-GGUF',
|
modelUrl: 'aladar/TinyLLama-v0-GGUF',
|
||||||
tags: ['freeform', 'tags'],
|
tags: ['freeform', 'tags'],
|
||||||
createdAt: 0,
|
|
||||||
availableVersions: [
|
availableVersions: [
|
||||||
{
|
{
|
||||||
id: 'tinyllama-1.1b-chat-v0.3.Q2_K.gguf',
|
name: 'TinyLLama-v0.Q8_0.gguf',
|
||||||
name: 'tinyllama-1.1b-chat-v0.3.Q2_K.gguf',
|
quantizationName: '',
|
||||||
quantMethod: '',
|
bits: 2,
|
||||||
|
size: 5816320,
|
||||||
|
maxRamRequired: 256000000,
|
||||||
|
usecase:
|
||||||
|
'smallest, significant quality loss - not recommended for most purposes',
|
||||||
|
downloadLink:
|
||||||
|
'https://huggingface.co/aladar/TinyLLama-v0-GGUF/resolve/main/TinyLLama-v0.Q8_0.gguf',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'TinyLLama-v0.f16.gguf',
|
||||||
|
quantizationName: '',
|
||||||
|
bits: 2,
|
||||||
|
size: 10240000,
|
||||||
|
maxRamRequired: 256000000,
|
||||||
|
usecase:
|
||||||
|
'smallest, significant quality loss - not recommended for most purposes',
|
||||||
|
downloadLink:
|
||||||
|
'https://huggingface.co/aladar/TinyLLama-v0-GGUF/resolve/main/TinyLLama-v0.f16.gguf',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'TinyLLama-v0.f32.gguf',
|
||||||
|
quantizationName: '',
|
||||||
bits: 2,
|
bits: 2,
|
||||||
size: 19660000,
|
size: 19660000,
|
||||||
maxRamRequired: 256000000,
|
maxRamRequired: 256000000,
|
||||||
@ -26,6 +45,6 @@ export const dummyModel: ModelCatalog = {
|
|||||||
'smallest, significant quality loss - not recommended for most purposes',
|
'smallest, significant quality loss - not recommended for most purposes',
|
||||||
downloadLink:
|
downloadLink:
|
||||||
'https://huggingface.co/aladar/TinyLLama-v0-GGUF/resolve/main/TinyLLama-v0.f32.gguf',
|
'https://huggingface.co/aladar/TinyLLama-v0-GGUF/resolve/main/TinyLLama-v0.f32.gguf',
|
||||||
} as ModelVersion,
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,48 +0,0 @@
|
|||||||
import {
|
|
||||||
ChatMessage,
|
|
||||||
Message,
|
|
||||||
MessageSenderType,
|
|
||||||
MessageStatus,
|
|
||||||
MessageType,
|
|
||||||
NewMessageResponse,
|
|
||||||
RawMessage,
|
|
||||||
} from '@janhq/core'
|
|
||||||
|
|
||||||
export const toChatMessage = (
|
|
||||||
m: RawMessage | Message | NewMessageResponse,
|
|
||||||
conversationId?: string
|
|
||||||
): ChatMessage => {
|
|
||||||
const createdAt = new Date(m.createdAt ?? '').getTime()
|
|
||||||
const imageUrls: string[] = []
|
|
||||||
const imageUrl = undefined
|
|
||||||
if (imageUrl) {
|
|
||||||
imageUrls.push(imageUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
const messageType = MessageType.Text
|
|
||||||
const messageSenderType =
|
|
||||||
m.user === 'user' ? MessageSenderType.User : MessageSenderType.Ai
|
|
||||||
|
|
||||||
const content = m.message ?? ''
|
|
||||||
|
|
||||||
const senderName = m.user === 'user' ? 'You' : 'Assistant'
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: (m.id ?? 0).toString(),
|
|
||||||
conversationId: (
|
|
||||||
(m as RawMessage | NewMessageResponse)?.conversationId ??
|
|
||||||
conversationId ??
|
|
||||||
0
|
|
||||||
).toString(),
|
|
||||||
messageType: messageType,
|
|
||||||
messageSenderType: messageSenderType,
|
|
||||||
senderUid: m.user?.toString() || '0',
|
|
||||||
senderName: senderName,
|
|
||||||
senderAvatarUrl:
|
|
||||||
m.user === 'user' ? 'icons/avatar.svg' : 'icons/app_icon.svg',
|
|
||||||
text: content,
|
|
||||||
imageUrls: imageUrls,
|
|
||||||
createdAt: createdAt,
|
|
||||||
status: MessageStatus.Ready,
|
|
||||||
}
|
|
||||||
}
|
|
||||||