Merge branch 'dev' into docs-add-docker-installation

This commit is contained in:
Henry 2024-02-11 10:16:16 +07:00 committed by GitHub
commit 48455b851c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 359 additions and 101 deletions

View File

@ -76,31 +76,31 @@ Jan is an open-source ChatGPT alternative that runs 100% offline on your compute
<tr style="text-align:center"> <tr style="text-align:center">
<td style="text-align:center"><b>Experimental (Nightly Build)</b></td> <td style="text-align:center"><b>Experimental (Nightly Build)</b></td>
<td style="text-align:center"> <td style="text-align:center">
<a href='https://delta.jan.ai/latest/jan-win-x64-0.4.6-264.exe'> <a href='https://delta.jan.ai/latest/jan-win-x64-0.4.6-267.exe'>
<img src='./docs/static/img/windows.png' style="height:14px; width: 14px" /> <img src='./docs/static/img/windows.png' style="height:14px; width: 14px" />
<b>jan.exe</b> <b>jan.exe</b>
</a> </a>
</td> </td>
<td style="text-align:center"> <td style="text-align:center">
<a href='https://delta.jan.ai/latest/jan-mac-x64-0.4.6-264.dmg'> <a href='https://delta.jan.ai/latest/jan-mac-x64-0.4.6-267.dmg'>
<img src='./docs/static/img/mac.png' style="height:15px; width: 15px" /> <img src='./docs/static/img/mac.png' style="height:15px; width: 15px" />
<b>Intel</b> <b>Intel</b>
</a> </a>
</td> </td>
<td style="text-align:center"> <td style="text-align:center">
<a href='https://delta.jan.ai/latest/jan-mac-arm64-0.4.6-264.dmg'> <a href='https://delta.jan.ai/latest/jan-mac-arm64-0.4.6-267.dmg'>
<img src='./docs/static/img/mac.png' style="height:15px; width: 15px" /> <img src='./docs/static/img/mac.png' style="height:15px; width: 15px" />
<b>M1/M2</b> <b>M1/M2</b>
</a> </a>
</td> </td>
<td style="text-align:center"> <td style="text-align:center">
<a href='https://delta.jan.ai/latest/jan-linux-amd64-0.4.6-264.deb'> <a href='https://delta.jan.ai/latest/jan-linux-amd64-0.4.6-267.deb'>
<img src='./docs/static/img/linux.png' style="height:14px; width: 14px" /> <img src='./docs/static/img/linux.png' style="height:14px; width: 14px" />
<b>jan.deb</b> <b>jan.deb</b>
</a> </a>
</td> </td>
<td style="text-align:center"> <td style="text-align:center">
<a href='https://delta.jan.ai/latest/jan-linux-x86_64-0.4.6-264.AppImage'> <a href='https://delta.jan.ai/latest/jan-linux-x86_64-0.4.6-267.AppImage'>
<img src='./docs/static/img/linux.png' style="height:14px; width: 14px" /> <img src='./docs/static/img/linux.png' style="height:14px; width: 14px" />
<b>jan.AppImage</b> <b>jan.AppImage</b>
</a> </a>

View File

@ -1,5 +1,5 @@
import { DownloadRoute } from '../../../api' import { DownloadRoute } from '../../../api'
import { join } from 'path' import { join, sep } from 'path'
import { DownloadManager } from '../../download' import { DownloadManager } from '../../download'
import { HttpServer } from '../HttpServer' import { HttpServer } from '../HttpServer'
import { createWriteStream } from 'fs' import { createWriteStream } from 'fs'
@ -38,7 +38,7 @@ export const downloadRouter = async (app: HttpServer) => {
}) })
const localPath = normalizedArgs[1] const localPath = normalizedArgs[1]
const array = localPath.split('/') const array = localPath.split(sep)
const fileName = array.pop() ?? '' const fileName = array.pop() ?? ''
const modelId = array.pop() ?? '' const modelId = array.pop() ?? ''
console.debug('downloadFile', normalizedArgs, fileName, modelId) console.debug('downloadFile', normalizedArgs, fileName, modelId)
@ -99,7 +99,7 @@ export const downloadRouter = async (app: HttpServer) => {
}) })
const localPath = normalizedArgs[0] const localPath = normalizedArgs[0]
const fileName = localPath.split('/').pop() ?? '' const fileName = localPath.split(sep).pop() ?? ''
const rq = DownloadManager.instance.networkRequests[fileName] const rq = DownloadManager.instance.networkRequests[fileName]
DownloadManager.instance.networkRequests[fileName] = undefined DownloadManager.instance.networkRequests[fileName] = undefined
rq?.abort() rq?.abort()

View File

@ -1,3 +1,4 @@
export * from './messageEntity' export * from './messageEntity'
export * from './messageInterface' export * from './messageInterface'
export * from './messageEvent' export * from './messageEvent'
export * from './messageRequestType'

View File

