chore: handle many issues with app settings and message actions (#5086)

* chore: handle many issues with app settings and message actions

* Update web-app/src/services/mcp.ts

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
This commit is contained in:
Louis 2025-05-23 21:23:52 +07:00 committed by GitHub
parent 634efb9d9d
commit 125104320e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 115 additions and 84 deletions

View File

@ -125,10 +125,25 @@ export const ThreadContent = memo(
deleteMessage(toSendMessage.thread_id, toSendMessage.id ?? '')
toSendMessage = threadMessages.pop()
}
if (toSendMessage)
if (toSendMessage) {
deleteMessage(toSendMessage.thread_id, toSendMessage.id ?? '')
sendMessage(toSendMessage.content?.[0]?.text?.value || '')
}
}, [deleteMessage, getMessages, item, sendMessage])
const removeMessage = useCallback(() => {
if (item.role === 'assistant' || item.role === 'tool') {
const threadMessages = getMessages(item.thread_id)
let toSendMessage = threadMessages.pop()
while (toSendMessage && toSendMessage?.role !== 'user') {
deleteMessage(toSendMessage.thread_id, toSendMessage.id ?? '')
toSendMessage = threadMessages.pop()
}
} else {
deleteMessage(item.thread_id, item.id)
}
}, [deleteMessage, getMessages, item])
const editMessage = useCallback(
(messageId: string) => {
const threadMessages = getMessages(item.thread_id)
@ -302,7 +317,7 @@ export const ThreadContent = memo(
<button
className="flex items-center gap-1 hover:text-accent transition-colors cursor-pointer group relative"
onClick={() => {
deleteMessage(item.thread_id, item.id)
removeMessage()
}}
>
<IconTrash size={16} />

View File

@ -118,14 +118,16 @@ export default function AddEditMCPServer({
if (open && editingKey && initialData) {
setServerName(editingKey)
setCommand(initialData.command)
setArgs(initialData.args.length > 0 ? initialData.args : [''])
setArgs(initialData.args?.length > 0 ? initialData.args : [''])
if (initialData.env) {
// Convert env object to arrays of keys and values
const keys = Object.keys(initialData.env)
const values = keys.map((key) => initialData.env[key])
setEnvKeys(keys.length > 0 ? keys : [''])
setEnvValues(values.length > 0 ? values : [''])
}
} else if (open) {
// Add mode - reset form
resetForm()

View File

@ -98,6 +98,9 @@ export const useChat = () => {
let isCompleted = false
let attempts = 0
const availableTools = selectedModel?.capabilities?.includes('tools')
? tools
: []
while (
!isCompleted &&
!abortController.signal.aborted &&
@ -110,9 +113,11 @@ export const useChat = () => {
provider,
builder.getMessages(),
abortController,
tools,
availableTools,
// TODO: replace it with according provider setting later on
selectedProvider === 'llama.cpp' && tools.length > 0 ? false : true
selectedProvider === 'llama.cpp' && availableTools.length > 0
? false
: true
)
if (!completion) throw new Error('No completion received')

View File

@ -1,6 +1,4 @@
import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'
import { localStorageKey } from '@/constants/localStorage'
import { updateMCPConfig } from '@/services/mcp'
// Define the structure of an MCP server configuration
@ -25,11 +23,10 @@ type MCPServerStoreState = {
addServer: (key: string, config: MCPServerConfig) => void
editServer: (key: string, config: MCPServerConfig) => void
deleteServer: (key: string) => void
setServers: (servers: MCPServers) => void
}
export const useMCPServers = create<MCPServerStoreState>()(
persist(
(set) => ({
export const useMCPServers = create<MCPServerStoreState>()((set) => ({
open: true,
mcpServers: {}, // Start with empty object
loading: false,
@ -54,7 +51,12 @@ export const useMCPServers = create<MCPServerStoreState>()(
updateMCPConfig(JSON.stringify({ mcpServers }))
return { mcpServers }
}),
setServers: (servers) =>
set((state) => {
const mcpServers = { ...state.mcpServers, ...servers }
updateMCPConfig(JSON.stringify({ mcpServers }))
return { mcpServers }
}),
// Delete an MCP server by key
deleteServer: (key) =>
set((state) => {
@ -75,10 +77,4 @@ export const useMCPServers = create<MCPServerStoreState>()(
deletedServerKeys: [...state.deletedServerKeys, key],
}
}),
}),
{
name: localStorageKey.settingMCPSevers, // Using existing key for now
storage: createJSONStorage(() => localStorage),
}
)
)
}))

View File

@ -33,8 +33,8 @@ export const useModelProvider = create<ModelProviderState>()(
)
const models = existingProvider?.models || []
const mergedModels = [
...(provider?.models ?? []),
...models.filter(
...models,
...(provider?.models ?? []).filter(
(e) => !provider?.models.some((m) => m.id === e.id)
),
]

View File

@ -8,18 +8,22 @@ import { getProviders } from '@/services/providers'
import { fetchThreads } from '@/services/threads'
import { ModelManager } from '@janhq/core'
import { useEffect } from 'react'
import { useMCPServers } from '@/hooks/useMCPServers'
import { getMCPConfig } from '@/services/mcp'
export function DataProvider() {
const { setProviders } = useModelProvider()
const { setThreads } = useThreads()
const { setMessages } = useMessages()
const { checkForUpdate } = useAppUpdater()
const { setServers } = useMCPServers()
useEffect(() => {
fetchModels().then((models) => {
models?.forEach((model) => ModelManager.instance().register(model))
getProviders().then(setProviders)
})
getMCPConfig().then((data) => setServers(data.mcpServers ?? []))
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

View File

@ -225,7 +225,7 @@ function Hardware() {
title="Name"
actions={
<span className="text-main-view-fg/80">
{hardwareData.os.name}
{hardwareData.os?.name}
</span>
}
/>
@ -233,7 +233,7 @@ function Hardware() {
title="Version"
actions={
<span className="text-main-view-fg/80">
{hardwareData.os.version}
{hardwareData.os?.version}
</span>
}
/>
@ -245,7 +245,7 @@ function Hardware() {
title="Model"
actions={
<span className="text-main-view-fg/80">
{hardwareData.cpu.model}
{hardwareData.cpu?.model}
</span>
}
/>
@ -253,7 +253,7 @@ function Hardware() {
title="Architecture"
actions={
<span className="text-main-view-fg/80">
{hardwareData.cpu.arch}
{hardwareData.cpu?.arch}
</span>
}
/>
@ -261,16 +261,16 @@ function Hardware() {
title="Cores"
actions={
<span className="text-main-view-fg/80">
{hardwareData.cpu.cores}
{hardwareData.cpu?.cores}
</span>
}
/>
{hardwareData.cpu.instructions.join(', ').length > 0 && (
{hardwareData.cpu?.instructions.join(', ').length > 0 && (
<CardItem
title="Instructions"
actions={
<span className="text-main-view-fg/80">
{hardwareData.cpu.instructions.join(', ')}
{hardwareData.cpu?.instructions?.join(', ')}
</span>
}
/>
@ -280,11 +280,11 @@ function Hardware() {
actions={
<div className="flex items-center gap-2">
<Progress
value={hardwareData.cpu.usage}
value={hardwareData.cpu?.usage}
className="h-2 w-10"
/>
<span className="text-main-view-fg/80">
{hardwareData.cpu.usage.toFixed(2)}%
{hardwareData.cpu?.usage?.toFixed(2)}%
</span>
</div>
}
@ -305,7 +305,7 @@ function Hardware() {
title="Available RAM"
actions={
<span className="text-main-view-fg/80">
{formatMegaBytes(hardwareData.ram.available)}
{formatMegaBytes(hardwareData.ram?.available)}
</span>
}
/>
@ -315,16 +315,16 @@ function Hardware() {
<div className="flex items-center gap-2">
<Progress
value={
((hardwareData.ram.total - hardwareData.ram.available) /
hardwareData.ram.total) *
((hardwareData.ram?.total - hardwareData.ram?.available) /
hardwareData.ram?.total) *
100
}
className="h-2 w-10"
/>
<span className="text-main-view-fg/80">
{(
((hardwareData.ram.total - hardwareData.ram.available) /
hardwareData.ram.total) *
((hardwareData.ram?.total - hardwareData.ram?.available) /
hardwareData.ram?.total) *
100
).toFixed(2)}
%

View File

@ -140,7 +140,7 @@ function MCPServers() {
const intervalId = setInterval(() => {
getConnectedServers().then(setConnectedServers)
}, 5000)
}, 3000)
return () => clearInterval(intervalId)
}, [setConnectedServers])
@ -223,9 +223,9 @@ function MCPServers() {
<div className="text-sm text-main-view-fg/70">
<div>Command: {config.command}</div>
<div className="my-1 break-all">
Args: {config.args.join(', ')}
Args: {config?.args?.join(', ')}
</div>
{Object.keys(config.env).length > 0 && (
{config.env && Object.keys(config.env).length > 0 && (
<div className="break-all">
Env:{' '}
{Object.entries(config.env)

View File

@ -10,6 +10,15 @@ export const updateMCPConfig = async (configs: string) => {
await window.core?.api?.restartMcpServers()
}
/**
* @description This function gets the MCP configuration.
* @returns {Promise<object>} The MCP configuration.
*/
export const getMCPConfig = async () => {
const mcpConfig = JSON.parse((await window.core?.api?.getMcpConfigs()) ?? '{}')
return mcpConfig
}
/**
* @description This function gets the MCP configuration.
* @returns {Promise<string>} The MCP configuration.