Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions web-app/src/constants/localStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ export const localStorageKey = {
toolAvailability: 'tool-availability',
mcpGlobalPermissions: 'mcp-global-permissions',
lastUsedModel: 'last-used-model',
lastUsedAssistant: 'last-used-assistant',
}
37 changes: 28 additions & 9 deletions web-app/src/hooks/__tests__/useChat.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,31 @@ vi.mock('../usePrompt', () => ({
}))

vi.mock('../useAppState', () => ({
useAppState: vi.fn(() => ({
tools: [],
updateTokenSpeed: vi.fn(),
resetTokenSpeed: vi.fn(),
updateTools: vi.fn(),
updateStreamingContent: vi.fn(),
updateLoadingModel: vi.fn(),
setAbortController: vi.fn(),
})),
useAppState: Object.assign(
vi.fn(() => ({
tools: [],
updateTokenSpeed: vi.fn(),
resetTokenSpeed: vi.fn(),
updateTools: vi.fn(),
updateStreamingContent: vi.fn(),
updateLoadingModel: vi.fn(),
setAbortController: vi.fn(),
})),
{
getState: vi.fn(() => ({
tokenSpeed: { tokensPerSecond: 10 },
}))
}
),
}))

vi.mock('../useAssistant', () => ({
useAssistant: vi.fn(() => ({
assistants: [{
id: 'test-assistant',
instructions: 'test instructions',
parameters: { stream: true },
}],
currentAssistant: {
id: 'test-assistant',
instructions: 'test instructions',
Expand Down Expand Up @@ -88,6 +100,12 @@ vi.mock('../useModelContextApproval', () => ({
})),
}))

vi.mock('../useModelLoad', () => ({
useModelLoad: vi.fn(() => ({
setModelLoadError: vi.fn(),
})),
}))

vi.mock('@tanstack/react-router', () => ({
useRouter: vi.fn(() => ({
navigate: vi.fn(),
Expand All @@ -96,6 +114,7 @@ vi.mock('@tanstack/react-router', () => ({

vi.mock('@/lib/completion', () => ({
emptyThreadContent: { thread_id: 'test-thread', content: '' },
extractToolCall: vi.fn(),
newUserThreadContent: vi.fn(() => ({ thread_id: 'test-thread', content: 'user message' })),
newAssistantThreadContent: vi.fn(() => ({ thread_id: 'test-thread', content: 'assistant message' })),
sendCompletion: vi.fn(),
Expand Down
8 changes: 7 additions & 1 deletion web-app/src/hooks/useAppState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,20 @@
tokenSpeed: undefined,
currentToolCall: undefined,
updateStreamingContent: (content: ThreadMessage | undefined) => {
const assistants = useAssistant.getState().assistants
const currentAssistant = useAssistant.getState().currentAssistant

const selectedAssistant =
assistants.find((a) => a.id === currentAssistant.id) || assistants[0]

Check failure on line 42 in web-app/src/hooks/useAppState.ts

View workflow job for this annotation

GitHub Actions / test-on-windows-pr

src/hooks/__tests__/useAppState.test.ts > useAppState > should update streaming content

TypeError: Cannot read properties of undefined (reading 'find') ❯ Object.updateStreamingContent src/hooks/useAppState.ts:42:18 ❯ src/hooks/__tests__/useAppState.test.ts:57:22 ❯ node_modules/@testing-library/react/dist/act-compat.js:48:24 ❯ process.env.NODE_ENV.exports.act node_modules/react/cjs/react.development.js:1081:22 ❯ Proxy.<anonymous> node_modules/@testing-library/react/dist/act-compat.js:47:25 ❯ src/hooks/__tests__/useAppState.test.ts:56:5

Check failure on line 42 in web-app/src/hooks/useAppState.ts

View workflow job for this annotation

GitHub Actions / test-on-macos

src/hooks/__tests__/useAppState.test.ts > useAppState > should update streaming content

TypeError: Cannot read properties of undefined (reading 'find') ❯ Object.updateStreamingContent src/hooks/useAppState.ts:42:18 ❯ src/hooks/__tests__/useAppState.test.ts:57:22 ❯ node_modules/@testing-library/react/dist/act-compat.js:48:24 ❯ process.env.NODE_ENV.exports.act node_modules/react/cjs/react.development.js:1081:22 ❯ Proxy.<anonymous> node_modules/@testing-library/react/dist/act-compat.js:47:25 ❯ src/hooks/__tests__/useAppState.test.ts:56:5

Check failure on line 42 in web-app/src/hooks/useAppState.ts

View workflow job for this annotation

GitHub Actions / test-on-ubuntu

src/hooks/__tests__/useAppState.test.ts > useAppState > should update streaming content

TypeError: Cannot read properties of undefined (reading 'find') ❯ Object.updateStreamingContent src/hooks/useAppState.ts:42:18 ❯ src/hooks/__tests__/useAppState.test.ts:57:22 ❯ node_modules/@testing-library/react/dist/act-compat.js:48:24 ❯ process.env.NODE_ENV.exports.act node_modules/react/cjs/react.development.js:1081:22 ❯ Proxy.<anonymous> node_modules/@testing-library/react/dist/act-compat.js:47:25 ❯ src/hooks/__tests__/useAppState.test.ts:56:5

Check failure on line 42 in web-app/src/hooks/useAppState.ts

View workflow job for this annotation

GitHub Actions / coverage-check

src/hooks/__tests__/useAppState.test.ts > useAppState > should update streaming content

TypeError: Cannot read properties of undefined (reading 'find') ❯ Object.updateStreamingContent src/hooks/useAppState.ts:42:18 ❯ src/hooks/__tests__/useAppState.test.ts:57:22 ❯ node_modules/@testing-library/react/dist/act-compat.js:48:24 ❯ process.env.NODE_ENV.exports.act node_modules/react/cjs/react.development.js:1081:22 ❯ Proxy.<anonymous> node_modules/@testing-library/react/dist/act-compat.js:47:25 ❯ src/hooks/__tests__/useAppState.test.ts:56:5

set(() => ({
streamingContent: content
? {
...content,
created_at: content.created_at || Date.now(),
metadata: {
...content.metadata,
assistant: useAssistant.getState().currentAssistant,
assistant: selectedAssistant,
},
}
: undefined,
Expand Down
65 changes: 61 additions & 4 deletions web-app/src/hooks/useAssistant.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,37 @@
import { createAssistant, deleteAssistant } from '@/services/assistants'
import { Assistant as CoreAssistant } from '@janhq/core'
import { create } from 'zustand'
import { localStorageKey } from '@/constants/localStorage'

interface AssistantState {
assistants: Assistant[]
currentAssistant: Assistant
addAssistant: (assistant: Assistant) => void
updateAssistant: (assistant: Assistant) => void
deleteAssistant: (id: string) => void
setCurrentAssistant: (assistant: Assistant) => void
setCurrentAssistant: (assistant: Assistant, saveToStorage?: boolean) => void
setAssistants: (assistants: Assistant[]) => void
getLastUsedAssistant: () => string | null
setLastUsedAssistant: (assistantId: string) => void
initializeWithLastUsed: () => void
}

// Helper functions for localStorage
const getLastUsedAssistantId = (): string | null => {
try {
return localStorage.getItem(localStorageKey.lastUsedAssistant)
} catch (error) {
console.debug('Failed to get last used assistant from localStorage:', error)
return null
}
}

const setLastUsedAssistantId = (assistantId: string) => {
try {
localStorage.setItem(localStorageKey.lastUsedAssistant, assistantId)
} catch (error) {
console.debug('Failed to set last used assistant in localStorage:', error)
}
}

export const defaultAssistant: Assistant = {
Expand Down Expand Up @@ -51,17 +73,52 @@ export const useAssistant = create<AssistantState>()((set, get) => ({
})
},
deleteAssistant: (id) => {
const state = get()
deleteAssistant(
get().assistants.find((e) => e.id === id) as unknown as CoreAssistant
state.assistants.find((e) => e.id === id) as unknown as CoreAssistant
).catch((error) => {
console.error('Failed to delete assistant:', error)
})
set({ assistants: get().assistants.filter((a) => a.id !== id) })

// Check if we're deleting the current assistant
const wasCurrentAssistant = state.currentAssistant.id === id

set({ assistants: state.assistants.filter((a) => a.id !== id) })

// If the deleted assistant was current, fallback to default and update localStorage
if (wasCurrentAssistant) {
set({ currentAssistant: defaultAssistant })
setLastUsedAssistantId(defaultAssistant.id)
}
},
setCurrentAssistant: (assistant) => {
setCurrentAssistant: (assistant, saveToStorage = true) => {
set({ currentAssistant: assistant })
if (saveToStorage) {
setLastUsedAssistantId(assistant.id)
}
},
setAssistants: (assistants) => {
set({ assistants })
},
getLastUsedAssistant: () => {
return getLastUsedAssistantId()
},
setLastUsedAssistant: (assistantId) => {
setLastUsedAssistantId(assistantId)
},
initializeWithLastUsed: () => {
const lastUsedId = getLastUsedAssistantId()
if (lastUsedId) {
const lastUsedAssistant = get().assistants.find(
(a) => a.id === lastUsedId
)
if (lastUsedAssistant) {
set({ currentAssistant: lastUsedAssistant })
} else {
// Fallback to default if last used assistant was deleted
set({ currentAssistant: defaultAssistant })
setLastUsedAssistantId(defaultAssistant.id)
}
}
},
}))
11 changes: 8 additions & 3 deletions web-app/src/hooks/useChat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
updateLoadingModel,
setAbortController,
} = useAppState()
const { currentAssistant } = useAssistant()
const { assistants, currentAssistant } = useAssistant()
const { updateProvider } = useModelProvider()

const { approvedTools, showApprovalModal, allowAllMCPPermissions } =
Expand Down Expand Up @@ -72,6 +72,9 @@
return provider?.provider || selectedProvider
}, [provider, selectedProvider])

const selectedAssistant =
assistants.find((a) => a.id === currentAssistant.id) || assistants[0]

useEffect(() => {
function setTools() {
getTools().then((data: MCPTool[]) => {
Expand All @@ -90,14 +93,15 @@

const getCurrentThread = useCallback(async () => {
let currentThread = retrieveThread()

if (!currentThread) {
currentThread = await createThread(
{
id: selectedModel?.id ?? defaultModel(selectedProvider),
provider: selectedProvider,
},
prompt,
currentAssistant
selectedAssistant
)
router.navigate({
to: route.threadsDetail,
Expand All @@ -112,7 +116,7 @@
router,
selectedModel?.id,
selectedProvider,
currentAssistant,
selectedAssistant,
])

const restartModel = useCallback(
Expand Down Expand Up @@ -399,6 +403,7 @@
accumulatedText,
{
tokenSpeed: useAppState.getState().tokenSpeed,
assistant: currentAssistant,
}
)

Expand Down Expand Up @@ -431,12 +436,12 @@
} finally {
updateLoadingModel(false)
updateStreamingContent(undefined)
}

Check warning on line 439 in web-app/src/hooks/useChat.ts

View workflow job for this annotation

GitHub Actions / base_branch_cov

React Hook useCallback has a missing dependency: 'experimentalFeatures'. Either include it or remove the dependency array
},
[
getCurrentThread,
resetTokenSpeed,
currentProviderId,

Check warning on line 444 in web-app/src/hooks/useChat.ts

View workflow job for this annotation

GitHub Actions / test-on-windows-pr

React Hook useCallback has a missing dependency: 'experimentalFeatures'. Either include it or remove the dependency array

Check warning on line 444 in web-app/src/hooks/useChat.ts

View workflow job for this annotation

GitHub Actions / test-on-macos

React Hook useCallback has a missing dependency: 'experimentalFeatures'. Either include it or remove the dependency array

Check warning on line 444 in web-app/src/hooks/useChat.ts

View workflow job for this annotation

GitHub Actions / test-on-ubuntu

React Hook useCallback has a missing dependency: 'experimentalFeatures'. Either include it or remove the dependency array

Check warning on line 444 in web-app/src/hooks/useChat.ts

View workflow job for this annotation

GitHub Actions / coverage-check

React Hook useCallback has a missing dependency: 'experimentalFeatures'. Either include it or remove the dependency array
getProviderByName,
provider,
getMessages,
Expand Down
13 changes: 6 additions & 7 deletions web-app/src/hooks/useMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,18 @@ export const useMessages = create<MessageState>()((set, get) => ({
}))
},
addMessage: (message) => {
const assistants = useAssistant.getState().assistants
const currentAssistant = useAssistant.getState().currentAssistant

const selectedAssistant =
assistants.find((a) => a.id === currentAssistant.id) || assistants[0]

const newMessage = {
...message,
created_at: message.created_at || Date.now(),
metadata: {
...message.metadata,
assistant: {
id: currentAssistant?.id || '',
name: currentAssistant?.name || '',
avatar: currentAssistant?.avatar || '',
instructions: currentAssistant?.instructions || '',
parameters: currentAssistant?.parameters || '',
},
assistant: selectedAssistant,
},
}
createMessage(newMessage).then((createdMessage) => {
Expand Down
3 changes: 2 additions & 1 deletion web-app/src/providers/DataProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function DataProvider() {
const { setMessages } = useMessages()
const { checkForUpdate } = useAppUpdater()
const { setServers } = useMCPServers()
const { setAssistants } = useAssistant()
const { setAssistants, initializeWithLastUsed } = useAssistant()
const { setThreads } = useThreads()
const navigate = useNavigate()

Expand All @@ -37,6 +37,7 @@ export function DataProvider() {
// Only update assistants if we have valid data
if (data && Array.isArray(data) && data.length > 0) {
setAssistants(data as unknown as Assistant[])
initializeWithLastUsed()
}
})
.catch((error) => {
Expand Down
Loading