Merge pull request #4338 from janhq/main
Merge bug fixes from 0.5.12 into dev
This commit is contained in:
commit
da4336cc49
@ -40,7 +40,7 @@ export default class CortexConversationalExtension extends ConversationalExtensi
|
|||||||
async listThreads(): Promise<Thread[]> {
|
async listThreads(): Promise<Thread[]> {
|
||||||
return this.queue.add(() =>
|
return this.queue.add(() =>
|
||||||
ky
|
ky
|
||||||
.get(`${API_URL}/v1/threads`)
|
.get(`${API_URL}/v1/threads?limit=-1`)
|
||||||
.json<ThreadList>()
|
.json<ThreadList>()
|
||||||
.then((e) => e.data)
|
.then((e) => e.data)
|
||||||
) as Promise<Thread[]>
|
) as Promise<Thread[]>
|
||||||
@ -133,7 +133,7 @@ export default class CortexConversationalExtension extends ConversationalExtensi
|
|||||||
async listMessages(threadId: string): Promise<ThreadMessage[]> {
|
async listMessages(threadId: string): Promise<ThreadMessage[]> {
|
||||||
return this.queue.add(() =>
|
return this.queue.add(() =>
|
||||||
ky
|
ky
|
||||||
.get(`${API_URL}/v1/threads/${threadId}/messages?order=asc`)
|
.get(`${API_URL}/v1/threads/${threadId}/messages?order=asc&limit=-1`)
|
||||||
.json<MessageList>()
|
.json<MessageList>()
|
||||||
.then((e) => e.data)
|
.then((e) => e.data)
|
||||||
) as Promise<ThreadMessage[]>
|
) as Promise<ThreadMessage[]>
|
||||||
@ -147,7 +147,9 @@ export default class CortexConversationalExtension extends ConversationalExtensi
|
|||||||
*/
|
*/
|
||||||
async getThreadAssistant(threadId: string): Promise<ThreadAssistantInfo> {
|
async getThreadAssistant(threadId: string): Promise<ThreadAssistantInfo> {
|
||||||
return this.queue.add(() =>
|
return this.queue.add(() =>
|
||||||
ky.get(`${API_URL}/v1/assistants/${threadId}`).json<ThreadAssistantInfo>()
|
ky
|
||||||
|
.get(`${API_URL}/v1/assistants/${threadId}?limit=-1`)
|
||||||
|
.json<ThreadAssistantInfo>()
|
||||||
) as Promise<ThreadAssistantInfo>
|
) as Promise<ThreadAssistantInfo>
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
@ -188,7 +190,7 @@ export default class CortexConversationalExtension extends ConversationalExtensi
|
|||||||
* Do health check on cortex.cpp
|
* Do health check on cortex.cpp
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
healthz(): Promise<void> {
|
async healthz(): Promise<void> {
|
||||||
return ky
|
return ky
|
||||||
.get(`${API_URL}/healthz`, {
|
.get(`${API_URL}/healthz`, {
|
||||||
retry: { limit: 20, delay: () => 500, methods: ['get'] },
|
retry: { limit: 20, delay: () => 500, methods: ['get'] },
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
1.0.5-rc2
|
1.0.6
|
||||||
|
|||||||
@ -85,6 +85,63 @@
|
|||||||
},
|
},
|
||||||
"engine": "openai"
|
"engine": "openai"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"url": "https://openai.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"id": "gpt-4o-mini",
|
||||||
|
"object": "model",
|
||||||
|
"name": "OpenAI GPT 4o-mini",
|
||||||
|
"version": "1.1",
|
||||||
|
"description": "GPT-4o mini (“o” for “omni”) is a fast, affordable small model for focused tasks.",
|
||||||
|
"format": "api",
|
||||||
|
"settings": {
|
||||||
|
"vision_model": true
|
||||||
|
},
|
||||||
|
"parameters": {
|
||||||
|
"max_tokens": 16384,
|
||||||
|
"temperature": 0.7,
|
||||||
|
"top_p": 0.95,
|
||||||
|
"stream": true,
|
||||||
|
"stop": [],
|
||||||
|
"frequency_penalty": 0,
|
||||||
|
"presence_penalty": 0
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"author": "OpenAI",
|
||||||
|
"tags": ["General"]
|
||||||
|
},
|
||||||
|
"engine": "openai"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"url": "https://openai.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"id": "o1",
|
||||||
|
"object": "model",
|
||||||
|
"name": "OpenAI o1",
|
||||||
|
"version": "1.0",
|
||||||
|
"description": "OpenAI o1 is a new model with complex reasoning",
|
||||||
|
"format": "api",
|
||||||
|
"settings": {},
|
||||||
|
"parameters": {
|
||||||
|
"max_tokens": 100000,
|
||||||
|
"temperature": 1,
|
||||||
|
"top_p": 1,
|
||||||
|
"stream": true,
|
||||||
|
"frequency_penalty": 0,
|
||||||
|
"presence_penalty": 0
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"author": "OpenAI",
|
||||||
|
"tags": ["General"]
|
||||||
|
},
|
||||||
|
"engine": "openai"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"sources": [
|
"sources": [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -53,7 +53,7 @@ export class CortexAPI implements ICortexAPI {
|
|||||||
*/
|
*/
|
||||||
getModels(): Promise<Model[]> {
|
getModels(): Promise<Model[]> {
|
||||||
return this.queue
|
return this.queue
|
||||||
.add(() => ky.get(`${API_URL}/v1/models`).json<ModelList>())
|
.add(() => ky.get(`${API_URL}/v1/models?limit=-1`).json<ModelList>())
|
||||||
.then((e) =>
|
.then((e) =>
|
||||||
typeof e === 'object' ? e.data.map((e) => this.transformModel(e)) : []
|
typeof e === 'object' ? e.data.map((e) => this.transformModel(e)) : []
|
||||||
)
|
)
|
||||||
|
|||||||
@ -34,7 +34,11 @@ const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(
|
|||||||
return (
|
return (
|
||||||
<div className="textarea__wrapper">
|
<div className="textarea__wrapper">
|
||||||
<textarea
|
<textarea
|
||||||
className={twMerge('textarea', className)}
|
className={twMerge(
|
||||||
|
'textarea',
|
||||||
|
className,
|
||||||
|
autoResize && 'resize-none'
|
||||||
|
)}
|
||||||
ref={autoResize ? textareaRef : ref}
|
ref={autoResize ? textareaRef : ref}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -183,10 +183,7 @@ const ModelDropdown = ({
|
|||||||
if (!activeThread) return
|
if (!activeThread) return
|
||||||
const modelId = activeAssistant?.model?.id
|
const modelId = activeAssistant?.model?.id
|
||||||
|
|
||||||
let model = downloadedModels.find((model) => model.id === modelId)
|
const model = downloadedModels.find((model) => model.id === modelId)
|
||||||
if (!model) {
|
|
||||||
model = undefined
|
|
||||||
}
|
|
||||||
setSelectedModel(model)
|
setSelectedModel(model)
|
||||||
}, [
|
}, [
|
||||||
recommendedModel,
|
recommendedModel,
|
||||||
@ -378,14 +375,14 @@ const ModelDropdown = ({
|
|||||||
!selectedModel && 'text-[hsla(var(--text-tertiary))]'
|
!selectedModel && 'text-[hsla(var(--text-tertiary))]'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{selectedModel?.name || 'Select Model'}
|
{selectedModel?.name || 'Select a model'}
|
||||||
</span>
|
</span>
|
||||||
</Badge>
|
</Badge>
|
||||||
) : (
|
) : (
|
||||||
<Input
|
<Input
|
||||||
value={selectedModel?.name || ''}
|
value={selectedModel?.name || ''}
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
placeholder="Select Model"
|
placeholder="Select a model"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
readOnly
|
readOnly
|
||||||
suffixIcon={
|
suffixIcon={
|
||||||
|
|||||||
@ -18,7 +18,7 @@ import {
|
|||||||
extractInferenceParams,
|
extractInferenceParams,
|
||||||
ModelExtension,
|
ModelExtension,
|
||||||
} from '@janhq/core'
|
} from '@janhq/core'
|
||||||
import { useAtomValue, useSetAtom } from 'jotai'
|
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
|
||||||
import { ulid } from 'ulidx'
|
import { ulid } from 'ulidx'
|
||||||
|
|
||||||
import { activeModelAtom, stateModelAtom } from '@/hooks/useActiveModel'
|
import { activeModelAtom, stateModelAtom } from '@/hooks/useActiveModel'
|
||||||
@ -32,6 +32,7 @@ import {
|
|||||||
updateMessageAtom,
|
updateMessageAtom,
|
||||||
tokenSpeedAtom,
|
tokenSpeedAtom,
|
||||||
deleteMessageAtom,
|
deleteMessageAtom,
|
||||||
|
subscribedGeneratingMessageAtom,
|
||||||
} from '@/helpers/atoms/ChatMessage.atom'
|
} from '@/helpers/atoms/ChatMessage.atom'
|
||||||
import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom'
|
import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom'
|
||||||
import {
|
import {
|
||||||
@ -40,6 +41,7 @@ import {
|
|||||||
isGeneratingResponseAtom,
|
isGeneratingResponseAtom,
|
||||||
updateThreadAtom,
|
updateThreadAtom,
|
||||||
getActiveThreadModelParamsAtom,
|
getActiveThreadModelParamsAtom,
|
||||||
|
activeThreadAtom,
|
||||||
} from '@/helpers/atoms/Thread.atom'
|
} from '@/helpers/atoms/Thread.atom'
|
||||||
|
|
||||||
const maxWordForThreadTitle = 10
|
const maxWordForThreadTitle = 10
|
||||||
@ -54,6 +56,10 @@ export default function ModelHandler() {
|
|||||||
const activeModel = useAtomValue(activeModelAtom)
|
const activeModel = useAtomValue(activeModelAtom)
|
||||||
const setActiveModel = useSetAtom(activeModelAtom)
|
const setActiveModel = useSetAtom(activeModelAtom)
|
||||||
const setStateModel = useSetAtom(stateModelAtom)
|
const setStateModel = useSetAtom(stateModelAtom)
|
||||||
|
const [subscribedGeneratingMessage, setSubscribedGeneratingMessage] = useAtom(
|
||||||
|
subscribedGeneratingMessageAtom
|
||||||
|
)
|
||||||
|
const activeThread = useAtomValue(activeThreadAtom)
|
||||||
|
|
||||||
const updateThreadWaiting = useSetAtom(updateThreadWaitingForResponseAtom)
|
const updateThreadWaiting = useSetAtom(updateThreadWaitingForResponseAtom)
|
||||||
const threads = useAtomValue(threadsAtom)
|
const threads = useAtomValue(threadsAtom)
|
||||||
@ -62,11 +68,17 @@ export default function ModelHandler() {
|
|||||||
const setIsGeneratingResponse = useSetAtom(isGeneratingResponseAtom)
|
const setIsGeneratingResponse = useSetAtom(isGeneratingResponseAtom)
|
||||||
const updateThread = useSetAtom(updateThreadAtom)
|
const updateThread = useSetAtom(updateThreadAtom)
|
||||||
const messagesRef = useRef(messages)
|
const messagesRef = useRef(messages)
|
||||||
|
const messageGenerationSubscriber = useRef(subscribedGeneratingMessage)
|
||||||
const activeModelRef = useRef(activeModel)
|
const activeModelRef = useRef(activeModel)
|
||||||
|
const activeThreadRef = useRef(activeThread)
|
||||||
const activeModelParams = useAtomValue(getActiveThreadModelParamsAtom)
|
const activeModelParams = useAtomValue(getActiveThreadModelParamsAtom)
|
||||||
const activeModelParamsRef = useRef(activeModelParams)
|
const activeModelParamsRef = useRef(activeModelParams)
|
||||||
const setTokenSpeed = useSetAtom(tokenSpeedAtom)
|
const setTokenSpeed = useSetAtom(tokenSpeedAtom)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
activeThreadRef.current = activeThread
|
||||||
|
}, [activeThread])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
threadsRef.current = threads
|
threadsRef.current = threads
|
||||||
}, [threads])
|
}, [threads])
|
||||||
@ -87,6 +99,10 @@ export default function ModelHandler() {
|
|||||||
activeModelParamsRef.current = activeModelParams
|
activeModelParamsRef.current = activeModelParams
|
||||||
}, [activeModelParams])
|
}, [activeModelParams])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
messageGenerationSubscriber.current = subscribedGeneratingMessage
|
||||||
|
}, [subscribedGeneratingMessage])
|
||||||
|
|
||||||
const onNewMessageResponse = useCallback(
|
const onNewMessageResponse = useCallback(
|
||||||
async (message: ThreadMessage) => {
|
async (message: ThreadMessage) => {
|
||||||
if (message.type === MessageRequestType.Thread) {
|
if (message.type === MessageRequestType.Thread) {
|
||||||
@ -179,12 +195,19 @@ export default function ModelHandler() {
|
|||||||
|
|
||||||
const updateThreadMessage = useCallback(
|
const updateThreadMessage = useCallback(
|
||||||
(message: ThreadMessage) => {
|
(message: ThreadMessage) => {
|
||||||
updateMessage(
|
if (
|
||||||
message.id,
|
messageGenerationSubscriber.current &&
|
||||||
message.thread_id,
|
message.thread_id === activeThreadRef.current?.id &&
|
||||||
message.content,
|
!messageGenerationSubscriber.current!.thread_id
|
||||||
message.status
|
) {
|
||||||
)
|
updateMessage(
|
||||||
|
message.id,
|
||||||
|
message.thread_id,
|
||||||
|
message.content,
|
||||||
|
message.status
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (message.status === MessageStatus.Pending) {
|
if (message.status === MessageStatus.Pending) {
|
||||||
if (message.content.length) {
|
if (message.content.length) {
|
||||||
setIsGeneratingResponse(false)
|
setIsGeneratingResponse(false)
|
||||||
@ -244,6 +267,7 @@ export default function ModelHandler() {
|
|||||||
const metadata = {
|
const metadata = {
|
||||||
...thread.metadata,
|
...thread.metadata,
|
||||||
...(messageContent && { lastMessage: messageContent }),
|
...(messageContent && { lastMessage: messageContent }),
|
||||||
|
updated_at: Date.now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
updateThread({
|
updateThread({
|
||||||
@ -302,15 +326,10 @@ export default function ModelHandler() {
|
|||||||
|
|
||||||
const generateThreadTitle = (message: ThreadMessage, thread: Thread) => {
|
const generateThreadTitle = (message: ThreadMessage, thread: Thread) => {
|
||||||
// If this is the first ever prompt in the thread
|
// If this is the first ever prompt in the thread
|
||||||
if (
|
if ((thread.title ?? thread.metadata?.title)?.trim() !== defaultThreadTitle)
|
||||||
(thread.title ?? thread.metadata?.title)?.trim() !== defaultThreadTitle
|
|
||||||
) {
|
|
||||||
return
|
return
|
||||||
}
|
|
||||||
|
|
||||||
if (!activeModelRef.current) {
|
if (!activeModelRef.current) return
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check model engine; we don't want to generate a title when it's not a local engine. remote model using first promp
|
// Check model engine; we don't want to generate a title when it's not a local engine. remote model using first promp
|
||||||
if (!isLocalEngine(activeModelRef.current?.engine as InferenceEngine)) {
|
if (!isLocalEngine(activeModelRef.current?.engine as InferenceEngine)) {
|
||||||
@ -332,6 +351,7 @@ export default function ModelHandler() {
|
|||||||
...updatedThread,
|
...updatedThread,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
.catch(console.error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is the first time message comes in on a new thread
|
// This is the first time message comes in on a new thread
|
||||||
|
|||||||
@ -35,6 +35,13 @@ export const chatMessages = atom(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store subscribed generating message thread
|
||||||
|
*/
|
||||||
|
export const subscribedGeneratingMessageAtom = atom<{
|
||||||
|
thread_id?: string
|
||||||
|
}>({})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores the status of the messages load for each thread
|
* Stores the status of the messages load for each thread
|
||||||
*/
|
*/
|
||||||
@ -175,6 +182,17 @@ export const updateMessageAtom = atom(
|
|||||||
// Update thread last message
|
// Update thread last message
|
||||||
if (text.length)
|
if (text.length)
|
||||||
set(updateThreadStateLastMessageAtom, conversationId, text)
|
set(updateThreadStateLastMessageAtom, conversationId, text)
|
||||||
|
} else {
|
||||||
|
set(addNewMessageAtom, {
|
||||||
|
id,
|
||||||
|
thread_id: conversationId,
|
||||||
|
content: text,
|
||||||
|
status,
|
||||||
|
role: ChatCompletionRole.Assistant,
|
||||||
|
created_at: Date.now() / 1000,
|
||||||
|
completed_at: Date.now() / 1000,
|
||||||
|
object: 'thread.message',
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@ -20,12 +20,7 @@ export const CHAT_WIDTH = 'chatWidth'
|
|||||||
export const themesOptionsAtom = atomWithStorage<
|
export const themesOptionsAtom = atomWithStorage<
|
||||||
{ name: string; value: string }[]
|
{ name: string; value: string }[]
|
||||||
>(THEME_OPTIONS, [], undefined, { getOnInit: true })
|
>(THEME_OPTIONS, [], undefined, { getOnInit: true })
|
||||||
export const janThemesPathAtom = atomWithStorage<string | undefined>(
|
|
||||||
THEME_PATH,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
{ getOnInit: true }
|
|
||||||
)
|
|
||||||
export const selectedThemeIdAtom = atomWithStorage<string>(
|
export const selectedThemeIdAtom = atomWithStorage<string>(
|
||||||
THEME,
|
THEME,
|
||||||
'',
|
'',
|
||||||
|
|||||||
@ -125,6 +125,26 @@ export const waitingToSendMessage = atom<boolean | undefined>(undefined)
|
|||||||
*/
|
*/
|
||||||
export const isGeneratingResponseAtom = atom<boolean | undefined>(undefined)
|
export const isGeneratingResponseAtom = atom<boolean | undefined>(undefined)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new thread and add it to the thread list
|
||||||
|
*/
|
||||||
|
export const createNewThreadAtom = atom(null, (get, set, newThread: Thread) => {
|
||||||
|
// create thread state for this new thread
|
||||||
|
const currentState = { ...get(threadStatesAtom) }
|
||||||
|
|
||||||
|
const threadState: ThreadState = {
|
||||||
|
hasMore: false,
|
||||||
|
waitingForResponse: false,
|
||||||
|
lastMessage: undefined,
|
||||||
|
}
|
||||||
|
currentState[newThread.id] = threadState
|
||||||
|
set(threadStatesAtom, currentState)
|
||||||
|
|
||||||
|
// add the new thread on top of the thread list to the state
|
||||||
|
const threads = get(threadsAtom)
|
||||||
|
set(threadsAtom, [newThread, ...threads])
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove a thread state from the atom
|
* Remove a thread state from the atom
|
||||||
*/
|
*/
|
||||||
@ -180,12 +200,12 @@ export const updateThreadAtom = atom(
|
|||||||
)
|
)
|
||||||
|
|
||||||
// sort new threads based on updated at
|
// sort new threads based on updated at
|
||||||
threads.sort((thread1, thread2) => {
|
threads.sort((a, b) => {
|
||||||
const aDate = new Date(thread1.updated ?? 0)
|
return ((a.metadata?.updated_at as number) ?? 0) >
|
||||||
const bDate = new Date(thread2.updated ?? 0)
|
((b.metadata?.updated_at as number) ?? 0)
|
||||||
return bDate.getTime() - aDate.getTime()
|
? -1
|
||||||
|
: 1
|
||||||
})
|
})
|
||||||
|
|
||||||
set(threadsAtom, threads)
|
set(threadsAtom, threads)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@ -33,29 +33,12 @@ import { activeAssistantAtom } from '@/helpers/atoms/Assistant.atom'
|
|||||||
import { selectedModelAtom } from '@/helpers/atoms/Model.atom'
|
import { selectedModelAtom } from '@/helpers/atoms/Model.atom'
|
||||||
import {
|
import {
|
||||||
threadsAtom,
|
threadsAtom,
|
||||||
threadStatesAtom,
|
|
||||||
updateThreadAtom,
|
updateThreadAtom,
|
||||||
setThreadModelParamsAtom,
|
setThreadModelParamsAtom,
|
||||||
isGeneratingResponseAtom,
|
isGeneratingResponseAtom,
|
||||||
|
createNewThreadAtom,
|
||||||
} from '@/helpers/atoms/Thread.atom'
|
} from '@/helpers/atoms/Thread.atom'
|
||||||
|
|
||||||
const createNewThreadAtom = atom(null, (get, set, newThread: Thread) => {
|
|
||||||
// create thread state for this new thread
|
|
||||||
const currentState = { ...get(threadStatesAtom) }
|
|
||||||
|
|
||||||
const threadState: ThreadState = {
|
|
||||||
hasMore: false,
|
|
||||||
waitingForResponse: false,
|
|
||||||
lastMessage: undefined,
|
|
||||||
}
|
|
||||||
currentState[newThread.id] = threadState
|
|
||||||
set(threadStatesAtom, currentState)
|
|
||||||
|
|
||||||
// add the new thread on top of the thread list to the state
|
|
||||||
const threads = get(threadsAtom)
|
|
||||||
set(threadsAtom, [newThread, ...threads])
|
|
||||||
})
|
|
||||||
|
|
||||||
export const useCreateNewThread = () => {
|
export const useCreateNewThread = () => {
|
||||||
const createNewThread = useSetAtom(createNewThreadAtom)
|
const createNewThread = useSetAtom(createNewThreadAtom)
|
||||||
const { setActiveThread } = useSetActiveThread()
|
const { setActiveThread } = useSetActiveThread()
|
||||||
@ -153,6 +136,7 @@ export const useCreateNewThread = () => {
|
|||||||
updated: createdAt,
|
updated: createdAt,
|
||||||
metadata: {
|
metadata: {
|
||||||
title: 'New Thread',
|
title: 'New Thread',
|
||||||
|
updated_at: Date.now(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -207,9 +191,11 @@ export const useCreateNewThread = () => {
|
|||||||
async (thread: Thread) => {
|
async (thread: Thread) => {
|
||||||
updateThread(thread)
|
updateThread(thread)
|
||||||
|
|
||||||
setActiveAssistant(thread.assistants[0])
|
|
||||||
updateThreadCallback(thread)
|
updateThreadCallback(thread)
|
||||||
updateAssistantCallback(thread.id, thread.assistants[0])
|
if (thread.assistants && thread.assistants?.length > 0) {
|
||||||
|
setActiveAssistant(thread.assistants[0])
|
||||||
|
updateAssistantCallback(thread.id, thread.assistants[0])
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
updateThread,
|
updateThread,
|
||||||
|
|||||||
@ -55,17 +55,21 @@ describe('useDeleteThread', () => {
|
|||||||
const mockCleanMessages = jest.fn()
|
const mockCleanMessages = jest.fn()
|
||||||
;(useSetAtom as jest.Mock).mockReturnValue(() => mockCleanMessages)
|
;(useSetAtom as jest.Mock).mockReturnValue(() => mockCleanMessages)
|
||||||
;(useAtomValue as jest.Mock).mockReturnValue(['thread 1'])
|
;(useAtomValue as jest.Mock).mockReturnValue(['thread 1'])
|
||||||
const mockCreateNewThread = jest.fn()
|
|
||||||
;(useCreateNewThread as jest.Mock).mockReturnValue({
|
|
||||||
requestCreateNewThread: mockCreateNewThread,
|
|
||||||
})
|
|
||||||
|
|
||||||
const mockSaveThread = jest.fn()
|
const mockSaveThread = jest.fn()
|
||||||
const mockDeleteThread = jest.fn().mockResolvedValue({})
|
const mockDeleteMessage = jest.fn().mockResolvedValue({})
|
||||||
|
const mockModifyThread = jest.fn().mockResolvedValue({})
|
||||||
extensionManager.get = jest.fn().mockReturnValue({
|
extensionManager.get = jest.fn().mockReturnValue({
|
||||||
saveThread: mockSaveThread,
|
saveThread: mockSaveThread,
|
||||||
getThreadAssistant: jest.fn().mockResolvedValue({}),
|
getThreadAssistant: jest.fn().mockResolvedValue({}),
|
||||||
deleteThread: mockDeleteThread,
|
listMessages: jest.fn().mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: 'message1',
|
||||||
|
text: 'Message 1',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
deleteMessage: mockDeleteMessage,
|
||||||
|
modifyThread: mockModifyThread,
|
||||||
})
|
})
|
||||||
|
|
||||||
const { result } = renderHook(() => useDeleteThread())
|
const { result } = renderHook(() => useDeleteThread())
|
||||||
@ -74,8 +78,8 @@ describe('useDeleteThread', () => {
|
|||||||
await result.current.cleanThread('thread1')
|
await result.current.cleanThread('thread1')
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(mockDeleteThread).toHaveBeenCalled()
|
expect(mockDeleteMessage).toHaveBeenCalled()
|
||||||
expect(mockCreateNewThread).toHaveBeenCalled()
|
expect(mockModifyThread).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle errors when deleting a thread', async () => {
|
it('should handle errors when deleting a thread', async () => {
|
||||||
|
|||||||
@ -2,69 +2,68 @@ import { useCallback } from 'react'
|
|||||||
|
|
||||||
import { ExtensionTypeEnum, ConversationalExtension } from '@janhq/core'
|
import { ExtensionTypeEnum, ConversationalExtension } from '@janhq/core'
|
||||||
|
|
||||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
|
import { useAtom, useSetAtom } from 'jotai'
|
||||||
|
|
||||||
import { currentPromptAtom } from '@/containers/Providers/Jotai'
|
import { currentPromptAtom } from '@/containers/Providers/Jotai'
|
||||||
|
|
||||||
import { toaster } from '@/containers/Toast'
|
import { toaster } from '@/containers/Toast'
|
||||||
|
|
||||||
import { useCreateNewThread } from './useCreateNewThread'
|
import useSetActiveThread from './useSetActiveThread'
|
||||||
|
|
||||||
import { extensionManager } from '@/extension/ExtensionManager'
|
import { extensionManager } from '@/extension/ExtensionManager'
|
||||||
|
|
||||||
import { assistantsAtom } from '@/helpers/atoms/Assistant.atom'
|
|
||||||
import { deleteChatMessageAtom as deleteChatMessagesAtom } from '@/helpers/atoms/ChatMessage.atom'
|
import { deleteChatMessageAtom as deleteChatMessagesAtom } from '@/helpers/atoms/ChatMessage.atom'
|
||||||
import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom'
|
|
||||||
import {
|
import {
|
||||||
threadsAtom,
|
threadsAtom,
|
||||||
setActiveThreadIdAtom,
|
setActiveThreadIdAtom,
|
||||||
deleteThreadStateAtom,
|
deleteThreadStateAtom,
|
||||||
|
updateThreadAtom,
|
||||||
} from '@/helpers/atoms/Thread.atom'
|
} from '@/helpers/atoms/Thread.atom'
|
||||||
|
|
||||||
export default function useDeleteThread() {
|
export default function useDeleteThread() {
|
||||||
const [threads, setThreads] = useAtom(threadsAtom)
|
const [threads, setThreads] = useAtom(threadsAtom)
|
||||||
const { requestCreateNewThread } = useCreateNewThread()
|
const updateThread = useSetAtom(updateThreadAtom)
|
||||||
const assistants = useAtomValue(assistantsAtom)
|
|
||||||
const models = useAtomValue(downloadedModelsAtom)
|
|
||||||
|
|
||||||
const setCurrentPrompt = useSetAtom(currentPromptAtom)
|
const setCurrentPrompt = useSetAtom(currentPromptAtom)
|
||||||
const setActiveThreadId = useSetAtom(setActiveThreadIdAtom)
|
const setActiveThreadId = useSetAtom(setActiveThreadIdAtom)
|
||||||
const deleteMessages = useSetAtom(deleteChatMessagesAtom)
|
const deleteMessages = useSetAtom(deleteChatMessagesAtom)
|
||||||
|
|
||||||
const deleteThreadState = useSetAtom(deleteThreadStateAtom)
|
const deleteThreadState = useSetAtom(deleteThreadStateAtom)
|
||||||
|
const { setActiveThread } = useSetActiveThread()
|
||||||
|
|
||||||
const cleanThread = useCallback(
|
const cleanThread = useCallback(
|
||||||
async (threadId: string) => {
|
async (threadId: string) => {
|
||||||
const thread = threads.find((c) => c.id === threadId)
|
const messages = await extensionManager
|
||||||
if (!thread) return
|
|
||||||
const assistantInfo = await extensionManager
|
|
||||||
.get<ConversationalExtension>(ExtensionTypeEnum.Conversational)
|
.get<ConversationalExtension>(ExtensionTypeEnum.Conversational)
|
||||||
?.getThreadAssistant(thread.id)
|
?.listMessages(threadId)
|
||||||
|
|
||||||
if (!assistantInfo) return
|
|
||||||
const model = models.find((c) => c.id === assistantInfo?.model?.id)
|
|
||||||
|
|
||||||
requestCreateNewThread(
|
|
||||||
{
|
|
||||||
...assistantInfo,
|
|
||||||
id: assistants[0].id,
|
|
||||||
name: assistants[0].name,
|
|
||||||
},
|
|
||||||
model
|
|
||||||
? {
|
|
||||||
...model,
|
|
||||||
parameters: assistantInfo?.model?.parameters ?? {},
|
|
||||||
settings: assistantInfo?.model?.settings ?? {},
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
)
|
|
||||||
// Delete this thread
|
|
||||||
await extensionManager
|
|
||||||
.get<ConversationalExtension>(ExtensionTypeEnum.Conversational)
|
|
||||||
?.deleteThread(threadId)
|
|
||||||
.catch(console.error)
|
.catch(console.error)
|
||||||
|
if (messages) {
|
||||||
|
messages.forEach((message) => {
|
||||||
|
extensionManager
|
||||||
|
.get<ConversationalExtension>(ExtensionTypeEnum.Conversational)
|
||||||
|
?.deleteMessage(threadId, message.id)
|
||||||
|
.catch(console.error)
|
||||||
|
})
|
||||||
|
const thread = threads.find((e) => e.id === threadId)
|
||||||
|
if (thread) {
|
||||||
|
const updatedThread = {
|
||||||
|
...thread,
|
||||||
|
metadata: {
|
||||||
|
...thread.metadata,
|
||||||
|
title: 'New Thread',
|
||||||
|
lastMessage: '',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
extensionManager
|
||||||
|
.get<ConversationalExtension>(ExtensionTypeEnum.Conversational)
|
||||||
|
?.modifyThread(updatedThread)
|
||||||
|
.catch(console.error)
|
||||||
|
updateThread(updatedThread)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
deleteMessages(threadId)
|
||||||
},
|
},
|
||||||
[assistants, models, requestCreateNewThread, threads]
|
[deleteMessages, threads, updateThread]
|
||||||
)
|
)
|
||||||
|
|
||||||
const deleteThread = async (threadId: string) => {
|
const deleteThread = async (threadId: string) => {
|
||||||
@ -90,7 +89,7 @@ export default function useDeleteThread() {
|
|||||||
type: 'success',
|
type: 'success',
|
||||||
})
|
})
|
||||||
if (availableThreads.length > 0) {
|
if (availableThreads.length > 0) {
|
||||||
setActiveThreadId(availableThreads[0].id)
|
setActiveThread(availableThreads[0])
|
||||||
} else {
|
} else {
|
||||||
setActiveThreadId(undefined)
|
setActiveThreadId(undefined)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,6 @@ import cssVars from '@/utils/jsonToCssVariables'
|
|||||||
|
|
||||||
import { janDataFolderPathAtom } from '@/helpers/atoms/AppConfig.atom'
|
import { janDataFolderPathAtom } from '@/helpers/atoms/AppConfig.atom'
|
||||||
import {
|
import {
|
||||||
janThemesPathAtom,
|
|
||||||
selectedThemeIdAtom,
|
selectedThemeIdAtom,
|
||||||
themeDataAtom,
|
themeDataAtom,
|
||||||
themesOptionsAtom,
|
themesOptionsAtom,
|
||||||
@ -21,7 +20,6 @@ type NativeThemeProps = 'light' | 'dark'
|
|||||||
export const useLoadTheme = () => {
|
export const useLoadTheme = () => {
|
||||||
const janDataFolderPath = useAtomValue(janDataFolderPathAtom)
|
const janDataFolderPath = useAtomValue(janDataFolderPathAtom)
|
||||||
const [themeOptions, setThemeOptions] = useAtom(themesOptionsAtom)
|
const [themeOptions, setThemeOptions] = useAtom(themesOptionsAtom)
|
||||||
const [themePath, setThemePath] = useAtom(janThemesPathAtom)
|
|
||||||
const [themeData, setThemeData] = useAtom(themeDataAtom)
|
const [themeData, setThemeData] = useAtom(themeDataAtom)
|
||||||
const [selectedIdTheme, setSelectedIdTheme] = useAtom(selectedThemeIdAtom)
|
const [selectedIdTheme, setSelectedIdTheme] = useAtom(selectedThemeIdAtom)
|
||||||
const { setTheme } = useTheme()
|
const { setTheme } = useTheme()
|
||||||
@ -41,6 +39,14 @@ export const useLoadTheme = () => {
|
|||||||
[setTheme]
|
[setTheme]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const applyTheme = (theme: Theme) => {
|
||||||
|
const variables = cssVars(theme.variables)
|
||||||
|
const headTag = document.getElementsByTagName('head')[0]
|
||||||
|
const styleTag = document.createElement('style')
|
||||||
|
styleTag.innerHTML = `:root {${variables}}`
|
||||||
|
headTag.appendChild(styleTag)
|
||||||
|
}
|
||||||
|
|
||||||
const getThemes = useCallback(async () => {
|
const getThemes = useCallback(async () => {
|
||||||
if (!janDataFolderPath.length) return
|
if (!janDataFolderPath.length) return
|
||||||
const folderPath = await joinPath([janDataFolderPath, 'themes'])
|
const folderPath = await joinPath([janDataFolderPath, 'themes'])
|
||||||
@ -59,7 +65,6 @@ export const useLoadTheme = () => {
|
|||||||
|
|
||||||
if (janDataFolderPath.length > 0) {
|
if (janDataFolderPath.length > 0) {
|
||||||
if (!selectedIdTheme.length) return setSelectedIdTheme('joi-light')
|
if (!selectedIdTheme.length) return setSelectedIdTheme('joi-light')
|
||||||
setThemePath(folderPath)
|
|
||||||
const filePath = await joinPath([
|
const filePath = await joinPath([
|
||||||
`${folderPath}/${selectedIdTheme}`,
|
`${folderPath}/${selectedIdTheme}`,
|
||||||
`theme.json`,
|
`theme.json`,
|
||||||
@ -68,11 +73,7 @@ export const useLoadTheme = () => {
|
|||||||
|
|
||||||
setThemeData(theme)
|
setThemeData(theme)
|
||||||
setNativeTheme(theme.nativeTheme)
|
setNativeTheme(theme.nativeTheme)
|
||||||
const variables = cssVars(theme.variables)
|
applyTheme(theme)
|
||||||
const headTag = document.getElementsByTagName('head')[0]
|
|
||||||
const styleTag = document.createElement('style')
|
|
||||||
styleTag.innerHTML = `:root {${variables}}`
|
|
||||||
headTag.appendChild(styleTag)
|
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
janDataFolderPath,
|
janDataFolderPath,
|
||||||
@ -81,26 +82,21 @@ export const useLoadTheme = () => {
|
|||||||
setSelectedIdTheme,
|
setSelectedIdTheme,
|
||||||
setThemeData,
|
setThemeData,
|
||||||
setThemeOptions,
|
setThemeOptions,
|
||||||
setThemePath,
|
|
||||||
])
|
])
|
||||||
|
|
||||||
const applyTheme = useCallback(async () => {
|
const configureTheme = useCallback(async () => {
|
||||||
if (!themeData || !themeOptions || !themePath) {
|
if (!themeData || !themeOptions) {
|
||||||
await getThemes()
|
await getThemes()
|
||||||
} else {
|
} else {
|
||||||
const variables = cssVars(themeData.variables)
|
applyTheme(themeData)
|
||||||
const headTag = document.getElementsByTagName('head')[0]
|
|
||||||
const styleTag = document.createElement('style')
|
|
||||||
styleTag.innerHTML = `:root {${variables}}`
|
|
||||||
headTag.appendChild(styleTag)
|
|
||||||
}
|
}
|
||||||
setNativeTheme(themeData?.nativeTheme as NativeThemeProps)
|
setNativeTheme(themeData?.nativeTheme as NativeThemeProps)
|
||||||
}, [themeData, themeOptions, themePath, getThemes])
|
}, [themeData, themeOptions, getThemes, setNativeTheme])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
applyTheme()
|
configureTheme()
|
||||||
}, [
|
}, [
|
||||||
applyTheme,
|
configureTheme,
|
||||||
selectedIdTheme,
|
selectedIdTheme,
|
||||||
setNativeTheme,
|
setNativeTheme,
|
||||||
setSelectedIdTheme,
|
setSelectedIdTheme,
|
||||||
|
|||||||
@ -1,11 +1,15 @@
|
|||||||
import { ExtensionTypeEnum, Thread, ConversationalExtension } from '@janhq/core'
|
import { ExtensionTypeEnum, Thread, ConversationalExtension } from '@janhq/core'
|
||||||
|
|
||||||
import { useSetAtom } from 'jotai'
|
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
|
||||||
|
|
||||||
import { extensionManager } from '@/extension'
|
import { extensionManager } from '@/extension'
|
||||||
import { activeAssistantAtom } from '@/helpers/atoms/Assistant.atom'
|
import { activeAssistantAtom } from '@/helpers/atoms/Assistant.atom'
|
||||||
import { setConvoMessagesAtom } from '@/helpers/atoms/ChatMessage.atom'
|
|
||||||
import {
|
import {
|
||||||
|
setConvoMessagesAtom,
|
||||||
|
subscribedGeneratingMessageAtom,
|
||||||
|
} from '@/helpers/atoms/ChatMessage.atom'
|
||||||
|
import {
|
||||||
|
getActiveThreadIdAtom,
|
||||||
setActiveThreadIdAtom,
|
setActiveThreadIdAtom,
|
||||||
setThreadModelParamsAtom,
|
setThreadModelParamsAtom,
|
||||||
} from '@/helpers/atoms/Thread.atom'
|
} from '@/helpers/atoms/Thread.atom'
|
||||||
@ -13,14 +17,18 @@ import { ModelParams } from '@/types/model'
|
|||||||
|
|
||||||
export default function useSetActiveThread() {
|
export default function useSetActiveThread() {
|
||||||
const setActiveThreadId = useSetAtom(setActiveThreadIdAtom)
|
const setActiveThreadId = useSetAtom(setActiveThreadIdAtom)
|
||||||
const setThreadMessage = useSetAtom(setConvoMessagesAtom)
|
const activeThreadId = useAtomValue(getActiveThreadIdAtom)
|
||||||
|
const setThreadMessages = useSetAtom(setConvoMessagesAtom)
|
||||||
const setThreadModelParams = useSetAtom(setThreadModelParamsAtom)
|
const setThreadModelParams = useSetAtom(setThreadModelParamsAtom)
|
||||||
const setActiveAssistant = useSetAtom(activeAssistantAtom)
|
const setActiveAssistant = useSetAtom(activeAssistantAtom)
|
||||||
|
const [messageSubscriber, setMessageSubscriber] = useAtom(
|
||||||
|
subscribedGeneratingMessageAtom
|
||||||
|
)
|
||||||
|
|
||||||
const setActiveThread = async (thread: Thread) => {
|
const setActiveThread = async (thread: Thread) => {
|
||||||
if (!thread?.id) return
|
if (!thread?.id || activeThreadId === thread.id) return
|
||||||
|
|
||||||
setActiveThreadId(thread?.id)
|
setActiveThreadId(thread.id)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const assistantInfo = await getThreadAssistant(thread.id)
|
const assistantInfo = await getThreadAssistant(thread.id)
|
||||||
@ -32,7 +40,8 @@ export default function useSetActiveThread() {
|
|||||||
...assistantInfo?.model?.settings,
|
...assistantInfo?.model?.settings,
|
||||||
}
|
}
|
||||||
setThreadModelParams(thread?.id, modelParams)
|
setThreadModelParams(thread?.id, modelParams)
|
||||||
setThreadMessage(thread.id, messages)
|
setThreadMessages(thread.id, messages)
|
||||||
|
if (messageSubscriber.thread_id !== thread.id) setMessageSubscriber({})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,7 +26,12 @@ const useThreads = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const getThreads = async () => {
|
const getThreads = async () => {
|
||||||
const localThreads = await getLocalThreads()
|
const localThreads = (await getLocalThreads()).sort((a, b) => {
|
||||||
|
return ((a.metadata?.updated_at as number) ?? 0) >
|
||||||
|
((b.metadata?.updated_at as number) ?? 0)
|
||||||
|
? -1
|
||||||
|
: 1
|
||||||
|
})
|
||||||
const localThreadStates: Record<string, ThreadState> = {}
|
const localThreadStates: Record<string, ThreadState> = {}
|
||||||
const threadModelParams: Record<string, ModelParams> = {}
|
const threadModelParams: Record<string, ModelParams> = {}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@janhq/web",
|
"name": "@janhq/web",
|
||||||
"version": "0.5.11",
|
"version": "0.5.12",
|
||||||
"private": true,
|
"private": true,
|
||||||
"homepage": "./",
|
"homepage": "./",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@ -176,7 +176,7 @@ const LocalServerLeftPanel = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative z-50 mt-2 block">
|
<div className="relative mt-2 block">
|
||||||
<Input
|
<Input
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
errorRangePort && 'border-[hsla(var(--destructive-bg))]'
|
errorRangePort && 'border-[hsla(var(--destructive-bg))]'
|
||||||
|
|||||||
@ -8,9 +8,10 @@ import { useAtom, useAtomValue } from 'jotai'
|
|||||||
|
|
||||||
import { twMerge } from 'tailwind-merge'
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
|
import { janDataFolderPathAtom } from '@/helpers/atoms/AppConfig.atom'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
chatWidthAtom,
|
chatWidthAtom,
|
||||||
janThemesPathAtom,
|
|
||||||
reduceTransparentAtom,
|
reduceTransparentAtom,
|
||||||
selectedThemeIdAtom,
|
selectedThemeIdAtom,
|
||||||
spellCheckAtom,
|
spellCheckAtom,
|
||||||
@ -21,8 +22,8 @@ import {
|
|||||||
export default function AppearanceOptions() {
|
export default function AppearanceOptions() {
|
||||||
const [selectedIdTheme, setSelectedIdTheme] = useAtom(selectedThemeIdAtom)
|
const [selectedIdTheme, setSelectedIdTheme] = useAtom(selectedThemeIdAtom)
|
||||||
const themeOptions = useAtomValue(themesOptionsAtom)
|
const themeOptions = useAtomValue(themesOptionsAtom)
|
||||||
|
const janDataFolderPath = useAtomValue(janDataFolderPathAtom)
|
||||||
const { setTheme, theme } = useTheme()
|
const { setTheme, theme } = useTheme()
|
||||||
const janThemesPath = useAtomValue(janThemesPathAtom)
|
|
||||||
const [themeData, setThemeData] = useAtom(themeDataAtom)
|
const [themeData, setThemeData] = useAtom(themeDataAtom)
|
||||||
const [reduceTransparent, setReduceTransparent] = useAtom(
|
const [reduceTransparent, setReduceTransparent] = useAtom(
|
||||||
reduceTransparentAtom
|
reduceTransparentAtom
|
||||||
@ -48,6 +49,7 @@ export default function AppearanceOptions() {
|
|||||||
const handleClickTheme = useCallback(
|
const handleClickTheme = useCallback(
|
||||||
async (e: string) => {
|
async (e: string) => {
|
||||||
setSelectedIdTheme(e)
|
setSelectedIdTheme(e)
|
||||||
|
const janThemesPath = await joinPath([janDataFolderPath, 'themes'])
|
||||||
const filePath = await joinPath([`${janThemesPath}/${e}`, `theme.json`])
|
const filePath = await joinPath([`${janThemesPath}/${e}`, `theme.json`])
|
||||||
const theme: Theme = JSON.parse(await fs.readFileSync(filePath, 'utf-8'))
|
const theme: Theme = JSON.parse(await fs.readFileSync(filePath, 'utf-8'))
|
||||||
setThemeData(theme)
|
setThemeData(theme)
|
||||||
@ -59,7 +61,7 @@ export default function AppearanceOptions() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
janThemesPath,
|
janDataFolderPath,
|
||||||
reduceTransparent,
|
reduceTransparent,
|
||||||
setReduceTransparent,
|
setReduceTransparent,
|
||||||
setSelectedIdTheme,
|
setSelectedIdTheme,
|
||||||
|
|||||||
@ -23,9 +23,7 @@ const EmptyThread = () => {
|
|||||||
<LogoMark className="mx-auto mb-2 animate-wave" width={32} height={32} />
|
<LogoMark className="mx-auto mb-2 animate-wave" width={32} height={32} />
|
||||||
{showOnboardingStep ? (
|
{showOnboardingStep ? (
|
||||||
<>
|
<>
|
||||||
<p className="mt-1 font-medium">
|
<p className="mt-1 font-medium">{`You don't have any model`}</p>
|
||||||
{`You don't have a local model yet.`}
|
|
||||||
</p>
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setMainViewState(MainViewState.Hub)}
|
onClick={() => setMainViewState(MainViewState.Hub)}
|
||||||
variant="soft"
|
variant="soft"
|
||||||
|
|||||||
@ -14,7 +14,11 @@ import LoadModelError from '../LoadModelError'
|
|||||||
import EmptyThread from './EmptyThread'
|
import EmptyThread from './EmptyThread'
|
||||||
|
|
||||||
import { getCurrentChatMessagesAtom } from '@/helpers/atoms/ChatMessage.atom'
|
import { getCurrentChatMessagesAtom } from '@/helpers/atoms/ChatMessage.atom'
|
||||||
import { activeThreadAtom } from '@/helpers/atoms/Thread.atom'
|
import {
|
||||||
|
activeThreadAtom,
|
||||||
|
isGeneratingResponseAtom,
|
||||||
|
threadStatesAtom,
|
||||||
|
} from '@/helpers/atoms/Thread.atom'
|
||||||
|
|
||||||
const ChatConfigurator = memo(() => {
|
const ChatConfigurator = memo(() => {
|
||||||
const messages = useAtomValue(getCurrentChatMessagesAtom)
|
const messages = useAtomValue(getCurrentChatMessagesAtom)
|
||||||
@ -61,6 +65,12 @@ const ChatBody = memo(
|
|||||||
const prevScrollTop = useRef(0)
|
const prevScrollTop = useRef(0)
|
||||||
const isUserManuallyScrollingUp = useRef(false)
|
const isUserManuallyScrollingUp = useRef(false)
|
||||||
const currentThread = useAtomValue(activeThreadAtom)
|
const currentThread = useAtomValue(activeThreadAtom)
|
||||||
|
const threadStates = useAtomValue(threadStatesAtom)
|
||||||
|
const isGeneratingResponse = useAtomValue(isGeneratingResponseAtom)
|
||||||
|
|
||||||
|
const isStreamingResponse = Object.values(threadStates).some(
|
||||||
|
(threadState) => threadState.waitingForResponse
|
||||||
|
)
|
||||||
|
|
||||||
const count = useMemo(
|
const count = useMemo(
|
||||||
() => (messages?.length ?? 0) + (loadModelError ? 1 : 0),
|
() => (messages?.length ?? 0) + (loadModelError ? 1 : 0),
|
||||||
@ -76,14 +86,31 @@ const ChatBody = memo(
|
|||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Delay the scroll until the DOM is updated
|
|
||||||
if (parentRef.current) {
|
if (parentRef.current) {
|
||||||
requestAnimationFrame(() => {
|
parentRef.current.scrollTo({ top: parentRef.current.scrollHeight })
|
||||||
if (parentRef.current) {
|
virtualizer.scrollToIndex(count - 1)
|
||||||
parentRef.current.scrollTo({ top: parentRef.current.scrollHeight })
|
}
|
||||||
virtualizer.scrollToIndex(count - 1)
|
}, [count, virtualizer])
|
||||||
}
|
|
||||||
})
|
useEffect(() => {
|
||||||
|
if (parentRef.current && isGeneratingResponse) {
|
||||||
|
parentRef.current.scrollTo({ top: parentRef.current.scrollHeight })
|
||||||
|
virtualizer.scrollToIndex(count - 1)
|
||||||
|
}
|
||||||
|
}, [count, virtualizer, isGeneratingResponse])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (parentRef.current && isGeneratingResponse) {
|
||||||
|
parentRef.current.scrollTo({ top: parentRef.current.scrollHeight })
|
||||||
|
virtualizer.scrollToIndex(count - 1)
|
||||||
|
}
|
||||||
|
}, [count, virtualizer, isGeneratingResponse, currentThread?.id])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
isUserManuallyScrollingUp.current = false
|
||||||
|
if (parentRef.current) {
|
||||||
|
parentRef.current.scrollTo({ top: parentRef.current.scrollHeight })
|
||||||
|
virtualizer.scrollToIndex(count - 1)
|
||||||
}
|
}
|
||||||
}, [count, currentThread?.id, virtualizer])
|
}, [count, currentThread?.id, virtualizer])
|
||||||
|
|
||||||
@ -94,34 +121,38 @@ const ChatBody = memo(
|
|||||||
_,
|
_,
|
||||||
instance
|
instance
|
||||||
) => {
|
) => {
|
||||||
if (isUserManuallyScrollingUp.current === true) return false
|
if (isUserManuallyScrollingUp.current === true && isStreamingResponse)
|
||||||
|
return false
|
||||||
return (
|
return (
|
||||||
// item.start < (instance.scrollOffset ?? 0) &&
|
// item.start < (instance.scrollOffset ?? 0) &&
|
||||||
instance.scrollDirection !== 'backward'
|
instance.scrollDirection !== 'backward'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleScroll = useCallback((event: React.UIEvent<HTMLElement>) => {
|
const handleScroll = useCallback(
|
||||||
const currentScrollTop = event.currentTarget.scrollTop
|
(event: React.UIEvent<HTMLElement>) => {
|
||||||
|
|
||||||
if (prevScrollTop.current > currentScrollTop) {
|
|
||||||
isUserManuallyScrollingUp.current = true
|
|
||||||
} else {
|
|
||||||
const currentScrollTop = event.currentTarget.scrollTop
|
const currentScrollTop = event.currentTarget.scrollTop
|
||||||
const scrollHeight = event.currentTarget.scrollHeight
|
|
||||||
const clientHeight = event.currentTarget.clientHeight
|
|
||||||
|
|
||||||
if (currentScrollTop + clientHeight >= scrollHeight) {
|
if (prevScrollTop.current > currentScrollTop && isStreamingResponse) {
|
||||||
isUserManuallyScrollingUp.current = false
|
isUserManuallyScrollingUp.current = true
|
||||||
|
} else {
|
||||||
|
const currentScrollTop = event.currentTarget.scrollTop
|
||||||
|
const scrollHeight = event.currentTarget.scrollHeight
|
||||||
|
const clientHeight = event.currentTarget.clientHeight
|
||||||
|
|
||||||
|
if (currentScrollTop + clientHeight >= scrollHeight) {
|
||||||
|
isUserManuallyScrollingUp.current = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (isUserManuallyScrollingUp.current === true) {
|
if (isUserManuallyScrollingUp.current === true) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
}
|
}
|
||||||
prevScrollTop.current = currentScrollTop
|
prevScrollTop.current = currentScrollTop
|
||||||
}, [])
|
},
|
||||||
|
[isStreamingResponse]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col overflow-x-hidden">
|
<div className="flex h-full w-full flex-col overflow-x-hidden">
|
||||||
|
|||||||
@ -231,30 +231,24 @@ const ChatInput = () => {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<ul>
|
<ul>
|
||||||
<Tooltip
|
<li
|
||||||
trigger={
|
className={twMerge(
|
||||||
<li
|
'text-[hsla(var(--text-secondary)] hover:bg-secondary flex w-full items-center space-x-2 px-4 py-2 hover:bg-[hsla(var(--dropdown-menu-hover-bg))]',
|
||||||
className={twMerge(
|
activeAssistant?.model.settings?.vision_model &&
|
||||||
'text-[hsla(var(--text-secondary)] hover:bg-secondary flex w-full items-center space-x-2 px-4 py-2 hover:bg-[hsla(var(--dropdown-menu-hover-bg))]',
|
isModelSupportRagAndTools
|
||||||
activeAssistant?.model.settings?.vision_model ||
|
? 'cursor-pointer'
|
||||||
isModelSupportRagAndTools
|
: 'cursor-not-allowed opacity-50'
|
||||||
? 'cursor-pointer'
|
)}
|
||||||
: 'cursor-not-allowed opacity-50'
|
onClick={() => {
|
||||||
)}
|
if (activeAssistant?.model.settings?.vision_model) {
|
||||||
onClick={() => {
|
imageInputRef.current?.click()
|
||||||
if (activeAssistant?.model.settings?.vision_model) {
|
setShowAttacmentMenus(false)
|
||||||
imageInputRef.current?.click()
|
}
|
||||||
setShowAttacmentMenus(false)
|
}}
|
||||||
}
|
>
|
||||||
}}
|
<ImageIcon size={16} />
|
||||||
>
|
<span className="font-medium">Image</span>
|
||||||
<ImageIcon size={16} />
|
</li>
|
||||||
<span className="font-medium">Image</span>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
content="This feature only supports multimodal models."
|
|
||||||
disabled={activeAssistant?.model.settings?.vision_model}
|
|
||||||
/>
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
side="bottom"
|
side="bottom"
|
||||||
trigger={
|
trigger={
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { forwardRef, useEffect, useState } from 'react'
|
import React, { forwardRef, useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
events,
|
events,
|
||||||
@ -8,10 +8,14 @@ import {
|
|||||||
ThreadMessage,
|
ThreadMessage,
|
||||||
} from '@janhq/core'
|
} from '@janhq/core'
|
||||||
|
|
||||||
|
import { useAtom } from 'jotai'
|
||||||
|
|
||||||
import ErrorMessage from '@/containers/ErrorMessage'
|
import ErrorMessage from '@/containers/ErrorMessage'
|
||||||
|
|
||||||
import MessageContainer from '../TextMessage'
|
import MessageContainer from '../TextMessage'
|
||||||
|
|
||||||
|
import { subscribedGeneratingMessageAtom } from '@/helpers/atoms/ChatMessage.atom'
|
||||||
|
|
||||||
type Ref = HTMLDivElement
|
type Ref = HTMLDivElement
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -22,9 +26,13 @@ type Props = {
|
|||||||
const ChatItem = forwardRef<Ref, Props>((message, ref) => {
|
const ChatItem = forwardRef<Ref, Props>((message, ref) => {
|
||||||
const [content, setContent] = useState<ThreadContent[]>(message.content)
|
const [content, setContent] = useState<ThreadContent[]>(message.content)
|
||||||
const [status, setStatus] = useState<MessageStatus>(message.status)
|
const [status, setStatus] = useState<MessageStatus>(message.status)
|
||||||
|
const [subscribedGeneratingMessage, setSubscribedGeneratingMessage] = useAtom(
|
||||||
|
subscribedGeneratingMessageAtom
|
||||||
|
)
|
||||||
const [errorMessage, setErrorMessage] = useState<ThreadMessage | undefined>(
|
const [errorMessage, setErrorMessage] = useState<ThreadMessage | undefined>(
|
||||||
message.isCurrentMessage && !!message?.metadata?.error ? message : undefined
|
message.isCurrentMessage && !!message?.metadata?.error ? message : undefined
|
||||||
)
|
)
|
||||||
|
const subscribedGeneratingMessageRef = useRef(subscribedGeneratingMessage)
|
||||||
|
|
||||||
function onMessageUpdate(data: ThreadMessage) {
|
function onMessageUpdate(data: ThreadMessage) {
|
||||||
if (data.id === message.id) {
|
if (data.id === message.id) {
|
||||||
@ -32,9 +40,21 @@ const ChatItem = forwardRef<Ref, Props>((message, ref) => {
|
|||||||
if (data.status !== status) setStatus(data.status)
|
if (data.status !== status) setStatus(data.status)
|
||||||
if (data.status === MessageStatus.Error && message.isCurrentMessage)
|
if (data.status === MessageStatus.Error && message.isCurrentMessage)
|
||||||
setErrorMessage(data)
|
setErrorMessage(data)
|
||||||
|
|
||||||
|
// Update subscriber if the message is generating
|
||||||
|
if (
|
||||||
|
subscribedGeneratingMessageRef.current?.thread_id !== message.thread_id
|
||||||
|
)
|
||||||
|
setSubscribedGeneratingMessage({
|
||||||
|
thread_id: message.thread_id,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
subscribedGeneratingMessageRef.current = subscribedGeneratingMessage
|
||||||
|
}, [subscribedGeneratingMessage])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!message.isCurrentMessage && errorMessage) setErrorMessage(undefined)
|
if (!message.isCurrentMessage && errorMessage) setErrorMessage(undefined)
|
||||||
}, [message, errorMessage])
|
}, [message, errorMessage])
|
||||||
|
|||||||
@ -30,6 +30,7 @@ import RequestDownloadModel from './RequestDownloadModel'
|
|||||||
import { showSystemMonitorPanelAtom } from '@/helpers/atoms/App.atom'
|
import { showSystemMonitorPanelAtom } from '@/helpers/atoms/App.atom'
|
||||||
import { experimentalFeatureEnabledAtom } from '@/helpers/atoms/AppConfig.atom'
|
import { experimentalFeatureEnabledAtom } from '@/helpers/atoms/AppConfig.atom'
|
||||||
import { activeAssistantAtom } from '@/helpers/atoms/Assistant.atom'
|
import { activeAssistantAtom } from '@/helpers/atoms/Assistant.atom'
|
||||||
|
import { chatWidthAtom } from '@/helpers/atoms/Setting.atom'
|
||||||
import { activeThreadAtom } from '@/helpers/atoms/Thread.atom'
|
import { activeThreadAtom } from '@/helpers/atoms/Thread.atom'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -59,6 +60,7 @@ const ThreadCenterPanel = () => {
|
|||||||
const experimentalFeature = useAtomValue(experimentalFeatureEnabledAtom)
|
const experimentalFeature = useAtomValue(experimentalFeatureEnabledAtom)
|
||||||
const activeThread = useAtomValue(activeThreadAtom)
|
const activeThread = useAtomValue(activeThreadAtom)
|
||||||
const activeAssistant = useAtomValue(activeAssistantAtom)
|
const activeAssistant = useAtomValue(activeAssistantAtom)
|
||||||
|
const chatWidth = useAtomValue(chatWidthAtom)
|
||||||
const upload = uploader()
|
const upload = uploader()
|
||||||
const acceptedFormat: Accept = activeAssistant?.model.settings?.vision_model
|
const acceptedFormat: Accept = activeAssistant?.model.settings?.vision_model
|
||||||
? {
|
? {
|
||||||
@ -235,7 +237,14 @@ const ThreadCenterPanel = () => {
|
|||||||
{reloadModel && <ModelReload />}
|
{reloadModel && <ModelReload />}
|
||||||
|
|
||||||
{activeModel && isGeneratingResponse && <GenerateResponse />}
|
{activeModel && isGeneratingResponse && <GenerateResponse />}
|
||||||
<ChatInput />
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
'mx-auto w-full',
|
||||||
|
chatWidth === 'compact' && 'max-w-[700px]'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ChatInput />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CenterPanelContainer>
|
</CenterPanelContainer>
|
||||||
|
|||||||
@ -32,12 +32,9 @@ const Tools = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!activeThread) return
|
if (!activeThread) return
|
||||||
let model = downloadedModels.find(
|
const model = downloadedModels.find(
|
||||||
(model) => model.id === activeAssistant?.model.id
|
(model) => model.id === activeAssistant?.model.id
|
||||||
)
|
)
|
||||||
if (!model) {
|
|
||||||
model = recommendedModel
|
|
||||||
}
|
|
||||||
setSelectedModel(model)
|
setSelectedModel(model)
|
||||||
}, [
|
}, [
|
||||||
recommendedModel,
|
recommendedModel,
|
||||||
|
|||||||
@ -257,7 +257,7 @@ const ThreadRightPanel = () => {
|
|||||||
id="assistant-instructions"
|
id="assistant-instructions"
|
||||||
placeholder="Eg. You are a helpful assistant."
|
placeholder="Eg. You are a helpful assistant."
|
||||||
value={activeAssistant?.instructions ?? ''}
|
value={activeAssistant?.instructions ?? ''}
|
||||||
autoResize
|
// autoResize
|
||||||
onChange={onAssistantInstructionChanged}
|
onChange={onAssistantInstructionChanged}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user