refactor: entities references & logics
This commit is contained in:
parent
4823c4f3fe
commit
38b2e5e7a2
@ -1,5 +1,5 @@
|
||||
import { Thread } from "../index";
|
||||
import { JanPlugin } from "../plugin";
|
||||
import { Conversation } from "../types/index";
|
||||
|
||||
/**
|
||||
* Abstract class for conversational plugins.
|
||||
@ -17,10 +17,10 @@ export abstract class ConversationalPlugin extends JanPlugin {
|
||||
/**
|
||||
* Saves a conversation.
|
||||
* @abstract
|
||||
* @param {Conversation} conversation - The conversation to save.
|
||||
* @param {Thread} conversation - The conversation to save.
|
||||
* @returns {Promise<void>} A promise that resolves when the conversation is saved.
|
||||
*/
|
||||
abstract saveConversation(conversation: Conversation): Promise<void>;
|
||||
abstract saveConversation(conversation: Thread): Promise<void>;
|
||||
|
||||
/**
|
||||
* Deletes a conversation.
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { NewMessageRequest } from "../events";
|
||||
import { MessageRequest } from "../index";
|
||||
import { JanPlugin } from "../plugin";
|
||||
|
||||
/**
|
||||
@ -21,5 +21,5 @@ export abstract class InferencePlugin extends JanPlugin {
|
||||
* @param data - The data for the inference request.
|
||||
* @returns The result of the inference request.
|
||||
*/
|
||||
abstract inferenceRequest(data: NewMessageRequest): Promise<any>;
|
||||
abstract inferenceRequest(data: MessageRequest): Promise<any>;
|
||||
}
|
||||
|
||||
@ -43,7 +43,7 @@ export type MessageRequest = {
|
||||
|
||||
/**
|
||||
* The status of the message.
|
||||
* @presentation_object
|
||||
* @data_transfer_object
|
||||
*/
|
||||
export enum MessageStatus {
|
||||
/** Message is fully loaded. **/
|
||||
@ -53,7 +53,7 @@ export enum MessageStatus {
|
||||
}
|
||||
/**
|
||||
* The `ThreadMessage` type defines the shape of a thread's message object.
|
||||
* @stored_in_workspace
|
||||
* @stored
|
||||
*/
|
||||
export type ThreadMessage = {
|
||||
/** Unique identifier for the message, generated by default using the ULID method. **/
|
||||
@ -72,7 +72,7 @@ export type ThreadMessage = {
|
||||
|
||||
/**
|
||||
* The `Thread` type defines the shape of a thread object.
|
||||
* @stored_in_workspace
|
||||
* @stored
|
||||
*/
|
||||
export interface Thread {
|
||||
/** Unique identifier for the thread, generated by default using the ULID method. **/
|
||||
@ -95,7 +95,7 @@ export interface Thread {
|
||||
|
||||
/**
|
||||
* Model type defines the shape of a model object.
|
||||
* @stored_in_workspace
|
||||
* @stored
|
||||
*/
|
||||
export interface Model {
|
||||
/** Combination of owner and model name.*/
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { PluginType, fs } from '@janhq/core'
|
||||
import { ConversationalPlugin } from '@janhq/core/lib/plugins'
|
||||
import { Conversation } from '@janhq/core/lib/types'
|
||||
import { Thread } from '@janhq/core/lib/types'
|
||||
import { join } from 'path'
|
||||
|
||||
/**
|
||||
@ -35,7 +35,7 @@ export default class JSONConversationalPlugin implements ConversationalPlugin {
|
||||
/**
|
||||
* Returns a Promise that resolves to an array of Conversation objects.
|
||||
*/
|
||||
async getConversations(): Promise<Conversation[]> {
|
||||
async getConversations(): Promise<Thread[]> {
|
||||
try {
|
||||
const convoIds = await this.getConversationDocs()
|
||||
|
||||
@ -46,7 +46,7 @@ export default class JSONConversationalPlugin implements ConversationalPlugin {
|
||||
const convos = promiseResults
|
||||
.map((result) => {
|
||||
if (result.status === 'fulfilled') {
|
||||
return JSON.parse(result.value) as Conversation
|
||||
return JSON.parse(result.value) as Thread
|
||||
}
|
||||
})
|
||||
.filter((convo) => convo != null)
|
||||
@ -66,7 +66,7 @@ export default class JSONConversationalPlugin implements ConversationalPlugin {
|
||||
* Saves a Conversation object to a Markdown file.
|
||||
* @param conversation The Conversation object to save.
|
||||
*/
|
||||
saveConversation(conversation: Conversation): Promise<void> {
|
||||
saveConversation(conversation: Thread): Promise<void> {
|
||||
return fs
|
||||
.mkdir(`${JSONConversationalPlugin._homeDir}/${conversation.id}`)
|
||||
.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 {
|
||||
ChatCompletionMessage,
|
||||
ChatCompletionRole,
|
||||
EventName,
|
||||
MessageHistory,
|
||||
NewMessageRequest,
|
||||
MessageRequest,
|
||||
MessageStatus,
|
||||
PluginType,
|
||||
ThreadMessage,
|
||||
events,
|
||||
executeOnMain,
|
||||
} from "@janhq/core";
|
||||
@ -70,29 +73,19 @@ export default class JanInferencePlugin implements InferencePlugin {
|
||||
|
||||
/**
|
||||
* Makes a single response inference request.
|
||||
* @param {NewMessageRequest} data - The data for the inference request.
|
||||
* @param {MessageRequest} data - The data for the inference request.
|
||||
* @returns {Promise<any>} A promise that resolves with the inference response.
|
||||
*/
|
||||
async inferenceRequest(data: NewMessageRequest): Promise<any> {
|
||||
async inferenceRequest(data: MessageRequest): Promise<any> {
|
||||
const message = {
|
||||
...data,
|
||||
message: "",
|
||||
user: "assistant",
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
const prompts: [MessageHistory] = [
|
||||
{
|
||||
role: "user",
|
||||
content: data.message,
|
||||
},
|
||||
];
|
||||
const recentMessages = data.history ?? prompts;
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
requestInference([
|
||||
...recentMessages,
|
||||
{ role: "user", content: data.message },
|
||||
]).subscribe({
|
||||
requestInference(data.messages ?? []).subscribe({
|
||||
next: (content) => {
|
||||
message.message = content;
|
||||
},
|
||||
@ -108,37 +101,33 @@ export default class JanInferencePlugin implements InferencePlugin {
|
||||
|
||||
/**
|
||||
* Handles a new message request by making an inference request and emitting events.
|
||||
* @param {NewMessageRequest} data - The data for the new message request.
|
||||
* @param {MessageRequest} data - The data for the new message request.
|
||||
*/
|
||||
private async handleMessageRequest(data: NewMessageRequest) {
|
||||
const prompts: [MessageHistory] = [
|
||||
{
|
||||
role: "user",
|
||||
content: data.message,
|
||||
},
|
||||
];
|
||||
const recentMessages = data.history ?? prompts;
|
||||
const message = {
|
||||
private async handleMessageRequest(data: MessageRequest) {
|
||||
const message: ThreadMessage = {
|
||||
...data,
|
||||
message: "",
|
||||
user: "assistant",
|
||||
content: "",
|
||||
role: ChatCompletionRole.Assistant,
|
||||
createdAt: new Date().toISOString(),
|
||||
id: ulid(),
|
||||
status: MessageStatus.Pending,
|
||||
};
|
||||
events.emit(EventName.OnNewMessageResponse, message);
|
||||
|
||||
requestInference(recentMessages).subscribe({
|
||||
requestInference(data.messages).subscribe({
|
||||
next: (content) => {
|
||||
message.message = content;
|
||||
message.content = content;
|
||||
events.emit(EventName.OnMessageResponseUpdate, message);
|
||||
},
|
||||
complete: async () => {
|
||||
message.message = message.message.trim();
|
||||
message.content = message.content.trim();
|
||||
message.status = MessageStatus.Ready;
|
||||
events.emit(EventName.OnMessageResponseFinished, message);
|
||||
},
|
||||
error: async (err) => {
|
||||
message.message =
|
||||
message.message.trim() + "\n" + "Error occurred: " + err.message;
|
||||
message.content =
|
||||
message.content.trim() + "\n" + "Error occurred: " + err.message;
|
||||
message.status = MessageStatus.Ready;
|
||||
events.emit(EventName.OnMessageResponseUpdate, message);
|
||||
},
|
||||
});
|
||||
|
||||
@ -42,7 +42,7 @@ export default class JanModelPlugin implements ModelPlugin {
|
||||
*/
|
||||
async downloadModel(model: Model): Promise<void> {
|
||||
// create corresponding directory
|
||||
const directoryPath = join(JanModelPlugin._homeDir, model.productName)
|
||||
const directoryPath = join(JanModelPlugin._homeDir, model.name)
|
||||
await fs.mkdir(directoryPath)
|
||||
|
||||
// path to model binary
|
||||
@ -72,7 +72,7 @@ export default class JanModelPlugin implements ModelPlugin {
|
||||
* @returns A Promise that resolves when the model is saved.
|
||||
*/
|
||||
async saveModel(model: Model): Promise<void> {
|
||||
const directoryPath = join(JanModelPlugin._homeDir, model.productName)
|
||||
const directoryPath = join(JanModelPlugin._homeDir, model.name)
|
||||
const jsonFilePath = join(directoryPath, `${model.id}.json`)
|
||||
|
||||
try {
|
||||
|
||||
@ -32,9 +32,9 @@ export default function ModalCancelDownload({
|
||||
const { modelDownloadStateAtom } = useDownloadState()
|
||||
useGetPerformanceTag()
|
||||
const downloadAtom = useMemo(
|
||||
() => atom((get) => get(modelDownloadStateAtom)[suitableModel.id]),
|
||||
() => atom((get) => get(modelDownloadStateAtom)[suitableModel.name]),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[suitableModel.id]
|
||||
[suitableModel.name]
|
||||
)
|
||||
const downloadState = useAtomValue(downloadAtom)
|
||||
|
||||
|
||||
@ -4,38 +4,31 @@ import { ReactNode, useEffect, useRef } from 'react'
|
||||
import {
|
||||
events,
|
||||
EventName,
|
||||
NewMessageResponse,
|
||||
ThreadMessage,
|
||||
PluginType,
|
||||
ChatMessage,
|
||||
MessageStatus,
|
||||
} from '@janhq/core'
|
||||
import { Conversation, Message, MessageStatus } from '@janhq/core'
|
||||
import { ConversationalPlugin, ModelPlugin } from '@janhq/core/lib/plugins'
|
||||
import { useAtomValue, useSetAtom } from 'jotai'
|
||||
|
||||
import { useDownloadState } from '@/hooks/useDownloadState'
|
||||
import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
|
||||
|
||||
import { toChatMessage } from '@/utils/message'
|
||||
|
||||
import {
|
||||
addNewMessageAtom,
|
||||
chatMessages,
|
||||
updateMessageAtom,
|
||||
} from '@/helpers/atoms/ChatMessage.atom'
|
||||
import {
|
||||
updateConversationAtom,
|
||||
updateConversationWaitingForResponseAtom,
|
||||
userConversationsAtom,
|
||||
} from '@/helpers/atoms/Conversation.atom'
|
||||
import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom'
|
||||
import { pluginManager } from '@/plugin'
|
||||
|
||||
let currentConversation: Conversation | undefined = undefined
|
||||
|
||||
export default function EventHandler({ children }: { children: ReactNode }) {
|
||||
const addNewMessage = useSetAtom(addNewMessageAtom)
|
||||
const updateMessage = useSetAtom(updateMessageAtom)
|
||||
const updateConversation = useSetAtom(updateConversationAtom)
|
||||
|
||||
const { setDownloadState, setDownloadStateSuccess } = useDownloadState()
|
||||
const { downloadedModels, setDownloadedModels } = useGetDownloadedModels()
|
||||
@ -52,92 +45,55 @@ export default function EventHandler({ children }: { children: ReactNode }) {
|
||||
convoRef.current = conversations
|
||||
}, [messages, conversations])
|
||||
|
||||
async function handleNewMessageResponse(message: NewMessageResponse) {
|
||||
if (message.conversationId) {
|
||||
const convo = convoRef.current.find((e) => e.id == message.conversationId)
|
||||
async function handleNewMessageResponse(message: ThreadMessage) {
|
||||
if (message.threadId) {
|
||||
const convo = convoRef.current.find((e) => e.id == message.threadId)
|
||||
if (!convo) return
|
||||
const newResponse = toChatMessage(message)
|
||||
addNewMessage(newResponse)
|
||||
addNewMessage(message)
|
||||
}
|
||||
}
|
||||
async function handleMessageResponseUpdate(
|
||||
messageResponse: NewMessageResponse
|
||||
) {
|
||||
async function handleMessageResponseUpdate(messageResponse: ThreadMessage) {
|
||||
if (
|
||||
messageResponse.conversationId &&
|
||||
messageResponse.threadId &&
|
||||
messageResponse.id &&
|
||||
messageResponse.message
|
||||
messageResponse.content
|
||||
) {
|
||||
updateMessage(
|
||||
messageResponse.id,
|
||||
messageResponse.conversationId,
|
||||
messageResponse.message,
|
||||
messageResponse.threadId,
|
||||
messageResponse.content,
|
||||
MessageStatus.Pending
|
||||
)
|
||||
}
|
||||
|
||||
if (messageResponse.conversationId) {
|
||||
if (
|
||||
!currentConversation ||
|
||||
currentConversation.id !== messageResponse.conversationId
|
||||
) {
|
||||
if (convoRef.current && messageResponse.conversationId)
|
||||
currentConversation = convoRef.current.find(
|
||||
(e) => e.id == messageResponse.conversationId
|
||||
)
|
||||
}
|
||||
|
||||
if (currentConversation) {
|
||||
const updatedConv: Conversation = {
|
||||
...currentConversation,
|
||||
lastMessage: messageResponse.message,
|
||||
}
|
||||
|
||||
updateConversation(updatedConv)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleMessageResponseFinished(
|
||||
messageResponse: NewMessageResponse
|
||||
) {
|
||||
if (!messageResponse.conversationId || !convoRef.current) return
|
||||
updateConvWaiting(messageResponse.conversationId, false)
|
||||
async function handleMessageResponseFinished(messageResponse: ThreadMessage) {
|
||||
if (!messageResponse.threadId || !convoRef.current) return
|
||||
updateConvWaiting(messageResponse.threadId, false)
|
||||
|
||||
if (
|
||||
messageResponse.conversationId &&
|
||||
messageResponse.threadId &&
|
||||
messageResponse.id &&
|
||||
messageResponse.message
|
||||
messageResponse.content
|
||||
) {
|
||||
updateMessage(
|
||||
messageResponse.id,
|
||||
messageResponse.conversationId,
|
||||
messageResponse.message,
|
||||
messageResponse.threadId,
|
||||
messageResponse.content,
|
||||
MessageStatus.Ready
|
||||
)
|
||||
}
|
||||
|
||||
const convo = convoRef.current.find(
|
||||
(e) => e.id == messageResponse.conversationId
|
||||
)
|
||||
if (convo) {
|
||||
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(),
|
||||
})
|
||||
const thread = convoRef.current.find(
|
||||
(e) => e.id == messageResponse.threadId
|
||||
)
|
||||
if (thread) {
|
||||
pluginManager
|
||||
.get<ConversationalPlugin>(PluginType.Conversational)
|
||||
?.saveConversation({
|
||||
...convo,
|
||||
id: convo.id ?? '',
|
||||
name: convo.name ?? '',
|
||||
message: convo.lastMessage ?? '',
|
||||
messages: messagesData,
|
||||
...thread,
|
||||
id: thread.id ?? '',
|
||||
messages: messagesRef.current[thread.id] ?? [],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { ChatMessage, MessageStatus } from '@janhq/core'
|
||||
import { MessageStatus, ThreadMessage } from '@janhq/core'
|
||||
import { atom } from 'jotai'
|
||||
|
||||
import { getActiveConvoIdAtom } from './Conversation.atom'
|
||||
@ -6,12 +6,12 @@ import { getActiveConvoIdAtom } from './Conversation.atom'
|
||||
/**
|
||||
* Stores all chat messages for all conversations
|
||||
*/
|
||||
export const chatMessages = atom<Record<string, ChatMessage[]>>({})
|
||||
export const chatMessages = atom<Record<string, ThreadMessage[]>>({})
|
||||
|
||||
/**
|
||||
* Return the chat messages for the current active conversation
|
||||
*/
|
||||
export const getCurrentChatMessagesAtom = atom<ChatMessage[]>((get) => {
|
||||
export const getCurrentChatMessagesAtom = atom<ThreadMessage[]>((get) => {
|
||||
const activeConversationId = get(getActiveConvoIdAtom)
|
||||
if (!activeConversationId) return []
|
||||
const messages = get(chatMessages)[activeConversationId]
|
||||
@ -20,11 +20,11 @@ export const getCurrentChatMessagesAtom = atom<ChatMessage[]>((get) => {
|
||||
|
||||
export const setCurrentChatMessagesAtom = atom(
|
||||
null,
|
||||
(get, set, messages: ChatMessage[]) => {
|
||||
(get, set, messages: ThreadMessage[]) => {
|
||||
const currentConvoId = get(getActiveConvoIdAtom)
|
||||
if (!currentConvoId) return
|
||||
|
||||
const newData: Record<string, ChatMessage[]> = {
|
||||
const newData: Record<string, ThreadMessage[]> = {
|
||||
...get(chatMessages),
|
||||
}
|
||||
newData[currentConvoId] = messages
|
||||
@ -34,8 +34,8 @@ export const setCurrentChatMessagesAtom = atom(
|
||||
|
||||
export const setConvoMessagesAtom = atom(
|
||||
null,
|
||||
(get, set, messages: ChatMessage[], convoId: string) => {
|
||||
const newData: Record<string, ChatMessage[]> = {
|
||||
(get, set, messages: ThreadMessage[], convoId: string) => {
|
||||
const newData: Record<string, ThreadMessage[]> = {
|
||||
...get(chatMessages),
|
||||
}
|
||||
newData[convoId] = messages
|
||||
@ -48,14 +48,14 @@ export const setConvoMessagesAtom = atom(
|
||||
*/
|
||||
export const addOldMessagesAtom = atom(
|
||||
null,
|
||||
(get, set, newMessages: ChatMessage[]) => {
|
||||
(get, set, newMessages: ThreadMessage[]) => {
|
||||
const currentConvoId = get(getActiveConvoIdAtom)
|
||||
if (!currentConvoId) return
|
||||
|
||||
const currentMessages = get(chatMessages)[currentConvoId] ?? []
|
||||
const updatedMessages = [...currentMessages, ...newMessages]
|
||||
|
||||
const newData: Record<string, ChatMessage[]> = {
|
||||
const newData: Record<string, ThreadMessage[]> = {
|
||||
...get(chatMessages),
|
||||
}
|
||||
newData[currentConvoId] = updatedMessages
|
||||
@ -65,14 +65,14 @@ export const addOldMessagesAtom = atom(
|
||||
|
||||
export const addNewMessageAtom = atom(
|
||||
null,
|
||||
(get, set, newMessage: ChatMessage) => {
|
||||
(get, set, newMessage: ThreadMessage) => {
|
||||
const currentConvoId = get(getActiveConvoIdAtom)
|
||||
if (!currentConvoId) return
|
||||
|
||||
const currentMessages = get(chatMessages)[currentConvoId] ?? []
|
||||
const updatedMessages = [newMessage, ...currentMessages]
|
||||
|
||||
const newData: Record<string, ChatMessage[]> = {
|
||||
const newData: Record<string, ThreadMessage[]> = {
|
||||
...get(chatMessages),
|
||||
}
|
||||
newData[currentConvoId] = updatedMessages
|
||||
@ -81,7 +81,7 @@ export const addNewMessageAtom = atom(
|
||||
)
|
||||
|
||||
export const deleteConversationMessage = atom(null, (get, set, id: string) => {
|
||||
const newData: Record<string, ChatMessage[]> = {
|
||||
const newData: Record<string, ThreadMessage[]> = {
|
||||
...get(chatMessages),
|
||||
}
|
||||
newData[id] = []
|
||||
@ -101,11 +101,11 @@ export const updateMessageAtom = atom(
|
||||
const messages = get(chatMessages)[conversationId] ?? []
|
||||
const message = messages.find((e) => e.id === id)
|
||||
if (message) {
|
||||
message.text = text
|
||||
message.content = text
|
||||
message.status = status
|
||||
const updatedMessages = [...messages]
|
||||
|
||||
const newData: Record<string, ChatMessage[]> = {
|
||||
const newData: Record<string, ThreadMessage[]> = {
|
||||
...get(chatMessages),
|
||||
}
|
||||
newData[conversationId] = updatedMessages
|
||||
@ -130,14 +130,14 @@ export const updateLastMessageAsReadyAtom = atom(
|
||||
if (!messageToUpdate) return
|
||||
|
||||
const index = currentMessages.indexOf(messageToUpdate)
|
||||
const updatedMsg: ChatMessage = {
|
||||
const updatedMsg: ThreadMessage = {
|
||||
...messageToUpdate,
|
||||
status: MessageStatus.Ready,
|
||||
text: text,
|
||||
content: text,
|
||||
}
|
||||
|
||||
currentMessages[index] = updatedMsg
|
||||
const newData: Record<string, ChatMessage[]> = {
|
||||
const newData: Record<string, ThreadMessage[]> = {
|
||||
...get(chatMessages),
|
||||
}
|
||||
newData[currentConvoId] = currentMessages
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { Conversation, ConversationState } from '@janhq/core'
|
||||
import { Thread } from '@janhq/core'
|
||||
import { atom } from 'jotai'
|
||||
|
||||
import { ThreadState } from '@/types/conversation'
|
||||
|
||||
/**
|
||||
* Stores the current active conversation id.
|
||||
*/
|
||||
@ -19,11 +21,8 @@ export const waitingToSendMessage = atom<boolean | undefined>(undefined)
|
||||
/**
|
||||
* Stores all conversation states for the current user
|
||||
*/
|
||||
export const conversationStatesAtom = atom<Record<string, ConversationState>>(
|
||||
{}
|
||||
)
|
||||
export const currentConvoStateAtom = atom<ConversationState | undefined>(
|
||||
(get) => {
|
||||
export const conversationStatesAtom = atom<Record<string, ThreadState>>({})
|
||||
export const currentConvoStateAtom = atom<ThreadState | undefined>((get) => {
|
||||
const activeConvoId = get(activeConversationIdAtom)
|
||||
if (!activeConvoId) {
|
||||
console.debug('Active convo id is undefined')
|
||||
@ -31,11 +30,10 @@ export const currentConvoStateAtom = atom<ConversationState | undefined>(
|
||||
}
|
||||
|
||||
return get(conversationStatesAtom)[activeConvoId]
|
||||
}
|
||||
)
|
||||
})
|
||||
export const addNewConversationStateAtom = atom(
|
||||
null,
|
||||
(get, set, conversationId: string, state: ConversationState) => {
|
||||
(get, set, conversationId: string, state: ThreadState) => {
|
||||
const currentState = { ...get(conversationStatesAtom) }
|
||||
currentState[conversationId] = state
|
||||
set(conversationStatesAtom, currentState)
|
||||
@ -75,14 +73,14 @@ export const updateConversationHasMoreAtom = atom(
|
||||
|
||||
export const updateConversationAtom = atom(
|
||||
null,
|
||||
(get, set, conversation: Conversation) => {
|
||||
(get, set, conversation: Thread) => {
|
||||
const id = conversation.id
|
||||
if (!id) return
|
||||
const convo = get(userConversationsAtom).find((c) => c.id === id)
|
||||
if (!convo) return
|
||||
|
||||
const newConversations: Conversation[] = get(userConversationsAtom).map(
|
||||
(c) => (c.id === id ? conversation : c)
|
||||
const newConversations: Thread[] = get(userConversationsAtom).map((c) =>
|
||||
c.id === id ? conversation : c
|
||||
)
|
||||
|
||||
// sort new conversations based on updated at
|
||||
@ -99,7 +97,7 @@ export const updateConversationAtom = atom(
|
||||
/**
|
||||
* Stores all conversations for the current user
|
||||
*/
|
||||
export const userConversationsAtom = atom<Conversation[]>([])
|
||||
export const currentConversationAtom = atom<Conversation | undefined>((get) =>
|
||||
export const userConversationsAtom = atom<Thread[]>([])
|
||||
export const currentConversationAtom = atom<Thread | undefined>((get) =>
|
||||
get(userConversationsAtom).find((c) => c.id === get(getActiveConvoIdAtom))
|
||||
)
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { join } from 'path'
|
||||
|
||||
import { PluginType } from '@janhq/core'
|
||||
import { InferencePlugin } from '@janhq/core/lib/plugins'
|
||||
import { Model } from '@janhq/core/lib/types'
|
||||
@ -10,7 +12,6 @@ import { toaster } from '@/containers/Toast'
|
||||
import { useGetDownloadedModels } from './useGetDownloadedModels'
|
||||
|
||||
import { pluginManager } from '@/plugin'
|
||||
import { join } from 'path'
|
||||
|
||||
const activeAssistantModelAtom = atom<Model | undefined>(undefined)
|
||||
|
||||
@ -43,7 +44,7 @@ export function useActiveModel() {
|
||||
|
||||
const currentTime = Date.now()
|
||||
console.debug('Init model: ', modelId)
|
||||
const path = join('models', model.productName, modelId)
|
||||
const path = join('models', model.name, modelId)
|
||||
const res = await initModel(path)
|
||||
if (res?.error) {
|
||||
const errorMessage = `${res.error}`
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { PluginType } from '@janhq/core'
|
||||
import { Conversation, Model } from '@janhq/core'
|
||||
import { Thread, Model } from '@janhq/core'
|
||||
import { ConversationalPlugin } from '@janhq/core/lib/plugins'
|
||||
import { useAtom, useSetAtom } from 'jotai'
|
||||
|
||||
@ -20,11 +20,11 @@ export const useCreateConversation = () => {
|
||||
const addNewConvoState = useSetAtom(addNewConversationStateAtom)
|
||||
|
||||
const requestCreateConvo = async (model: Model) => {
|
||||
const conversationName = model.name
|
||||
const mappedConvo: Conversation = {
|
||||
const summary = model.name
|
||||
const mappedConvo: Thread = {
|
||||
id: generateConversationId(),
|
||||
modelId: model.id,
|
||||
name: conversationName,
|
||||
summary,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
messages: [],
|
||||
@ -37,11 +37,7 @@ export const useCreateConversation = () => {
|
||||
|
||||
pluginManager
|
||||
.get<ConversationalPlugin>(PluginType.Conversational)
|
||||
?.saveConversation({
|
||||
...mappedConvo,
|
||||
name: mappedConvo.name ?? '',
|
||||
messages: [],
|
||||
})
|
||||
?.saveConversation(mappedConvo)
|
||||
setUserConversations([mappedConvo, ...userConversations])
|
||||
setActiveConvoId(mappedConvo.id)
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { join } from 'path'
|
||||
|
||||
import { PluginType } from '@janhq/core'
|
||||
import { ModelPlugin } from '@janhq/core/lib/plugins'
|
||||
import { Model } from '@janhq/core/lib/types'
|
||||
@ -7,13 +9,12 @@ import { toaster } from '@/containers/Toast'
|
||||
import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
|
||||
|
||||
import { pluginManager } from '@/plugin/PluginManager'
|
||||
import { join } from 'path'
|
||||
|
||||
export default function useDeleteModel() {
|
||||
const { setDownloadedModels, downloadedModels } = useGetDownloadedModels()
|
||||
|
||||
const deleteModel = async (model: Model) => {
|
||||
const path = join('models', model.productName, model.id)
|
||||
const path = join('models', model.name, model.id)
|
||||
await pluginManager.get<ModelPlugin>(PluginType.Model)?.deleteModel(path)
|
||||
|
||||
// reload models
|
||||
|
||||
@ -7,6 +7,7 @@ import { useAtom } from 'jotai'
|
||||
import { useDownloadState } from './useDownloadState'
|
||||
|
||||
import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom'
|
||||
|
||||
import { pluginManager } from '@/plugin/PluginManager'
|
||||
|
||||
export default function useDownloadModel() {
|
||||
@ -20,28 +21,24 @@ export default function useDownloadModel() {
|
||||
modelVersion: ModelVersion
|
||||
): Model => {
|
||||
return {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
id: modelVersion.id,
|
||||
name: modelVersion.name,
|
||||
quantMethod: modelVersion.quantMethod,
|
||||
/**
|
||||
* Id will be used for the model file name
|
||||
* Should be the version name
|
||||
*/
|
||||
id: modelVersion.name,
|
||||
name: model.name,
|
||||
quantizationName: modelVersion.quantizationName,
|
||||
bits: modelVersion.bits,
|
||||
size: modelVersion.size,
|
||||
maxRamRequired: modelVersion.maxRamRequired,
|
||||
usecase: modelVersion.usecase,
|
||||
downloadLink: modelVersion.downloadLink,
|
||||
startDownloadAt: modelVersion.startDownloadAt,
|
||||
finishDownloadAt: modelVersion.finishDownloadAt,
|
||||
productId: model.id,
|
||||
productName: model.name,
|
||||
shortDescription: model.shortDescription,
|
||||
longDescription: model.longDescription,
|
||||
avatarUrl: model.avatarUrl,
|
||||
author: model.author,
|
||||
version: model.version,
|
||||
modelUrl: model.modelUrl,
|
||||
createdAt: new Date(model.createdAt).getTime(),
|
||||
updatedAt: new Date(model.updatedAt ?? '').getTime(),
|
||||
status: '',
|
||||
releaseDate: -1,
|
||||
tags: model.tags,
|
||||
}
|
||||
@ -53,7 +50,7 @@ export default function useDownloadModel() {
|
||||
) => {
|
||||
// set an initial download state
|
||||
setDownloadState({
|
||||
modelId: modelVersion.id,
|
||||
modelId: modelVersion.name,
|
||||
time: {
|
||||
elapsed: 0,
|
||||
remaining: 0,
|
||||
@ -64,10 +61,9 @@ export default function useDownloadModel() {
|
||||
total: 0,
|
||||
transferred: 0,
|
||||
},
|
||||
fileName: modelVersion.id,
|
||||
fileName: modelVersion.name,
|
||||
})
|
||||
|
||||
modelVersion.startDownloadAt = Date.now()
|
||||
const assistantModel = assistanModel(model, modelVersion)
|
||||
|
||||
setDownloadingModels([...downloadingModels, assistantModel])
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Model, Conversation } from '@janhq/core'
|
||||
|
||||
import { Model, Thread } from '@janhq/core'
|
||||
import { useAtomValue } from 'jotai'
|
||||
|
||||
import { useActiveModel } from './useActiveModel'
|
||||
import { useGetDownloadedModels } from './useGetDownloadedModels'
|
||||
|
||||
import { currentConversationAtom } from '@/helpers/atoms/Conversation.atom'
|
||||
|
||||
export default function useGetInputState() {
|
||||
@ -12,7 +15,7 @@ export default function useGetInputState() {
|
||||
const { downloadedModels } = useGetDownloadedModels()
|
||||
|
||||
const handleInputState = (
|
||||
convo: Conversation | undefined,
|
||||
convo: Thread | undefined,
|
||||
currentModel: Model | undefined
|
||||
) => {
|
||||
if (convo == null) return
|
||||
|
||||
@ -1,16 +1,14 @@
|
||||
import { PluginType, ChatMessage, ConversationState } from '@janhq/core'
|
||||
import { PluginType, Thread } from '@janhq/core'
|
||||
import { ConversationalPlugin } from '@janhq/core/lib/plugins'
|
||||
import { Conversation } from '@janhq/core/lib/types'
|
||||
import { useSetAtom } from 'jotai'
|
||||
|
||||
import { toChatMessage } from '@/utils/message'
|
||||
|
||||
import { setConvoMessagesAtom } from '@/helpers/atoms/ChatMessage.atom'
|
||||
import {
|
||||
conversationStatesAtom,
|
||||
userConversationsAtom,
|
||||
} from '@/helpers/atoms/Conversation.atom'
|
||||
import { pluginManager } from '@/plugin/PluginManager'
|
||||
import { ThreadState } from '@/types/conversation'
|
||||
|
||||
const useGetUserConversations = () => {
|
||||
const setConversationStates = useSetAtom(conversationStatesAtom)
|
||||
@ -19,19 +17,16 @@ const useGetUserConversations = () => {
|
||||
|
||||
const getUserConversations = async () => {
|
||||
try {
|
||||
const convos: Conversation[] | undefined = await pluginManager
|
||||
const convos: Thread[] | undefined = await pluginManager
|
||||
.get<ConversationalPlugin>(PluginType.Conversational)
|
||||
?.getConversations()
|
||||
const convoStates: Record<string, ConversationState> = {}
|
||||
const convoStates: Record<string, ThreadState> = {}
|
||||
convos?.forEach((convo) => {
|
||||
convoStates[convo.id ?? ''] = {
|
||||
hasMore: true,
|
||||
waitingForResponse: false,
|
||||
}
|
||||
setConvoMessages(
|
||||
convo.messages.map<ChatMessage>((msg) => toChatMessage(msg)),
|
||||
convo.id ?? ''
|
||||
)
|
||||
setConvoMessages(convo.messages, convo.id ?? '')
|
||||
})
|
||||
setConversationStates(convoStates)
|
||||
setConversations(convos ?? [])
|
||||
|
||||
@ -1,18 +1,20 @@
|
||||
import {
|
||||
ChatCompletionMessage,
|
||||
ChatCompletionRole,
|
||||
EventName,
|
||||
MessageHistory,
|
||||
NewMessageRequest,
|
||||
MessageRequest,
|
||||
MessageStatus,
|
||||
PluginType,
|
||||
Thread,
|
||||
events,
|
||||
ChatMessage,
|
||||
Message,
|
||||
Conversation,
|
||||
MessageSenderType,
|
||||
} from '@janhq/core'
|
||||
import { ConversationalPlugin, InferencePlugin } from '@janhq/core/lib/plugins'
|
||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
|
||||
import { currentPromptAtom } from '@/containers/Providers/Jotai'
|
||||
|
||||
import { ulid } from 'ulid'
|
||||
|
||||
import { currentPromptAtom } from '@/containers/Providers/Jotai'
|
||||
|
||||
import {
|
||||
addNewMessageAtom,
|
||||
getCurrentChatMessagesAtom,
|
||||
@ -23,7 +25,6 @@ import {
|
||||
updateConversationWaitingForResponseAtom,
|
||||
} from '@/helpers/atoms/Conversation.atom'
|
||||
import { pluginManager } from '@/plugin/PluginManager'
|
||||
import { toChatMessage } from '@/utils/message'
|
||||
|
||||
export default function useSendChatMessage() {
|
||||
const currentConvo = useAtomValue(currentConversationAtom)
|
||||
@ -35,7 +36,7 @@ export default function useSendChatMessage() {
|
||||
|
||||
let timeout: NodeJS.Timeout | undefined = undefined
|
||||
|
||||
function updateConvSummary(newMessage: NewMessageRequest) {
|
||||
function updateConvSummary(newMessage: MessageRequest) {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
@ -46,13 +47,19 @@ export default function useSendChatMessage() {
|
||||
currentConvo.summary === '' ||
|
||||
currentConvo.summary.startsWith('Prompt:')
|
||||
) {
|
||||
const summaryMsg: ChatCompletionMessage = {
|
||||
role: ChatCompletionRole.User,
|
||||
content:
|
||||
'summary this conversation in 5 words, the response should just include the summary',
|
||||
}
|
||||
// Request convo summary
|
||||
setTimeout(async () => {
|
||||
newMessage.message =
|
||||
'summary this conversation in 5 words, the response should just include the summary'
|
||||
const result = await pluginManager
|
||||
.get<InferencePlugin>(PluginType.Inference)
|
||||
?.inferenceRequest(newMessage)
|
||||
?.inferenceRequest({
|
||||
...newMessage,
|
||||
messages: newMessage.messages?.concat([summaryMsg]),
|
||||
})
|
||||
|
||||
if (
|
||||
result?.message &&
|
||||
@ -68,15 +75,7 @@ export default function useSendChatMessage() {
|
||||
.get<ConversationalPlugin>(PluginType.Conversational)
|
||||
?.saveConversation({
|
||||
...updatedConv,
|
||||
name: updatedConv.name ?? '',
|
||||
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(),
|
||||
})),
|
||||
messages: currentMessages,
|
||||
})
|
||||
}
|
||||
}, 1000)
|
||||
@ -95,29 +94,32 @@ export default function useSendChatMessage() {
|
||||
updateConvWaiting(convoId, true)
|
||||
|
||||
const prompt = currentPrompt.trim()
|
||||
const messageHistory: MessageHistory[] = currentMessages
|
||||
.map((msg) => ({
|
||||
role: msg.senderUid,
|
||||
content: msg.text ?? '',
|
||||
const messages: ChatCompletionMessage[] = currentMessages
|
||||
.map<ChatCompletionMessage>((msg) => ({
|
||||
role: msg.role ?? ChatCompletionRole.User,
|
||||
content: msg.content ?? '',
|
||||
}))
|
||||
.reverse()
|
||||
.concat([
|
||||
{
|
||||
role: MessageSenderType.User,
|
||||
role: ChatCompletionRole.User,
|
||||
content: prompt,
|
||||
} as MessageHistory,
|
||||
} as ChatCompletionMessage,
|
||||
])
|
||||
const newMessage: NewMessageRequest = {
|
||||
const newMessage: MessageRequest = {
|
||||
id: ulid(),
|
||||
conversationId: convoId,
|
||||
message: prompt,
|
||||
user: MessageSenderType.User,
|
||||
createdAt: new Date().toISOString(),
|
||||
history: messageHistory,
|
||||
threadId: convoId,
|
||||
messages,
|
||||
}
|
||||
|
||||
const newChatMessage = toChatMessage(newMessage)
|
||||
addNewMessage(newChatMessage)
|
||||
addNewMessage({
|
||||
id: newMessage.id,
|
||||
threadId: newMessage.threadId,
|
||||
content: prompt,
|
||||
role: ChatCompletionRole.User,
|
||||
createdAt: new Date().toISOString(),
|
||||
status: MessageStatus.Ready,
|
||||
})
|
||||
|
||||
// delay randomly from 50 - 100ms
|
||||
// to prevent duplicate message id
|
||||
@ -126,19 +128,11 @@ export default function useSendChatMessage() {
|
||||
|
||||
events.emit(EventName.OnNewMessageRequest, newMessage)
|
||||
if (!currentConvo?.summary && currentConvo) {
|
||||
const updatedConv: Conversation = {
|
||||
const updatedConv: Thread = {
|
||||
...currentConvo,
|
||||
lastMessage: prompt,
|
||||
summary: `Prompt: ${prompt}`,
|
||||
}
|
||||
|
||||
updateConversation(updatedConv)
|
||||
} else if (currentConvo) {
|
||||
const updatedConv: Conversation = {
|
||||
...currentConvo,
|
||||
lastMessage: prompt,
|
||||
}
|
||||
|
||||
updateConversation(updatedConv)
|
||||
}
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ const ChatBody: React.FC = () => {
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col-reverse overflow-y-auto">
|
||||
{messages.map((message) => (
|
||||
<ChatItem message={message} key={message.id} />
|
||||
<ChatItem {...message} key={message.id} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -1,24 +1,14 @@
|
||||
import React, { forwardRef } from 'react'
|
||||
import { ChatMessage } from '@janhq/core'
|
||||
import SimpleTextMessage from '../SimpleTextMessage'
|
||||
|
||||
type Props = {
|
||||
message: ChatMessage
|
||||
}
|
||||
import { ThreadMessage } from '@janhq/core'
|
||||
|
||||
import SimpleTextMessage from '../SimpleTextMessage'
|
||||
|
||||
type Ref = HTMLDivElement
|
||||
|
||||
const ChatItem = forwardRef<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">
|
||||
<SimpleTextMessage
|
||||
status={message.status}
|
||||
key={message.id}
|
||||
avatarUrl={message.senderAvatarUrl}
|
||||
senderName={message.senderName}
|
||||
createdAt={message.createdAt}
|
||||
senderType={message.messageSenderType}
|
||||
text={message.text}
|
||||
/>
|
||||
<SimpleTextMessage {...message} />
|
||||
</div>
|
||||
))
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import { Conversation, Model } from '@janhq/core/lib/types'
|
||||
import { Thread, Model } from '@janhq/core'
|
||||
import { Button } from '@janhq/uikit'
|
||||
import { motion as m } from 'framer-motion'
|
||||
import { useAtomValue, useSetAtom } from 'jotai'
|
||||
@ -41,7 +41,7 @@ export default function HistoryList() {
|
||||
return
|
||||
}
|
||||
|
||||
const handleActiveModel = async (convo: Conversation) => {
|
||||
const handleActiveModel = async (convo: Thread) => {
|
||||
if (convo.modelId == null) {
|
||||
console.debug('modelId is undefined')
|
||||
return
|
||||
@ -90,15 +90,16 @@ export default function HistoryList() {
|
||||
'relative flex cursor-pointer flex-col border-b border-border px-4 py-2 hover:bg-secondary/20',
|
||||
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">
|
||||
{convo.updatedAt &&
|
||||
displayDate(new Date(convo.updatedAt).getTime())}
|
||||
</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">
|
||||
{convo?.lastMessage ?? 'No new message'}
|
||||
{/* TODO: Check latest message update */}
|
||||
{convo?.messages[0]?.content ?? 'No new message'}
|
||||
</p>
|
||||
{activeModel && activeConvoId === convo.id && (
|
||||
<m.div
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
/* 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 { Marked } from 'marked'
|
||||
@ -16,15 +16,6 @@ import BubbleLoader from '@/containers/Loader/Bubble'
|
||||
|
||||
import { displayDate } from '@/utils/datetime'
|
||||
|
||||
type Props = {
|
||||
avatarUrl: string
|
||||
senderName: string
|
||||
createdAt: number
|
||||
senderType: MessageSenderType
|
||||
status: MessageStatus
|
||||
text?: string
|
||||
}
|
||||
|
||||
const marked = new Marked(
|
||||
markedHighlight({
|
||||
langPrefix: 'hljs',
|
||||
@ -50,16 +41,9 @@ const marked = new Marked(
|
||||
}
|
||||
)
|
||||
|
||||
const SimpleTextMessage: React.FC<Props> = ({
|
||||
senderName,
|
||||
senderType,
|
||||
createdAt,
|
||||
// will use status as streaming text
|
||||
// status,
|
||||
text = '',
|
||||
}) => {
|
||||
const parsedText = marked.parse(text)
|
||||
const isUser = senderType === 'user'
|
||||
const SimpleTextMessage: React.FC<ThreadMessage> = (props) => {
|
||||
const parsedText = marked.parse(props.content ?? '')
|
||||
const isUser = props.role === ChatCompletionRole.User
|
||||
|
||||
return (
|
||||
<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} />}
|
||||
<div className="text-sm font-extrabold ">{senderName}</div>
|
||||
<p className="text-xs font-medium">{displayDate(createdAt)}</p>
|
||||
<div className="text-sm font-extrabold capitalize">{props.role}</div>
|
||||
<p className="text-xs font-medium">{displayDate(props.createdAt)}</p>
|
||||
</div>
|
||||
|
||||
<div className={twMerge('w-full')}>
|
||||
{text === '' ? (
|
||||
{!props.content || props.content === '' ? (
|
||||
<BubbleLoader />
|
||||
) : (
|
||||
<>
|
||||
|
||||
@ -126,7 +126,7 @@ const ChatScreen = () => {
|
||||
{isEnableChat && currentConvo && (
|
||||
<div className="h-[53px] flex-shrink-0 border-b border-border bg-background p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span>{currentConvo?.name ?? ''}</span>
|
||||
<span>{currentConvo?.summary ?? ''}</span>
|
||||
<div
|
||||
className={twMerge(
|
||||
'flex items-center space-x-3',
|
||||
|
||||
@ -33,7 +33,7 @@ const ExploreModelItem = forwardRef<HTMLDivElement, Props>(({ model }, ref) => {
|
||||
return null
|
||||
}
|
||||
|
||||
const { quantMethod, bits, maxRamRequired, usecase } = suitableModel
|
||||
const { quantizationName, bits, maxRamRequired, usecase } = suitableModel
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -73,7 +73,9 @@ const ExploreModelItem = forwardRef<HTMLDivElement, Props>(({ model }, ref) => {
|
||||
<span className="font-semibold">Version</span>
|
||||
<div className="mt-2 flex space-x-2">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@ -105,7 +107,7 @@ const ExploreModelItem = forwardRef<HTMLDivElement, Props>(({ model }, ref) => {
|
||||
<ModelVersionList
|
||||
model={model}
|
||||
versions={model.availableVersions}
|
||||
recommendedVersion={suitableModel?.id ?? ''}
|
||||
recommendedVersion={suitableModel?.name ?? ''}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -35,8 +35,8 @@ const ExploreModelItemHeader: React.FC<Props> = ({
|
||||
const { performanceTag, title, getPerformanceForModel } =
|
||||
useGetPerformanceTag()
|
||||
const downloadAtom = useMemo(
|
||||
() => atom((get) => get(modelDownloadStateAtom)[suitableModel.id]),
|
||||
[suitableModel.id]
|
||||
() => atom((get) => get(modelDownloadStateAtom)[suitableModel.name]),
|
||||
[suitableModel.name]
|
||||
)
|
||||
const downloadState = useAtomValue(downloadAtom)
|
||||
const { setMainViewState } = useMainViewState()
|
||||
@ -51,8 +51,9 @@ const ExploreModelItemHeader: React.FC<Props> = ({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [exploreModel, suitableModel])
|
||||
|
||||
// TODO: Comparing between Model Id and Version Name?
|
||||
const isDownloaded =
|
||||
downloadedModels.find((model) => model.id === suitableModel.id) != null
|
||||
downloadedModels.find((model) => model.id === suitableModel.name) != null
|
||||
|
||||
let downloadButton = (
|
||||
<Button onClick={() => onDownloadClick()}>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { ModelCatalog } from '@janhq/core/lib/types'
|
||||
|
||||
import ExploreModelItem from '@/screens/ExploreModels/ExploreModelItem'
|
||||
|
||||
type Props = {
|
||||
@ -7,7 +8,9 @@ type Props = {
|
||||
|
||||
const ExploreModelList: React.FC<Props> = ({ models }) => (
|
||||
<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>
|
||||
)
|
||||
|
||||
|
||||
@ -1,15 +1,20 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import React, { useMemo } from 'react'
|
||||
|
||||
import { ModelCatalog, ModelVersion } from '@janhq/core/lib/types'
|
||||
import { Button } from '@janhq/uikit'
|
||||
import { Badge } from '@janhq/uikit'
|
||||
import { atom, useAtomValue } from 'jotai'
|
||||
|
||||
import ModalCancelDownload from '@/containers/ModalCancelDownload'
|
||||
|
||||
import { MainViewState } from '@/constants/screens'
|
||||
|
||||
import useDownloadModel from '@/hooks/useDownloadModel'
|
||||
import { useDownloadState } from '@/hooks/useDownloadState'
|
||||
import { useGetDownloadedModels } from '@/hooks/useGetDownloadedModels'
|
||||
import { useMainViewState } from '@/hooks/useMainViewState'
|
||||
|
||||
import { toGigabytes } from '@/utils/converter'
|
||||
|
||||
type Props = {
|
||||
@ -23,13 +28,13 @@ const ModelVersionItem: React.FC<Props> = ({ model, modelVersion }) => {
|
||||
const { downloadedModels } = useGetDownloadedModels()
|
||||
const { setMainViewState } = useMainViewState()
|
||||
const isDownloaded =
|
||||
downloadedModels.find((model) => model.id === modelVersion.id) != null
|
||||
downloadedModels.find((model) => model.id === modelVersion.name) != null
|
||||
|
||||
const { modelDownloadStateAtom, downloadStates } = useDownloadState()
|
||||
|
||||
const downloadAtom = useMemo(
|
||||
() => atom((get) => get(modelDownloadStateAtom)[modelVersion.id ?? '']),
|
||||
[modelVersion.id]
|
||||
() => atom((get) => get(modelDownloadStateAtom)[modelVersion.name ?? '']),
|
||||
[modelVersion.name]
|
||||
)
|
||||
const downloadState = useAtomValue(downloadAtom)
|
||||
|
||||
|
||||
@ -17,10 +17,10 @@ export default function ModelVersionList({
|
||||
<div className="pt-4">
|
||||
{versions.map((item) => (
|
||||
<ModelVersionItem
|
||||
key={item.id}
|
||||
key={item.name}
|
||||
model={model}
|
||||
modelVersion={item}
|
||||
isRecommended={item.id === recommendedVersion}
|
||||
isRecommended={item.name === recommendedVersion}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@ -76,7 +76,7 @@ const MyModelsScreen = () => {
|
||||
<h2 className="mb-1 font-medium capitalize">
|
||||
{model.author}
|
||||
</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">
|
||||
<Badge themes="secondary">v{model.version}</Badge>
|
||||
<Badge themes="outline">GGUF</Badge>
|
||||
@ -101,7 +101,7 @@ const MyModelsScreen = () => {
|
||||
<ModalTitle>Are you sure?</ModalTitle>
|
||||
</ModalHeader>
|
||||
<p className="leading-relaxed">
|
||||
Delete model {model.productName}, v{model.version},{' '}
|
||||
Delete model {model.name}, v{model.version},{' '}
|
||||
{toGigabytes(model.size)}.
|
||||
</p>
|
||||
<ModalFooter>
|
||||
|
||||
2
web/types/conversation.d.ts
vendored
2
web/types/conversation.d.ts
vendored
@ -1,4 +1,4 @@
|
||||
export type ConversationState = {
|
||||
export type ThreadState = {
|
||||
hasMore: boolean
|
||||
waitingForResponse: boolean
|
||||
error?: Error
|
||||
|
||||
@ -3,11 +3,11 @@ export const isToday = (timestamp: number) => {
|
||||
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'
|
||||
|
||||
let displayDate = new Date(timestamp).toLocaleString()
|
||||
if (isToday(timestamp)) {
|
||||
if (typeof timestamp === 'number' && isToday(timestamp)) {
|
||||
displayDate = new Date(timestamp).toLocaleTimeString(undefined, {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
|
||||
@ -7,18 +7,37 @@ export const dummyModel: ModelCatalog = {
|
||||
shortDescription: 'TinyLlama-1.1B-Chat-v0.3-GGUF',
|
||||
longDescription: 'https://huggingface.co/aladar/TinyLLama-v0-GGUF/tree/main',
|
||||
avatarUrl: '',
|
||||
status: '',
|
||||
releaseDate: Date.now(),
|
||||
author: 'aladar',
|
||||
version: '1.0.0',
|
||||
modelUrl: 'aladar/TinyLLama-v0-GGUF',
|
||||
tags: ['freeform', 'tags'],
|
||||
createdAt: 0,
|
||||
availableVersions: [
|
||||
{
|
||||
id: 'tinyllama-1.1b-chat-v0.3.Q2_K.gguf',
|
||||
name: 'tinyllama-1.1b-chat-v0.3.Q2_K.gguf',
|
||||
quantMethod: '',
|
||||
name: 'TinyLLama-v0.Q8_0.gguf',
|
||||
quantizationName: '',
|
||||
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,
|
||||
size: 19660000,
|
||||
maxRamRequired: 256000000,
|
||||
@ -26,6 +45,6 @@ export const dummyModel: ModelCatalog = {
|
||||
'smallest, significant quality loss - not recommended for most purposes',
|
||||
downloadLink:
|
||||
'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,
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user