@ -27,6 +27,8 @@ export type ThreadMessage = {
updated: number updated: number
/** The additional metadata of this message. **/ /** The additional metadata of this message. **/
metadata?: Record<string, unknown> metadata?: Record<string, unknown>
type?: string
} }
/** /**
@ -56,6 +58,8 @@ export type MessageRequest = {
/** The thread of this message is belong to. **/ /** The thread of this message is belong to. **/
// TODO: deprecate threadId field // TODO: deprecate threadId field
thread?: Thread thread?: Thread
type?: string
} }
/** /**

View File

@ -0,0 +1,5 @@
export enum MessageRequestType {
Thread = 'Thread',
Assistant = 'Assistant',
Summary = 'Summary',
}

View File

@ -1,5 +1,5 @@
import { ipcMain } from 'electron' import { ipcMain } from 'electron'
import { resolve } from 'path' import { resolve, sep } from 'path'
import { WindowManager } from './../managers/window' import { WindowManager } from './../managers/window'
import request from 'request' import request from 'request'
import { createWriteStream, renameSync } from 'fs' import { createWriteStream, renameSync } from 'fs'
@ -46,7 +46,7 @@ export function handleDownloaderIPCs() {
DownloadEvent.onFileDownloadError, DownloadEvent.onFileDownloadError,
{ {
fileName, fileName,
err: { message: 'aborted' }, error: 'aborted',
} }
) )
} }
@ -68,7 +68,7 @@ export function handleDownloaderIPCs() {
if (typeof localPath === 'string') { if (typeof localPath === 'string') {
localPath = normalizeFilePath(localPath) localPath = normalizeFilePath(localPath)
} }
const array = localPath.split('/') const array = localPath.split(sep)
const fileName = array.pop() ?? '' const fileName = array.pop() ?? ''
const modelId = array.pop() ?? '' const modelId = array.pop() ?? ''
@ -92,13 +92,13 @@ export function handleDownloaderIPCs() {
} }
) )
}) })
.on('error', function (err: Error) { .on('error', function (error: Error) {
WindowManager?.instance.currentWindow?.webContents.send( WindowManager?.instance.currentWindow?.webContents.send(
DownloadEvent.onFileDownloadError, DownloadEvent.onFileDownloadError,
{ {
fileName, fileName,
err,
modelId, modelId,
error,
} }
) )
}) })
@ -121,7 +121,7 @@ export function handleDownloaderIPCs() {
{ {
fileName, fileName,
modelId, modelId,
err: { message: 'aborted' }, error: 'aborted',
} }
) )
} }

View File

@ -25,25 +25,11 @@ import { migrateExtensions } from './utils/migration'
import { cleanUpAndQuit } from './utils/clean' import { cleanUpAndQuit } from './utils/clean'
import { setupExtensions } from './utils/extension' import { setupExtensions } from './utils/extension'
import { setupCore } from './utils/setup' import { setupCore } from './utils/setup'
import { setupReactDevTool } from './utils/dev'
app app
.whenReady() .whenReady()
.then(async () => { .then(setupReactDevTool)
if (!app.isPackaged) {
// Which means you're running from source code
const { default: installExtension, REACT_DEVELOPER_TOOLS } = await import(
'electron-devtools-installer'
) // Don't use import on top level, since the installer package is dev-only
try {
const name = installExtension(REACT_DEVELOPER_TOOLS)
console.log(`Added Extension: ${name}`)
} catch (err) {
console.log('An error occurred while installing devtools:')
console.error(err)
// Only log the error and don't throw it because it's not critical
}
}
})
.then(setupCore) .then(setupCore)
.then(createUserSpace) .then(createUserSpace)
.then(migrateExtensions) .then(migrateExtensions)

View File

@ -63,11 +63,11 @@
"build:test:darwin": "tsc -p . && electron-builder -p never -m --dir", "build:test:darwin": "tsc -p . && electron-builder -p never -m --dir",
"build:test:win32": "tsc -p . && electron-builder -p never -w --dir", "build:test:win32": "tsc -p . && electron-builder -p never -w --dir",
"build:test:linux": "tsc -p . && electron-builder -p never -l --dir", "build:test:linux": "tsc -p . && electron-builder -p never -l --dir",
"build:darwin": "tsc -p . && electron-builder -p never -m --x64 --arm64", "build:darwin": "tsc -p . && electron-builder -p never -m",
"build:win32": "tsc -p . && electron-builder -p never -w", "build:win32": "tsc -p . && electron-builder -p never -w",
"build:linux": "tsc -p . && electron-builder -p never -l deb -l AppImage", "build:linux": "tsc -p . && electron-builder -p never -l deb -l AppImage",
"build:publish": "yarn copy:assets && run-script-os", "build:publish": "yarn copy:assets && run-script-os",
"build:publish:darwin": "tsc -p . && electron-builder -p always -m --x64 --arm64", "build:publish:darwin": "tsc -p . && electron-builder -p always -m",
"build:publish:win32": "tsc -p . && electron-builder -p always -w", "build:publish:win32": "tsc -p . && electron-builder -p always -w",
"build:publish:linux": "tsc -p . && electron-builder -p always -l deb -l AppImage" "build:publish:linux": "tsc -p . && electron-builder -p always -l deb -l AppImage"
}, },

18
electron/utils/dev.ts Normal file
View File

@ -0,0 +1,18 @@
import { app } from 'electron'
export const setupReactDevTool = async () => {
if (!app.isPackaged) {
// Which means you're running from source code
const { default: installExtension, REACT_DEVELOPER_TOOLS } = await import(
'electron-devtools-installer'
) // Don't use import on top level, since the installer package is dev-only
try {
const name = installExtension(REACT_DEVELOPER_TOOLS)
console.log(`Added Extension: ${name}`)
} catch (err) {
console.log('An error occurred while installing devtools:')
console.error(err)
// Only log the error and don't throw it because it's not critical
}
}
}

View File

@ -1,6 +1,6 @@
{ {
"name": "@janhq/assistant-extension", "name": "@janhq/assistant-extension",
"version": "1.0.0", "version": "1.0.1",
"description": "This extension enables assistants, including Jan, a default assistant that can call all downloaded models", "description": "This extension enables assistants, including Jan, a default assistant that can call all downloaded models",
"main": "dist/index.js", "main": "dist/index.js",
"node": "dist/node/index.js", "node": "dist/node/index.js",

View File

@ -14,6 +14,7 @@ import {
export default class JanAssistantExtension extends AssistantExtension { export default class JanAssistantExtension extends AssistantExtension {
private static readonly _homeDir = "file://assistants"; private static readonly _homeDir = "file://assistants";
private static readonly _threadDir = "file://threads";
controller = new AbortController(); controller = new AbortController();
isCancelled = false; isCancelled = false;
@ -64,6 +65,8 @@ export default class JanAssistantExtension extends AssistantExtension {
if ( if (
data.model?.engine !== InferenceEngine.tool_retrieval_enabled || data.model?.engine !== InferenceEngine.tool_retrieval_enabled ||
!data.messages || !data.messages ||
// TODO: Since the engine is defined, its unsafe to assume that assistant tools are defined
// That could lead to an issue where thread stuck at generating response
!data.thread?.assistants[0]?.tools !data.thread?.assistants[0]?.tools
) { ) {
return; return;
@ -71,11 +74,12 @@ export default class JanAssistantExtension extends AssistantExtension {
const latestMessage = data.messages[data.messages.length - 1]; const latestMessage = data.messages[data.messages.length - 1];
// Ingest the document if needed // 1. Ingest the document if needed
if ( if (
latestMessage && latestMessage &&
latestMessage.content && latestMessage.content &&
typeof latestMessage.content !== "string" typeof latestMessage.content !== "string" &&
latestMessage.content.length > 1
) { ) {
const docFile = latestMessage.content[1]?.doc_url?.url; const docFile = latestMessage.content[1]?.doc_url?.url;
if (docFile) { if (docFile) {
@ -86,9 +90,29 @@ export default class JanAssistantExtension extends AssistantExtension {
data.model?.proxyEngine data.model?.proxyEngine
); );
} }
} else if (
// Check whether we need to ingest document or not
// Otherwise wrong context will be sent
!(await fs.existsSync(
await joinPath([
JanAssistantExtension._threadDir,
data.threadId,
"memory",
])
))
) {
// No document ingested, reroute the result to inference engine
const output = {
...data,
model: {
...data.model,
engine: data.model.proxyEngine,
},
};
events.emit(MessageEvent.OnMessageSent, output);
return;
} }
// 2. Load agent on thread changed
// Load agent on thread changed
if (instance.retrievalThreadId !== data.threadId) { if (instance.retrievalThreadId !== data.threadId) {
await executeOnMain(NODE, "toolRetrievalLoadThreadMemory", data.threadId); await executeOnMain(NODE, "toolRetrievalLoadThreadMemory", data.threadId);
@ -103,22 +127,22 @@ export default class JanAssistantExtension extends AssistantExtension {
); );
} }
// 3. Using the retrieval template with the result and query
if (latestMessage.content) { if (latestMessage.content) {
const prompt = const prompt =
typeof latestMessage.content === "string" typeof latestMessage.content === "string"
? latestMessage.content ? latestMessage.content
: latestMessage.content[0].text; : latestMessage.content[0].text;
// Retrieve the result // Retrieve the result
console.debug("toolRetrievalQuery", latestMessage.content);
const retrievalResult = await executeOnMain( const retrievalResult = await executeOnMain(
NODE, NODE,
"toolRetrievalQueryResult", "toolRetrievalQueryResult",
prompt prompt
); );
console.debug("toolRetrievalQueryResult", retrievalResult);
// Update the message content // Update message content
// Using the retrieval template with the result and query if (data.thread?.assistants[0]?.tools && retrievalResult)
if (data.thread?.assistants[0].tools)
data.messages[data.messages.length - 1].content = data.messages[data.messages.length - 1].content =
data.thread.assistants[0].tools[0].settings?.retrieval_template data.thread.assistants[0].tools[0].settings?.retrieval_template
?.replace("{CONTEXT}", retrievalResult) ?.replace("{CONTEXT}", retrievalResult)
@ -140,7 +164,7 @@ export default class JanAssistantExtension extends AssistantExtension {
return message; return message;
}); });
// Reroute the result to inference engine // 4. Reroute the result to inference engine
const output = { const output = {
...data, ...data,
model: { model: {

View File

@ -1,39 +1,39 @@
import { getJanDataFolderPath, normalizeFilePath } from "@janhq/core/node"; import { getJanDataFolderPath, normalizeFilePath } from "@janhq/core/node";
import { Retrieval } from "./tools/retrieval"; import { retrieval } from "./tools/retrieval";
import path from "path"; import path from "path";
const retrieval = new Retrieval(); export function toolRetrievalUpdateTextSplitter(
export async function toolRetrievalUpdateTextSplitter(
chunkSize: number, chunkSize: number,
chunkOverlap: number, chunkOverlap: number
) { ) {
retrieval.updateTextSplitter(chunkSize, chunkOverlap); retrieval.updateTextSplitter(chunkSize, chunkOverlap);
return Promise.resolve();
} }
export async function toolRetrievalIngestNewDocument( export async function toolRetrievalIngestNewDocument(
file: string, file: string,
engine: string, engine: string
) { ) {
const filePath = path.join(getJanDataFolderPath(), normalizeFilePath(file)); const filePath = path.join(getJanDataFolderPath(), normalizeFilePath(file));
const threadPath = path.dirname(filePath.replace("files", "")); const threadPath = path.dirname(filePath.replace("files", ""));
retrieval.updateEmbeddingEngine(engine); retrieval.updateEmbeddingEngine(engine);
await retrieval.ingestAgentKnowledge(filePath, `${threadPath}/memory`); return retrieval
return Promise.resolve(); .ingestAgentKnowledge(filePath, `${threadPath}/memory`)
.catch((err) => {
console.error(err);
});
} }
export async function toolRetrievalLoadThreadMemory(threadId: string) { export async function toolRetrievalLoadThreadMemory(threadId: string) {
try { return retrieval
await retrieval.loadRetrievalAgent( .loadRetrievalAgent(
path.join(getJanDataFolderPath(), "threads", threadId, "memory"), path.join(getJanDataFolderPath(), "threads", threadId, "memory")
); )
return Promise.resolve(); .catch((err) => {
} catch (err) { console.error(err);
console.debug(err); });
}
} }
export async function toolRetrievalQueryResult(query: string) { export async function toolRetrievalQueryResult(query: string) {
const res = await retrieval.generateResult(query); return retrieval.generateResult(query).catch((err) => {
return Promise.resolve(res); console.error(err);
});
} }

View File

@ -35,6 +35,7 @@ export class Retrieval {
if (engine === "nitro") { if (engine === "nitro") {
this.embeddingModel = new OpenAIEmbeddings( this.embeddingModel = new OpenAIEmbeddings(
{ openAIApiKey: "nitro-embedding" }, { openAIApiKey: "nitro-embedding" },
// TODO: Raw settings
{ basePath: "http://127.0.0.1:3928/v1" }, { basePath: "http://127.0.0.1:3928/v1" },
); );
} else { } else {
@ -75,3 +76,5 @@ export class Retrieval {
return Promise.resolve(serializedDoc); return Promise.resolve(serializedDoc);
}; };
} }
export const retrieval = new Retrieval();

View File

@ -10,6 +10,7 @@ import {
ChatCompletionRole, ChatCompletionRole,
ContentType, ContentType,
MessageRequest, MessageRequest,
MessageRequestType,
MessageStatus, MessageStatus,
ThreadContent, ThreadContent,
ThreadMessage, ThreadMessage,
@ -250,6 +251,7 @@ export default class JanInferenceNitroExtension extends InferenceExtension {
const message: ThreadMessage = { const message: ThreadMessage = {
id: ulid(), id: ulid(),
thread_id: data.threadId, thread_id: data.threadId,
type: data.type,
assistant_id: data.assistantId, assistant_id: data.assistantId,
role: ChatCompletionRole.Assistant, role: ChatCompletionRole.Assistant,
content: [], content: [],
@ -258,7 +260,10 @@ export default class JanInferenceNitroExtension extends InferenceExtension {
updated: timestamp, updated: timestamp,
object: "thread.message", object: "thread.message",
}; };
if (data.type !== MessageRequestType.Summary) {
events.emit(MessageEvent.OnMessageResponse, message); events.emit(MessageEvent.OnMessageResponse, message);
}
this.isCancelled = false; this.isCancelled = false;
this.controller = new AbortController(); this.controller = new AbortController();

View File

@ -18,6 +18,7 @@ import {
InferenceEngine, InferenceEngine,
BaseExtension, BaseExtension,
MessageEvent, MessageEvent,
MessageRequestType,
ModelEvent, ModelEvent,
InferenceEvent, InferenceEvent,
AppConfigurationEventName, AppConfigurationEventName,
@ -157,6 +158,7 @@ export default class JanInferenceOpenAIExtension extends BaseExtension {
const message: ThreadMessage = { const message: ThreadMessage = {
id: ulid(), id: ulid(),
thread_id: data.threadId, thread_id: data.threadId,
type: data.type,
assistant_id: data.assistantId, assistant_id: data.assistantId,
role: ChatCompletionRole.Assistant, role: ChatCompletionRole.Assistant,
content: [], content: [],
@ -165,7 +167,10 @@ export default class JanInferenceOpenAIExtension extends BaseExtension {
updated: timestamp, updated: timestamp,
object: "thread.message", object: "thread.message",
}; };
if (data.type !== MessageRequestType.Summary) {
events.emit(MessageEvent.OnMessageResponse, message); events.emit(MessageEvent.OnMessageResponse, message);
}
instance.isCancelled = false; instance.isCancelled = false;
instance.controller = new AbortController(); instance.controller = new AbortController();

View File

@ -21,6 +21,7 @@
&-item { &-item {
@apply hover:bg-secondary relative my-1 block w-full cursor-pointer select-none items-center rounded-sm px-4 py-2 text-sm data-[disabled]:pointer-events-none data-[disabled]:opacity-50; @apply hover:bg-secondary relative my-1 block w-full cursor-pointer select-none items-center rounded-sm px-4 py-2 text-sm data-[disabled]:pointer-events-none data-[disabled]:opacity-50;
@apply focus:outline-none focus-visible:outline-0;
} }
&-trigger-viewport { &-trigger-viewport {

View File

@ -14,7 +14,14 @@ import {
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai' import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'
import { MonitorIcon } from 'lucide-react' import {
MonitorIcon,
LayoutGridIcon,
FoldersIcon,
GlobeIcon,
CheckIcon,
CopyIcon,
} from 'lucide-react'
import { twMerge } from 'tailwind-merge' import { twMerge } from 'tailwind-merge'
@ -22,6 +29,7 @@ import { MainViewState } from '@/constants/screens'
import { useActiveModel } from '@/hooks/useActiveModel' import { useActiveModel } from '@/hooks/useActiveModel'
import { useClipboard } from '@/hooks/useClipboard'
import { useMainViewState } from '@/hooks/useMainViewState' import { useMainViewState } from '@/hooks/useMainViewState'
import useRecommendedModel from '@/hooks/useRecommendedModel' import useRecommendedModel from '@/hooks/useRecommendedModel'
@ -42,6 +50,8 @@ import {
export const selectedModelAtom = atom<Model | undefined>(undefined) export const selectedModelAtom = atom<Model | undefined>(undefined)
const engineOptions = ['Local', 'Remote']
// TODO: Move all of the unscoped logics outside of the component // TODO: Move all of the unscoped logics outside of the component
const DropdownListSidebar = ({ const DropdownListSidebar = ({
strictedThread = true, strictedThread = true,
@ -51,13 +61,24 @@ const DropdownListSidebar = ({
const activeThread = useAtomValue(activeThreadAtom) const activeThread = useAtomValue(activeThreadAtom)
const [selectedModel, setSelectedModel] = useAtom(selectedModelAtom) const [selectedModel, setSelectedModel] = useAtom(selectedModelAtom)
const setThreadModelParams = useSetAtom(setThreadModelParamsAtom) const setThreadModelParams = useSetAtom(setThreadModelParamsAtom)
const [isTabActive, setIsTabActive] = useState(0)
const { stateModel } = useActiveModel() const { stateModel } = useActiveModel()
const [serverEnabled, setServerEnabled] = useAtom(serverEnabledAtom) const [serverEnabled, setServerEnabled] = useAtom(serverEnabledAtom)
const { setMainViewState } = useMainViewState() const { setMainViewState } = useMainViewState()
const [loader, setLoader] = useState(0) const [loader, setLoader] = useState(0)
const { recommendedModel, downloadedModels } = useRecommendedModel() const { recommendedModel, downloadedModels } = useRecommendedModel()
const { updateModelParameter } = useUpdateModelParameters() const { updateModelParameter } = useUpdateModelParameters()
const clipboard = useClipboard({ timeout: 1000 })
const [copyId, setCopyId] = useState('')
const localModel = downloadedModels.filter(
(model) => model.engine === InferenceEngine.nitro
)
const remoteModel = downloadedModels.filter(
(model) => model.engine === InferenceEngine.openai
)
const modelOptions = isTabActive === 0 ? localModel : remoteModel
useEffect(() => { useEffect(() => {
if (!activeThread) return if (!activeThread) return
@ -171,28 +192,93 @@ const DropdownListSidebar = ({
</SelectTrigger> </SelectTrigger>
<SelectPortal> <SelectPortal>
<SelectContent className="right-2 block w-full min-w-[450px] pr-0"> <SelectContent className="right-2 block w-full min-w-[450px] pr-0">
<div className="flex w-full items-center space-x-2 px-4 py-2"> <div className="relative px-2 py-2 dark:bg-secondary/50">
<MonitorIcon size={20} className="text-muted-foreground" /> <ul className="inline-flex w-full space-x-2 rounded-lg bg-zinc-100 px-1 dark:bg-secondary">
<span>Local</span> {engineOptions.map((name, i) => {
return (
<li
className={twMerge(
'relative my-1 flex w-full cursor-pointer items-center justify-center space-x-2 px-2 py-2',
isTabActive === i &&
'rounded-md bg-background dark:bg-white'
)}
key={i}
onClick={() => setIsTabActive(i)}
>
{i === 0 ? (
<MonitorIcon
size={20}
className="z-50 text-muted-foreground"
/>
) : (
<GlobeIcon
size={20}
className="z-50 text-muted-foreground"
/>
)}
<span
className={twMerge(
'relative z-50 font-medium text-muted-foreground',
isTabActive === i &&
'font-bold text-foreground dark:text-black'
)}
>
{name}
</span>
</li>
)
})}
</ul>
</div> </div>
<div className="border-b border-border" /> <div className="border-b border-border" />
{downloadedModels.length === 0 ? ( {downloadedModels.length === 0 ? (
<div className="px-4 py-2"> <div className="px-4 py-2">
<p>{`Oops, you don't have a model yet.`}</p> <p>{`Oops, you don't have a model yet.`}</p>
</div> </div>
) : ( ) : (
<SelectGroup> <SelectGroup className="py-2">
{downloadedModels.map((x, i) => ( <>
<SelectItem {modelOptions.map((x, i) => (
<div
key={i} key={i}
value={x.id}
className={twMerge( className={twMerge(
x.id === selectedModel?.id && 'bg-secondary' x.id === selectedModel?.id && 'bg-secondary',
'hover:bg-secondary'
)} )}
> >
<div className="flex w-full justify-between"> <SelectItem
<span className="line-clamp-1 block">{x.name}</span> value={x.id}
<div className="space-x-2"> className={twMerge(
x.id === selectedModel?.id && 'bg-secondary',
'my-0 pb-8 pt-4'
)}
>
<div className="relative flex w-full justify-between">
{x.engine === InferenceEngine.openai && (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="absolute top-1"
>
<path
d="M18.5681 8.18423C18.7917 7.51079 18.8691 6.79739 18.795 6.09168C18.7209 5.38596 18.497 4.70419 18.1384 4.0919C17.6067 3.16642 16.7948 2.43369 15.8199 1.99936C14.8449 1.56503 13.7572 1.45153 12.7135 1.67523C12.1206 1.0157 11.3646 0.523789 10.5214 0.248906C9.67823 -0.0259764 8.77756 -0.0741542 7.90986 0.109212C7.04216 0.292577 6.23798 0.701031 5.57809 1.29355C4.91821 1.88607 4.42584 2.64179 4.15046 3.48481C3.45518 3.62739 2.79834 3.91672 2.22384 4.33347C1.64933 4.75023 1.1704 5.28481 0.81904 5.90148C0.281569 6.82542 0.0518576 7.89634 0.163116 8.95943C0.274374 10.0225 0.720837 11.0227 1.43796 11.8153C1.21351 12.4884 1.13539 13.2017 1.20883 13.9074C1.28227 14.6132 1.50557 15.2951 1.86379 15.9076C2.39616 16.8334 3.20872 17.5663 4.18438 18.0006C5.16004 18.4349 6.24841 18.5483 7.29262 18.3243C7.76367 18.8548 8.34248 19.2786 8.99038 19.5676C9.63828 19.8566 10.3404 20.004 11.0498 20C12.1195 20.001 13.1618 19.662 14.0263 19.032C14.8909 18.4021 15.5329 17.5137 15.8596 16.4951C16.5548 16.3523 17.2116 16.0629 17.786 15.6461C18.3605 15.2294 18.8395 14.6949 19.191 14.0784C19.7222 13.1558 19.9479 12.0889 19.836 11.0303C19.7242 9.97163 19.2804 8.9754 18.5681 8.18423ZM11.0498 18.691C10.1737 18.6924 9.32512 18.3853 8.65279 17.8236L8.77104 17.7566L12.753 15.4581C12.8521 15.4 12.9343 15.3171 12.9917 15.2176C13.0491 15.118 13.0796 15.0053 13.0802 14.8904V9.27631L14.7635 10.2501C14.7719 10.2544 14.7791 10.2605 14.7846 10.268C14.7901 10.2755 14.7937 10.2843 14.7952 10.2935V14.9456C14.7931 15.9383 14.3978 16.8898 13.6959 17.5917C12.9939 18.2936 12.0425 18.6889 11.0498 18.691ZM2.99921 15.2531C2.55985 14.4945 2.4021 13.6052 2.55371 12.7417L2.67204 12.8127L6.65787 15.1112C6.7565 15.1691 6.86877 15.1996 6.98312 15.1996C7.09747 15.1996 7.20975 15.1691 7.30837 15.1112L12.1774 12.3041V14.2478C12.1769 14.2579 12.1742 14.2677 12.1694 14.2766C12.1646 14.2855 12.1579 14.2932 12.1497 14.2991L8.11654 16.6251C7.25581 17.121 6.2335 17.255 5.27405 16.9978C4.3146 16.7405 3.49644 16.1131 2.99921 15.2531ZM1.95054 6.57965C2.39294 5.81612 3.09123 5.23375 3.92179 4.93565V9.66665C3.92029 9.78094 3.94949 9.89355 4.00635 9.99271C4.06321 10.0919 4.14564 10.174 4.24504 10.2304L9.09037 13.0256L7.40696 13.9994C7.39785 14.0042 7.38769 14.0068 7.37737 14.0068C7.36706 14.0068 7.3569 14.0042 7.34779 13.9994L3.32254 11.6773C2.46343 11.1793 1.83666 10.3612 1.57951 9.40204C1.32236 8.44291 1.45577 7.42095 1.95054 6.55998V6.57965ZM15.7808 9.79281L10.9197 6.96998L12.5992 5.99998C12.6083 5.99514 12.6185 5.99261 12.6288 5.99261C12.6391 5.99261 12.6493 5.99514 12.6584 5.99998L16.6836 8.32606C17.2991 8.68119 17.8008 9.20407 18.1303 9.83365C18.4597 10.4632 18.6032 11.1735 18.5441 11.8816C18.485 12.5898 18.2257 13.2664 17.7964 13.8327C17.3672 14.3989 16.7857 14.8314 16.1199 15.0796V10.3486C16.1164 10.2345 16.0833 10.1232 16.0238 10.0258C15.9644 9.92833 15.8807 9.8481 15.7808 9.79281ZM17.4564 7.27356L17.338 7.20256L13.3601 4.8844C13.2609 4.82617 13.1479 4.79547 13.0329 4.79547C12.9178 4.79547 12.8049 4.82617 12.7056 4.8844L7.84071 7.6914V5.74781C7.83967 5.73793 7.84132 5.72795 7.84549 5.71893C7.84965 5.70991 7.85618 5.70218 7.86437 5.69656L11.8896 3.3744C12.5066 3.01899 13.2119 2.84659 13.9232 2.87736C14.6345 2.90813 15.3224 3.14079 15.9063 3.54813C16.4903 3.95548 16.9461 4.52066 17.2206 5.17759C17.4952 5.83452 17.577 6.55602 17.4565 7.25773L17.4564 7.27356ZM6.92196 10.7191L5.23862 9.74931C5.2302 9.74424 5.223 9.73738 5.21753 9.72921C5.21205 9.72105 5.20845 9.71178 5.20696 9.70206V5.06181C5.20788 4.34996 5.41144 3.65307 5.79383 3.05265C6.17622 2.45222 6.72164 1.97305 7.36632 1.67118C8.011 1.3693 8.7283 1.2572 9.43434 1.34796C10.1404 1.43873 10.806 1.72861 11.3534 2.18373L11.235 2.25081L7.25321 4.54915C7.1541 4.60727 7.07182 4.69017 7.01445 4.78971C6.95707 4.88925 6.92658 5.00201 6.92596 5.1169L6.92196 10.7191ZM7.83662 8.74798L10.005 7.49815L12.1774 8.74798V11.2475L10.0129 12.4972L7.84062 11.2475L7.83662 8.74798Z"
fill="#18181B"
/>
</svg>
)}
<div
className={twMerge(
x.engine === InferenceEngine.openai && 'pl-8'
)}
>
<span className="line-clamp-1 block">
{x.name}
</span>
<div className="absolute right-0 top-2 space-x-2">
<span className="font-bold text-muted-foreground"> <span className="font-bold text-muted-foreground">
{toGibibytes(x.metadata.size)} {toGibibytes(x.metadata.size)}
</span> </span>
@ -201,18 +287,50 @@ const DropdownListSidebar = ({
)} )}
</div> </div>
</div> </div>
</div>
</SelectItem> </SelectItem>
<div
className={twMerge(
'absolute -mt-6 inline-flex items-center space-x-2 px-4 pb-2 text-muted-foreground',
x.engine === InferenceEngine.openai && 'left-8'
)}
>
<span className="text-xs">{x.id}</span>
{clipboard.copied && copyId === x.id ? (
<CheckIcon size={16} className="text-green-600" />
) : (
<CopyIcon
size={16}
className="z-20 cursor-pointer"
onClick={() => {
clipboard.copy(x.id)
setCopyId(x.id)
}}
/>
)}
</div>
</div>
))} ))}
</>
</SelectGroup> </SelectGroup>
)} )}
<div className="border-b border-border" /> <div className="border-b border-border" />
<div className="w-full px-4 py-2"> <div className="flex w-full space-x-2 px-4 py-2">
<Button
block
themes="secondary"
onClick={() => setMainViewState(MainViewState.Settings)}
>
<FoldersIcon size={20} className="mr-2" />
<span>My Models</span>
</Button>
<Button <Button
block block
className="bg-blue-100 font-bold text-blue-600 hover:bg-blue-100 hover:text-blue-600" className="bg-blue-100 font-bold text-blue-600 hover:bg-blue-100 hover:text-blue-600"
onClick={() => setMainViewState(MainViewState.Hub)} onClick={() => setMainViewState(MainViewState.Hub)}
> >
Explore The Hub <LayoutGridIcon size={20} className="mr-2" />
<span>Explore The Hub</span>
</Button> </Button>
</div> </div>
</SelectContent> </SelectContent>

View File

@ -2,16 +2,21 @@
import { ReactNode, useCallback, useEffect, useRef } from 'react' import { ReactNode, useCallback, useEffect, useRef } from 'react'
import { import {
ChatCompletionMessage,
ChatCompletionRole,
events, events,
ThreadMessage, ThreadMessage,
ExtensionTypeEnum, ExtensionTypeEnum,
MessageStatus, MessageStatus,
MessageRequest,
Model, Model,
ConversationalExtension, ConversationalExtension,
MessageEvent, MessageEvent,
MessageRequestType,
ModelEvent, ModelEvent,
} from '@janhq/core' } from '@janhq/core'
import { useAtomValue, useSetAtom } from 'jotai' import { useAtomValue, useSetAtom } from 'jotai'
import { ulid } from 'ulid'
import { import {
activeModelAtom, activeModelAtom,
@ -25,6 +30,7 @@ import { toaster } from '../Toast'
import { extensionManager } from '@/extension' import { extensionManager } from '@/extension'
import { import {
getCurrentChatMessagesAtom,
addNewMessageAtom, addNewMessageAtom,
updateMessageAtom, updateMessageAtom,
} from '@/helpers/atoms/ChatMessage.atom' } from '@/helpers/atoms/ChatMessage.atom'
@ -37,9 +43,11 @@ import {
} from '@/helpers/atoms/Thread.atom' } from '@/helpers/atoms/Thread.atom'
export default function EventHandler({ children }: { children: ReactNode }) { export default function EventHandler({ children }: { children: ReactNode }) {
const messages = useAtomValue(getCurrentChatMessagesAtom)
const addNewMessage = useSetAtom(addNewMessageAtom) const addNewMessage = useSetAtom(addNewMessageAtom)
const updateMessage = useSetAtom(updateMessageAtom) const updateMessage = useSetAtom(updateMessageAtom)
const downloadedModels = useAtomValue(downloadedModelsAtom) const downloadedModels = useAtomValue(downloadedModelsAtom)
const activeModel = useAtomValue(activeModelAtom)
const setActiveModel = useSetAtom(activeModelAtom) const setActiveModel = useSetAtom(activeModelAtom)
const setStateModel = useSetAtom(stateModelAtom) const setStateModel = useSetAtom(stateModelAtom)
const setQueuedMessage = useSetAtom(queuedMessageAtom) const setQueuedMessage = useSetAtom(queuedMessageAtom)
@ -51,6 +59,8 @@ export default function EventHandler({ children }: { children: ReactNode }) {
const threadsRef = useRef(threads) const threadsRef = useRef(threads)
const setIsGeneratingResponse = useSetAtom(isGeneratingResponseAtom) const setIsGeneratingResponse = useSetAtom(isGeneratingResponseAtom)
const updateThread = useSetAtom(updateThreadAtom) const updateThread = useSetAtom(updateThreadAtom)
const messagesRef = useRef(messages)
const activeModelRef = useRef(activeModel)
useEffect(() => { useEffect(() => {
threadsRef.current = threads threadsRef.current = threads
@ -60,9 +70,51 @@ export default function EventHandler({ children }: { children: ReactNode }) {
modelsRef.current = downloadedModels modelsRef.current = downloadedModels
}, [downloadedModels]) }, [downloadedModels])
useEffect(() => {
messagesRef.current = messages
}, [messages])
useEffect(() => {
activeModelRef.current = activeModel
}, [activeModel])
const onNewMessageResponse = useCallback( const onNewMessageResponse = useCallback(
(message: ThreadMessage) => { (message: ThreadMessage) => {
const thread = threadsRef.current?.find((e) => e.id == message.thread_id)
// If this is the first ever prompt in the thread
if (thread && thread.title.trim() == 'New Thread') {
// This is the first time message comes in on a new thread
// Summarize the first message, and make that the title of the Thread
// 1. Get the summary of the first prompt using whatever engine user is currently using
const firstPrompt = messagesRef?.current[0].content[0].text.value.trim()
const summarizeFirstPrompt =
'Summarize "' + firstPrompt + '" in 5 words as a title'
// Prompt: Given this query from user {query}, return to me the summary in 5 words as the title
const msgId = ulid()
const messages: ChatCompletionMessage[] = [
{
role: ChatCompletionRole.User,
content: summarizeFirstPrompt,
} as ChatCompletionMessage,
]
const firstPromptRequest: MessageRequest = {
id: msgId,
threadId: message.thread_id,
type: MessageRequestType.Summary,
messages,
model: activeModelRef?.current,
}
// 2. Update the title with the result of the inference
// the title will be updated as part of the `EventName.OnFirstPromptUpdate`
events.emit(MessageEvent.OnMessageSent, firstPromptRequest)
}
if (message.type !== MessageRequestType.Summary) {
addNewMessage(message) addNewMessage(message)
}
}, },
[addNewMessage] [addNewMessage]
) )
@ -134,6 +186,11 @@ export default function EventHandler({ children }: { children: ReactNode }) {
...(messageContent && { lastMessage: messageContent }), ...(messageContent && { lastMessage: messageContent }),
} }
// Update the Thread title with the response of the inference on the 1st prompt
if (message.type === MessageRequestType.Summary) {
thread.title = messageContent
}
updateThread({ updateThread({
...thread, ...thread,
metadata, metadata,
@ -146,10 +203,13 @@ export default function EventHandler({ children }: { children: ReactNode }) {
metadata, metadata,
}) })
// If this is not the summary of the Thread, don't need to add it to the Thread
if (message.type !== MessageRequestType.Summary) {
extensionManager extensionManager
.get<ConversationalExtension>(ExtensionTypeEnum.Conversational) .get<ConversationalExtension>(ExtensionTypeEnum.Conversational)
?.addNewMessage(message) ?.addNewMessage(message)
} }
}
}, },
[updateMessage, updateThreadWaiting, setIsGeneratingResponse, updateThread] [updateMessage, updateThreadWaiting, setIsGeneratingResponse, updateThread]
) )

View File

@ -44,7 +44,10 @@ export const setDownloadStateAtom = atom(
}) })
} else { } else {
let error = state.error let error = state.error
if (state.error?.includes('certificate')) { if (
typeof error?.includes === 'function' &&
state.error?.includes('certificate')
) {
error += error +=
'. To fix enable "Ignore SSL Certificates" in Advanced settings.' '. To fix enable "Ignore SSL Certificates" in Advanced settings.'
} }

View File

@ -6,6 +6,7 @@ import {
ChatCompletionRole, ChatCompletionRole,
ContentType, ContentType,
MessageRequest, MessageRequest,
MessageRequestType,
MessageStatus, MessageStatus,
ExtensionTypeEnum, ExtensionTypeEnum,
Thread, Thread,
@ -112,6 +113,7 @@ export default function useSendChatMessage() {
const messageRequest: MessageRequest = { const messageRequest: MessageRequest = {
id: ulid(), id: ulid(),
type: MessageRequestType.Thread,
messages: messages, messages: messages,
threadId: activeThread.id, threadId: activeThread.id,
model: activeThread.assistants[0].model ?? selectedModel, model: activeThread.assistants[0].model ?? selectedModel,
@ -209,6 +211,7 @@ export default function useSendChatMessage() {
} }
const messageRequest: MessageRequest = { const messageRequest: MessageRequest = {
id: msgId, id: msgId,
type: MessageRequestType.Thread,
threadId: activeThread.id, threadId: activeThread.id,
messages, messages,
model: { model: {
@ -218,8 +221,8 @@ export default function useSendChatMessage() {
}, },
thread: activeThread, thread: activeThread,
} }
const timestamp = Date.now()
const timestamp = Date.now()
const content: any = [] const content: any = []
if (base64Blob && fileUpload[0]?.type === 'image') { if (base64Blob && fileUpload[0]?.type === 'image') {

View File

@ -373,6 +373,28 @@ const LocalServerScreen = () => {
)} )}
> >
<div className="px-4 pt-4"> <div className="px-4 pt-4">
<div className="mb-4 flex items-start space-x-2">
<svg
width="18"
height="18"
viewBox="0 0 18 18"
className="mt-1 flex-shrink-0"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M9.00033 17.3337C13.6027 17.3337 17.3337 13.6027 17.3337 9.00033C17.3337 4.39795 13.6027 0.666992 9.00033 0.666992C4.39795 0.666992 0.666992 4.39795 0.666992 9.00033C0.666992 10.9978 1.36978 12.8311 2.54157 14.2666L0.910703 15.9111C0.390085 16.436 0.758808 17.3337 1.49507 17.3337H9.00033ZM5.25033 7.33366C5.25033 6.87342 5.62342 6.50033 6.08366 6.50033H11.917C12.3772 6.50033 12.7503 6.87342 12.7503 7.33366C12.7503 7.7939 12.3772 8.16699 11.917 8.16699H6.08366C5.62342 8.16699 5.25033 7.7939 5.25033 7.33366ZM6.08366 9.83366C5.62342 9.83366 5.25033 10.2068 5.25033 10.667C5.25033 11.1272 5.62342 11.5003 6.08366 11.5003H8.58366C9.0439 11.5003 9.41699 11.1272 9.41699 10.667C9.41699 10.2068 9.0439 9.83366 8.58366 9.83366H6.08366Z"
fill="#2563EB"
/>
</svg>
<p>
You can concurrently send requests to one active local model and
multiple remote models.
</p>
</div>
<DropdownListSidebar strictedThread={false} /> <DropdownListSidebar strictedThread={false} />
{loadModelError && ( {loadModelError && (
<div className="mt-3 flex space-x-2 text-xs"> <div className="mt-3 flex space-x-2 text-xs">