Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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',
}
3 changes: 2 additions & 1 deletion web-app/src/hooks/__tests__/useAppState.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ vi.mock('../useAssistant', () => ({
})),
{
getState: vi.fn(() => ({
currentAssistant: { id: 'test-assistant', name: 'Test Assistant' }
currentAssistant: { id: 'test-assistant', name: 'Test Assistant' },
assistants: [{ id: 'test-assistant', name: 'Test Assistant' }]
}))
}
)
Expand Down
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,17 +35,23 @@
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]

set(() => ({
streamingContent: content
? {
...content,
created_at: content.created_at || Date.now(),
metadata: {
...content.metadata,
assistant: useAssistant.getState().currentAssistant,
assistant: selectedAssistant,
},
}
: undefined,

Check warning on line 54 in web-app/src/hooks/useAppState.ts

View workflow job for this annotation

GitHub Actions / coverage-check

54 line is not covered with tests
}))
},
updateCurrentToolCall: (toolCall) => {
Expand Down Expand Up @@ -83,10 +89,10 @@
}
}

const timeDiffInSeconds =
(currentTimestamp - state.tokenSpeed.lastTimestamp) / 1000 // Time difference in seconds
const totalTokenCount = state.tokenSpeed.tokenCount + 1
const averageTokenSpeed =

Check warning on line 95 in web-app/src/hooks/useAppState.ts

View workflow job for this annotation

GitHub Actions / coverage-check

92-95 lines are not covered with tests
totalTokenCount / (timeDiffInSeconds > 0 ? timeDiffInSeconds : 1) // Calculate average token speed
return {
tokenSpeed: {
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
}
}

Check warning on line 27 in web-app/src/hooks/useAssistant.ts

View workflow job for this annotation

GitHub Actions / coverage-check

21-27 lines are not covered with tests

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

Check warning on line 34 in web-app/src/hooks/useAssistant.ts

View workflow job for this annotation

GitHub Actions / coverage-check

33-34 lines are not covered with tests
}

export const defaultAssistant: Assistant = {
Expand All @@ -30,7 +52,7 @@
addAssistant: (assistant) => {
set({ assistants: [...get().assistants, assistant] })
createAssistant(assistant as unknown as CoreAssistant).catch((error) => {
console.error('Failed to create assistant:', error)

Check warning on line 55 in web-app/src/hooks/useAssistant.ts

View workflow job for this annotation

GitHub Actions / coverage-check

55 line is not covered with tests
})
},
updateAssistant: (assistant) => {
Expand All @@ -43,25 +65,60 @@
currentAssistant:
state.currentAssistant.id === assistant.id
? assistant
: state.currentAssistant,

Check warning on line 68 in web-app/src/hooks/useAssistant.ts

View workflow job for this annotation

GitHub Actions / coverage-check

68 line is not covered with tests
})
// Create assistant already cover update logic
createAssistant(assistant as unknown as CoreAssistant).catch((error) => {
console.error('Failed to update assistant:', error)

Check warning on line 72 in web-app/src/hooks/useAssistant.ts

View workflow job for this annotation

GitHub Actions / coverage-check

72 line is not covered with tests
})
},
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)

Check warning on line 80 in web-app/src/hooks/useAssistant.ts

View workflow job for this annotation

GitHub Actions / coverage-check

80 line is not covered with tests
})
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)
}

Check warning on line 92 in web-app/src/hooks/useAssistant.ts

View workflow job for this annotation

GitHub Actions / coverage-check

90-92 lines are not covered with tests
},
setCurrentAssistant: (assistant) => {
setCurrentAssistant: (assistant, saveToStorage = true) => {
set({ currentAssistant: assistant })
if (saveToStorage) {
setLastUsedAssistantId(assistant.id)
}
},
setAssistants: (assistants) => {
set({ assistants })
},
getLastUsedAssistant: () => {
return getLastUsedAssistantId()
},

Check warning on line 105 in web-app/src/hooks/useAssistant.ts

View workflow job for this annotation

GitHub Actions / coverage-check

104-105 lines are not covered with tests
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-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-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-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