fix tests
This commit is contained in:
parent
340042682a
commit
fc784620e0
@ -12,6 +12,8 @@ export type SettingComponentProps = {
|
|||||||
extensionName?: string
|
extensionName?: string
|
||||||
requireModelReload?: boolean
|
requireModelReload?: boolean
|
||||||
configType?: ConfigType
|
configType?: ConfigType
|
||||||
|
titleKey?: string
|
||||||
|
descriptionKey?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ConfigType = 'runtime' | 'setting'
|
export type ConfigType = 'runtime' | 'setting'
|
||||||
|
|||||||
@ -41,7 +41,7 @@ export function getRAGTools(retrievalLimit: number): MCPTool[] {
|
|||||||
{
|
{
|
||||||
name: GET_CHUNKS,
|
name: GET_CHUNKS,
|
||||||
description:
|
description:
|
||||||
'Retrieve chunks from a file by their order range. For a single chunk, use start_order = end_order. Thread context is inferred automatically; you may optionally provide {"scope":"thread"}.',
|
'Retrieve chunks from a file by their order range. For a single chunk, use start_order = end_order. Thread context is inferred automatically; you may optionally provide {"scope":"thread"}. Use sparingly; intended for advanced usage. Prefer using retrieve instead for relevance-based fetching.',
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
|
|||||||
@ -565,7 +565,7 @@ const ChatInput = ({
|
|||||||
|
|
||||||
// If thread exists, ingest images immediately
|
// If thread exists, ingest images immediately
|
||||||
if (currentThreadId) {
|
if (currentThreadId) {
|
||||||
;(async () => {
|
void (async () => {
|
||||||
for (const img of newFiles) {
|
for (const img of newFiles) {
|
||||||
try {
|
try {
|
||||||
// Mark as processing
|
// Mark as processing
|
||||||
|
|||||||
@ -184,9 +184,9 @@ export const ThreadContent = memo(
|
|||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
.filter(Boolean)
|
.filter((v) => v !== null)
|
||||||
// Keep embedded document metadata in the message for regenerate
|
// Keep embedded document metadata in the message for regenerate
|
||||||
sendMessage(rawText, true, attachments)
|
sendMessage(textContent, true, attachments)
|
||||||
}
|
}
|
||||||
}, [deleteMessage, getMessages, item, sendMessage])
|
}, [deleteMessage, getMessages, item, sendMessage])
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { create } from 'zustand'
|
import { create } from 'zustand'
|
||||||
import { ExtensionManager } from '@/lib/extension'
|
import { ExtensionManager } from '@/lib/extension'
|
||||||
import { ExtensionTypeEnum, type RAGExtension } from '@janhq/core'
|
import { ExtensionTypeEnum, type RAGExtension, type SettingComponentProps } from '@janhq/core'
|
||||||
|
|
||||||
export type AttachmentsSettings = {
|
export type AttachmentsSettings = {
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
@ -14,7 +14,7 @@ export type AttachmentsSettings = {
|
|||||||
|
|
||||||
type AttachmentsStore = AttachmentsSettings & {
|
type AttachmentsStore = AttachmentsSettings & {
|
||||||
// Dynamic controller definitions for rendering UI
|
// Dynamic controller definitions for rendering UI
|
||||||
settingsDefs: any[]
|
settingsDefs: SettingComponentProps[]
|
||||||
loadSettingsDefs: () => Promise<void>
|
loadSettingsDefs: () => Promise<void>
|
||||||
setEnabled: (v: boolean) => void
|
setEnabled: (v: boolean) => void
|
||||||
setMaxFileSizeMB: (v: number) => void
|
setMaxFileSizeMB: (v: number) => void
|
||||||
@ -27,7 +27,7 @@ type AttachmentsStore = AttachmentsSettings & {
|
|||||||
|
|
||||||
const getRagExtension = (): RAGExtension | undefined => {
|
const getRagExtension = (): RAGExtension | undefined => {
|
||||||
try {
|
try {
|
||||||
return ExtensionManager.getInstance().get<RAGExtension>(ExtensionTypeEnum.RAG) as any
|
return ExtensionManager.getInstance().get<RAGExtension>(ExtensionTypeEnum.RAG)
|
||||||
} catch {
|
} catch {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
@ -43,94 +43,124 @@ export const useAttachments = create<AttachmentsStore>()((set) => ({
|
|||||||
searchMode: 'auto',
|
searchMode: 'auto',
|
||||||
settingsDefs: [],
|
settingsDefs: [],
|
||||||
loadSettingsDefs: async () => {
|
loadSettingsDefs: async () => {
|
||||||
const ext = getRagExtension() as any
|
const ext = getRagExtension()
|
||||||
if (!ext?.getSettings) return
|
if (!ext?.getSettings) return
|
||||||
try {
|
try {
|
||||||
const defs = await ext.getSettings()
|
const defs = await ext.getSettings()
|
||||||
if (Array.isArray(defs)) set({ settingsDefs: defs })
|
if (Array.isArray(defs)) set({ settingsDefs: defs })
|
||||||
} catch {}
|
} catch (e) {
|
||||||
|
console.debug('Failed to load attachment settings defs:', e)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
setEnabled: async (v) => {
|
setEnabled: async (v) => {
|
||||||
const ext = getRagExtension()
|
const ext = getRagExtension()
|
||||||
if (ext?.updateSettings) {
|
if (ext?.updateSettings) {
|
||||||
await ext.updateSettings([{ key: 'enabled', controllerProps: { value: !!v } } as any])
|
await ext.updateSettings([
|
||||||
|
{ key: 'enabled', controllerProps: { value: !!v } } as Partial<SettingComponentProps>,
|
||||||
|
])
|
||||||
}
|
}
|
||||||
set((s) => ({
|
set((s) => ({
|
||||||
enabled: v,
|
enabled: v,
|
||||||
settingsDefs: s.settingsDefs.map((d) =>
|
settingsDefs: s.settingsDefs.map((d) =>
|
||||||
d.key === 'enabled' ? { ...d, controllerProps: { ...d.controllerProps, value: !!v } } : d
|
d.key === 'enabled'
|
||||||
|
? ({ ...d, controllerProps: { ...d.controllerProps, value: !!v } } as SettingComponentProps)
|
||||||
|
: d
|
||||||
),
|
),
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
setMaxFileSizeMB: async (val) => {
|
setMaxFileSizeMB: async (val) => {
|
||||||
const ext = getRagExtension()
|
const ext = getRagExtension()
|
||||||
if (ext?.updateSettings) {
|
if (ext?.updateSettings) {
|
||||||
await ext.updateSettings([{ key: 'max_file_size_mb', controllerProps: { value: val } } as any])
|
await ext.updateSettings([
|
||||||
|
{ key: 'max_file_size_mb', controllerProps: { value: val } } as Partial<SettingComponentProps>,
|
||||||
|
])
|
||||||
}
|
}
|
||||||
set((s) => ({
|
set((s) => ({
|
||||||
maxFileSizeMB: val,
|
maxFileSizeMB: val,
|
||||||
settingsDefs: s.settingsDefs.map((d) =>
|
settingsDefs: s.settingsDefs.map((d) =>
|
||||||
d.key === 'max_file_size_mb' ? { ...d, controllerProps: { ...d.controllerProps, value: val } } : d
|
d.key === 'max_file_size_mb'
|
||||||
|
? ({ ...d, controllerProps: { ...d.controllerProps, value: val } } as SettingComponentProps)
|
||||||
|
: d
|
||||||
),
|
),
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
setRetrievalLimit: async (val) => {
|
setRetrievalLimit: async (val) => {
|
||||||
const ext = getRagExtension()
|
const ext = getRagExtension()
|
||||||
if (ext?.updateSettings) {
|
if (ext?.updateSettings) {
|
||||||
await ext.updateSettings([{ key: 'retrieval_limit', controllerProps: { value: val } } as any])
|
await ext.updateSettings([
|
||||||
|
{ key: 'retrieval_limit', controllerProps: { value: val } } as Partial<SettingComponentProps>,
|
||||||
|
])
|
||||||
}
|
}
|
||||||
set((s) => ({
|
set((s) => ({
|
||||||
retrievalLimit: val,
|
retrievalLimit: val,
|
||||||
settingsDefs: s.settingsDefs.map((d) =>
|
settingsDefs: s.settingsDefs.map((d) =>
|
||||||
d.key === 'retrieval_limit' ? { ...d, controllerProps: { ...d.controllerProps, value: val } } : d
|
d.key === 'retrieval_limit'
|
||||||
|
? ({ ...d, controllerProps: { ...d.controllerProps, value: val } } as SettingComponentProps)
|
||||||
|
: d
|
||||||
),
|
),
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
setRetrievalThreshold: async (val) => {
|
setRetrievalThreshold: async (val) => {
|
||||||
const ext = getRagExtension()
|
const ext = getRagExtension()
|
||||||
if (ext?.updateSettings) {
|
if (ext?.updateSettings) {
|
||||||
await ext.updateSettings([{ key: 'retrieval_threshold', controllerProps: { value: val } } as any])
|
await ext.updateSettings([
|
||||||
|
{ key: 'retrieval_threshold', controllerProps: { value: val } } as Partial<SettingComponentProps>,
|
||||||
|
])
|
||||||
}
|
}
|
||||||
set((s) => ({
|
set((s) => ({
|
||||||
retrievalThreshold: val,
|
retrievalThreshold: val,
|
||||||
settingsDefs: s.settingsDefs.map((d) =>
|
settingsDefs: s.settingsDefs.map((d) =>
|
||||||
d.key === 'retrieval_threshold' ? { ...d, controllerProps: { ...d.controllerProps, value: val } } : d
|
d.key === 'retrieval_threshold'
|
||||||
|
? ({ ...d, controllerProps: { ...d.controllerProps, value: val } } as SettingComponentProps)
|
||||||
|
: d
|
||||||
),
|
),
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
setChunkSizeTokens: async (val) => {
|
setChunkSizeTokens: async (val) => {
|
||||||
const ext = getRagExtension()
|
const ext = getRagExtension()
|
||||||
if (ext?.updateSettings) {
|
if (ext?.updateSettings) {
|
||||||
await ext.updateSettings([{ key: 'chunk_size_tokens', controllerProps: { value: val } } as any])
|
await ext.updateSettings([
|
||||||
|
{ key: 'chunk_size_tokens', controllerProps: { value: val } } as Partial<SettingComponentProps>,
|
||||||
|
])
|
||||||
}
|
}
|
||||||
set((s) => ({
|
set((s) => ({
|
||||||
chunkSizeTokens: val,
|
chunkSizeTokens: val,
|
||||||
settingsDefs: s.settingsDefs.map((d) =>
|
settingsDefs: s.settingsDefs.map((d) =>
|
||||||
d.key === 'chunk_size_tokens' ? { ...d, controllerProps: { ...d.controllerProps, value: val } } : d
|
d.key === 'chunk_size_tokens'
|
||||||
|
? ({ ...d, controllerProps: { ...d.controllerProps, value: val } } as SettingComponentProps)
|
||||||
|
: d
|
||||||
),
|
),
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
setOverlapTokens: async (val) => {
|
setOverlapTokens: async (val) => {
|
||||||
const ext = getRagExtension()
|
const ext = getRagExtension()
|
||||||
if (ext?.updateSettings) {
|
if (ext?.updateSettings) {
|
||||||
await ext.updateSettings([{ key: 'overlap_tokens', controllerProps: { value: val } } as any])
|
await ext.updateSettings([
|
||||||
|
{ key: 'overlap_tokens', controllerProps: { value: val } } as Partial<SettingComponentProps>,
|
||||||
|
])
|
||||||
}
|
}
|
||||||
set((s) => ({
|
set((s) => ({
|
||||||
overlapTokens: val,
|
overlapTokens: val,
|
||||||
settingsDefs: s.settingsDefs.map((d) =>
|
settingsDefs: s.settingsDefs.map((d) =>
|
||||||
d.key === 'overlap_tokens' ? { ...d, controllerProps: { ...d.controllerProps, value: val } } : d
|
d.key === 'overlap_tokens'
|
||||||
|
? ({ ...d, controllerProps: { ...d.controllerProps, value: val } } as SettingComponentProps)
|
||||||
|
: d
|
||||||
),
|
),
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
setSearchMode: async (v) => {
|
setSearchMode: async (v) => {
|
||||||
const ext = getRagExtension()
|
const ext = getRagExtension()
|
||||||
if (ext?.updateSettings) {
|
if (ext?.updateSettings) {
|
||||||
await ext.updateSettings([{ key: 'search_mode', controllerProps: { value: v } } as any])
|
await ext.updateSettings([
|
||||||
|
{ key: 'search_mode', controllerProps: { value: v } } as Partial<SettingComponentProps>,
|
||||||
|
])
|
||||||
}
|
}
|
||||||
set((s) => ({
|
set((s) => ({
|
||||||
searchMode: v,
|
searchMode: v,
|
||||||
settingsDefs: s.settingsDefs.map((d) =>
|
settingsDefs: s.settingsDefs.map((d) =>
|
||||||
d.key === 'search_mode' ? { ...d, controllerProps: { ...d.controllerProps, value: v } } : d
|
d.key === 'search_mode'
|
||||||
|
? ({ ...d, controllerProps: { ...d.controllerProps, value: v } } as SettingComponentProps)
|
||||||
|
: d
|
||||||
),
|
),
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
@ -139,22 +169,26 @@ export const useAttachments = create<AttachmentsStore>()((set) => ({
|
|||||||
// Initialize from extension settings once on import
|
// Initialize from extension settings once on import
|
||||||
;(async () => {
|
;(async () => {
|
||||||
try {
|
try {
|
||||||
const ext = getRagExtension() as any
|
const ext = getRagExtension()
|
||||||
if (!ext?.getSettings) return
|
if (!ext?.getSettings) return
|
||||||
const settings = await ext.getSettings()
|
const settings = await ext.getSettings()
|
||||||
if (!Array.isArray(settings)) return
|
if (!Array.isArray(settings)) return
|
||||||
const map = new Map<string, any>()
|
const map = new Map<string, unknown>()
|
||||||
for (const s of settings) map.set(s.key, s?.controllerProps?.value)
|
for (const s of settings) map.set(s.key, s?.controllerProps?.value)
|
||||||
// seed defs and values
|
// seed defs and values
|
||||||
useAttachments.setState((prev) => ({
|
useAttachments.setState((prev) => ({
|
||||||
settingsDefs: settings,
|
settingsDefs: settings,
|
||||||
enabled: map.get('enabled') ?? prev.enabled,
|
enabled: (map.get('enabled') as boolean | undefined) ?? prev.enabled,
|
||||||
maxFileSizeMB: map.get('max_file_size_mb') ?? prev.maxFileSizeMB,
|
maxFileSizeMB: (map.get('max_file_size_mb') as number | undefined) ?? prev.maxFileSizeMB,
|
||||||
retrievalLimit: map.get('retrieval_limit') ?? prev.retrievalLimit,
|
retrievalLimit: (map.get('retrieval_limit') as number | undefined) ?? prev.retrievalLimit,
|
||||||
retrievalThreshold: map.get('retrieval_threshold') ?? prev.retrievalThreshold,
|
retrievalThreshold:
|
||||||
chunkSizeTokens: map.get('chunk_size_tokens') ?? prev.chunkSizeTokens,
|
(map.get('retrieval_threshold') as number | undefined) ?? prev.retrievalThreshold,
|
||||||
overlapTokens: map.get('overlap_tokens') ?? prev.overlapTokens,
|
chunkSizeTokens: (map.get('chunk_size_tokens') as number | undefined) ?? prev.chunkSizeTokens,
|
||||||
searchMode: map.get('search_mode') ?? prev.searchMode,
|
overlapTokens: (map.get('overlap_tokens') as number | undefined) ?? prev.overlapTokens,
|
||||||
|
searchMode:
|
||||||
|
(map.get('search_mode') as 'auto' | 'ann' | 'linear' | undefined) ?? prev.searchMode,
|
||||||
}))
|
}))
|
||||||
} catch {}
|
} catch (e) {
|
||||||
|
console.debug('Failed to initialize attachment settings from extension:', e)
|
||||||
|
}
|
||||||
})()
|
})()
|
||||||
|
|||||||
@ -380,7 +380,7 @@ export const useChat = () => {
|
|||||||
// Build the user content once; use it for both the outbound request
|
// Build the user content once; use it for both the outbound request
|
||||||
// and persisting to the store so both are identical.
|
// and persisting to the store so both are identical.
|
||||||
if (updateAttachmentProcessing) {
|
if (updateAttachmentProcessing) {
|
||||||
updateAttachmentProcessing('__CLEAR_ALL__' as any, 'clear_all')
|
updateAttachmentProcessing('__CLEAR_ALL__', 'clear_all')
|
||||||
}
|
}
|
||||||
const userContent = newUserThreadContent(
|
const userContent = newUserThreadContent(
|
||||||
activeThread.id,
|
activeThread.id,
|
||||||
|
|||||||
@ -241,7 +241,10 @@ export const sendCompletion = async (
|
|||||||
usableTools = [...tools, ...ragTools]
|
usableTools = [...tools, ...ragTools]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch (e) {
|
||||||
|
// Ignore RAG tool injection errors during completion setup
|
||||||
|
console.debug('Skipping RAG tools injection:', e)
|
||||||
|
}
|
||||||
|
|
||||||
const engine = ExtensionManager.getInstance().getEngine(provider.provider)
|
const engine = ExtensionManager.getInstance().getEngine(provider.provider)
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
import { ChatCompletionMessageParam } from 'token.js'
|
import { ChatCompletionMessageParam } from 'token.js'
|
||||||
import { ChatCompletionMessageToolCall } from 'openai/resources'
|
import { ChatCompletionMessageToolCall } from 'openai/resources'
|
||||||
import { ThreadMessage } from '@janhq/core'
|
import { ThreadMessage, ContentType } from '@janhq/core'
|
||||||
import { removeReasoningContent } from '@/utils/reasoning'
|
import { removeReasoningContent } from '@/utils/reasoning'
|
||||||
// Attachments are now handled upstream in newUserThreadContent
|
// Attachments are now handled upstream in newUserThreadContent
|
||||||
|
|
||||||
|
type ThreadContent = NonNullable<ThreadMessage['content']>[number]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @fileoverview Helper functions for creating chat completion request.
|
* @fileoverview Helper functions for creating chat completion request.
|
||||||
* These functions are used to create chat completion request objects
|
* These functions are used to create chat completion request objects
|
||||||
@ -22,7 +23,14 @@ export class CompletionMessagesBuilder {
|
|||||||
this.messages.push(
|
this.messages.push(
|
||||||
...messages
|
...messages
|
||||||
.filter((e) => !e.metadata?.error)
|
.filter((e) => !e.metadata?.error)
|
||||||
.map<ChatCompletionMessageParam>((msg) => this.toCompletionParamFromThread(msg))
|
.map<ChatCompletionMessageParam>((msg) => {
|
||||||
|
const param = this.toCompletionParamFromThread(msg)
|
||||||
|
// In constructor context, normalize empty user text to a placeholder
|
||||||
|
if (param.role === 'user' && typeof param.content === 'string' && param.content === '') {
|
||||||
|
return { ...param, content: '.' }
|
||||||
|
}
|
||||||
|
return param
|
||||||
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,19 +53,20 @@ export class CompletionMessagesBuilder {
|
|||||||
|
|
||||||
// User messages: handle multimodal content
|
// User messages: handle multimodal content
|
||||||
if (Array.isArray(msg.content) && msg.content.length > 1) {
|
if (Array.isArray(msg.content) && msg.content.length > 1) {
|
||||||
const content = msg.content.map((part: any) => {
|
const content = msg.content.map((part: ThreadContent) => {
|
||||||
if (part.type === 'text') {
|
if (part.type === ContentType.Text) {
|
||||||
return { type: 'text', text: part.text?.value ?? '' }
|
return { type: 'text' as const, text: part.text?.value ?? '' }
|
||||||
}
|
}
|
||||||
if (part.type === 'image_url') {
|
if (part.type === ContentType.Image) {
|
||||||
return {
|
return {
|
||||||
type: 'image_url',
|
type: 'image_url' as const,
|
||||||
image_url: { url: part.image_url?.url || '', detail: part.image_url?.detail || 'auto' },
|
image_url: { url: part.image_url?.url || '', detail: part.image_url?.detail || 'auto' },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return part
|
// Fallback for unknown content types
|
||||||
|
return { type: 'text' as const, text: '' }
|
||||||
})
|
})
|
||||||
return { role: 'user', content } as any
|
return { role: 'user', content } as ChatCompletionMessageParam
|
||||||
}
|
}
|
||||||
// Single text part
|
// Single text part
|
||||||
const text = msg?.content?.[0]?.text?.value ?? '.'
|
const text = msg?.content?.[0]?.text?.value ?? '.'
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { createFileRoute } from '@tanstack/react-router'
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
import { route } from '@/constants/routes'
|
|
||||||
import SettingsMenu from '@/containers/SettingsMenu'
|
import SettingsMenu from '@/containers/SettingsMenu'
|
||||||
import HeaderPage from '@/containers/HeaderPage'
|
import HeaderPage from '@/containers/HeaderPage'
|
||||||
import { Card, CardItem } from '@/containers/Card'
|
import { Card, CardItem } from '@/containers/Card'
|
||||||
import { useAttachments } from '@/hooks/useAttachments'
|
import { useAttachments } from '@/hooks/useAttachments'
|
||||||
|
import type { SettingComponentProps } from '@janhq/core'
|
||||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||||
import { PlatformGuard } from '@/lib/platform/PlatformGuard'
|
import { PlatformGuard } from '@/lib/platform/PlatformGuard'
|
||||||
import { DynamicControllerSetting } from '@/containers/dynamicControllerSetting'
|
import { DynamicControllerSetting } from '@/containers/dynamicControllerSetting'
|
||||||
@ -11,14 +11,13 @@ import { PlatformFeature } from '@/lib/platform/types'
|
|||||||
import { useEffect, useState, useCallback, useRef } from 'react'
|
import { useEffect, useState, useCallback, useRef } from 'react'
|
||||||
import { useShallow } from 'zustand/react/shallow'
|
import { useShallow } from 'zustand/react/shallow'
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
export const Route = createFileRoute('/settings/attachments')({
|
||||||
export const Route = createFileRoute(route.settings.attachments as any)({
|
|
||||||
component: AttachmentsSettings,
|
component: AttachmentsSettings,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Helper to extract constraints from settingsDefs
|
// Helper to extract constraints from settingsDefs
|
||||||
function getConstraints(def: any) {
|
function getConstraints(def: SettingComponentProps) {
|
||||||
const props = def?.controllerProps || {}
|
const props = def.controllerProps as Partial<{ min: number; max: number; step: number }>
|
||||||
return {
|
return {
|
||||||
min: props.min ?? -Infinity,
|
min: props.min ?? -Infinity,
|
||||||
max: props.max ?? Infinity,
|
max: props.max ?? Infinity,
|
||||||
@ -27,7 +26,7 @@ function getConstraints(def: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Helper to validate and clamp numeric values
|
// Helper to validate and clamp numeric values
|
||||||
function clampValue(val: any, def: any, currentValue: number): number {
|
function clampValue(val: unknown, def: SettingComponentProps, currentValue: number): number {
|
||||||
const num = typeof val === 'number' ? val : Number(val)
|
const num = typeof val === 'number' ? val : Number(val)
|
||||||
if (!Number.isFinite(num)) return currentValue
|
if (!Number.isFinite(num)) return currentValue
|
||||||
const { min, max, step } = getConstraints(def)
|
const { min, max, step } = getConstraints(def)
|
||||||
@ -40,7 +39,7 @@ function AttachmentsSettings() {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const hookDefs = useAttachments((s) => s.settingsDefs)
|
const hookDefs = useAttachments((s) => s.settingsDefs)
|
||||||
const loadDefs = useAttachments((s) => s.loadSettingsDefs)
|
const loadDefs = useAttachments((s) => s.loadSettingsDefs)
|
||||||
const [defs, setDefs] = useState<any[]>([])
|
const [defs, setDefs] = useState<SettingComponentProps[]>([])
|
||||||
|
|
||||||
// Load schema from extension via the hook once
|
// Load schema from extension via the hook once
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -73,32 +72,36 @@ function AttachmentsSettings() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Local state for inputs to allow intermediate values while typing
|
// Local state for inputs to allow intermediate values while typing
|
||||||
const [localValues, setLocalValues] = useState<Record<string, any>>({})
|
const [localValues, setLocalValues] = useState<Record<string, string | number | boolean | string[]>>({})
|
||||||
|
|
||||||
// Debounce timers
|
// Debounce timers
|
||||||
const timersRef = useRef<Record<string, NodeJS.Timeout>>({})
|
const timersRef = useRef<Record<string, ReturnType<typeof setTimeout>>>({})
|
||||||
|
|
||||||
// Cleanup timers on unmount
|
// Cleanup timers on unmount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const timers = timersRef.current
|
||||||
return () => {
|
return () => {
|
||||||
Object.values(timersRef.current).forEach(clearTimeout)
|
Object.values(timers).forEach(clearTimeout)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Debounced setter with validation
|
// Debounced setter with validation
|
||||||
const debouncedSet = useCallback((key: string, val: any, def: any) => {
|
const debouncedSet = useCallback((key: string, val: unknown, def: SettingComponentProps) => {
|
||||||
// Clear existing timer for this key
|
// Clear existing timer for this key
|
||||||
if (timersRef.current[key]) {
|
if (timersRef.current[key]) {
|
||||||
clearTimeout(timersRef.current[key])
|
clearTimeout(timersRef.current[key])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set local value immediately for responsive UI
|
// Set local value immediately for responsive UI
|
||||||
setLocalValues((prev) => ({ ...prev, [key]: val }))
|
setLocalValues((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[key]: val as string | number | boolean | string[]
|
||||||
|
}))
|
||||||
|
|
||||||
// For non-numeric inputs, apply immediately without debounce
|
// For non-numeric inputs, apply immediately without debounce
|
||||||
if (key === 'enabled' || key === 'search_mode') {
|
if (key === 'enabled' || key === 'search_mode') {
|
||||||
if (key === 'enabled') sel.setEnabled(!!val)
|
if (key === 'enabled') sel.setEnabled(!!val)
|
||||||
else if (key === 'search_mode') sel.setSearchMode(val)
|
else if (key === 'search_mode') sel.setSearchMode(val as 'auto' | 'ann' | 'linear')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,7 +139,10 @@ function AttachmentsSettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update local value to validated one
|
// Update local value to validated one
|
||||||
setLocalValues((prev) => ({ ...prev, [key]: validated }))
|
setLocalValues((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[key]: validated as string | number | boolean | string[]
|
||||||
|
}))
|
||||||
}, 500) // 500ms debounce
|
}, 500) // 500ms debounce
|
||||||
}, [sel])
|
}, [sel])
|
||||||
|
|
||||||
@ -174,8 +180,30 @@ function AttachmentsSettings() {
|
|||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
|
|
||||||
const currentValue = localValues[d.key] !== undefined ? localValues[d.key] : storeValue
|
const currentValue =
|
||||||
const props = { ...(d.controllerProps || {}), value: currentValue }
|
localValues[d.key] !== undefined ? localValues[d.key] : storeValue
|
||||||
|
|
||||||
|
// Convert to DynamicControllerSetting compatible props
|
||||||
|
const baseProps = d.controllerProps
|
||||||
|
const normalizedValue: string | number | boolean = (() => {
|
||||||
|
if (Array.isArray(currentValue)) {
|
||||||
|
return currentValue.join(',')
|
||||||
|
}
|
||||||
|
return currentValue as string | number | boolean
|
||||||
|
})()
|
||||||
|
|
||||||
|
const props = {
|
||||||
|
value: normalizedValue,
|
||||||
|
placeholder: 'placeholder' in baseProps ? baseProps.placeholder : undefined,
|
||||||
|
type: 'type' in baseProps ? baseProps.type : undefined,
|
||||||
|
options: 'options' in baseProps ? baseProps.options : undefined,
|
||||||
|
input_actions: 'inputActions' in baseProps ? baseProps.inputActions : undefined,
|
||||||
|
rows: undefined,
|
||||||
|
min: 'min' in baseProps ? baseProps.min : undefined,
|
||||||
|
max: 'max' in baseProps ? baseProps.max : undefined,
|
||||||
|
step: 'step' in baseProps ? baseProps.step : undefined,
|
||||||
|
recommended: 'recommended' in baseProps ? baseProps.recommended : undefined,
|
||||||
|
}
|
||||||
|
|
||||||
const title = d.titleKey ? t(d.titleKey) : d.title
|
const title = d.titleKey ? t(d.titleKey) : d.title
|
||||||
const description = d.descriptionKey ? t(d.descriptionKey) : d.description
|
const description = d.descriptionKey ? t(d.descriptionKey) : d.description
|
||||||
@ -188,7 +216,7 @@ function AttachmentsSettings() {
|
|||||||
actions={
|
actions={
|
||||||
<DynamicControllerSetting
|
<DynamicControllerSetting
|
||||||
controllerType={d.controllerType}
|
controllerType={d.controllerType}
|
||||||
controllerProps={props as any}
|
controllerProps={props}
|
||||||
onChange={(val) => debouncedSet(d.key, val, d)}
|
onChange={(val) => debouncedSet(d.key, val, d)}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,14 +16,15 @@ export class DefaultRAGService implements RAGService {
|
|||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
async callTool(args: { toolName: string; arguments: object; threadId?: string }): Promise<MCPToolCallResult> {
|
async callTool(args: { toolName: string; arguments: Record<string, unknown>; threadId?: string }): Promise<MCPToolCallResult> {
|
||||||
const ext = ExtensionManager.getInstance().get<RAGExtension>(ExtensionTypeEnum.RAG)
|
const ext = ExtensionManager.getInstance().get<RAGExtension>(ExtensionTypeEnum.RAG)
|
||||||
if (!ext?.callTool) {
|
if (!ext?.callTool) {
|
||||||
return { error: 'RAG extension not available', content: [{ type: 'text', text: 'RAG extension not available' }] }
|
return { error: 'RAG extension not available', content: [{ type: 'text', text: 'RAG extension not available' }] }
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
// Inject thread context when scope requires it
|
// Inject thread context when scope requires it
|
||||||
const a: any = { ...(args.arguments as any) }
|
type ToolCallArgs = Record<string, unknown> & { scope?: string; thread_id?: string }
|
||||||
|
const a: ToolCallArgs = { ...(args.arguments as Record<string, unknown>) }
|
||||||
if (!a.scope) a.scope = 'thread'
|
if (!a.scope) a.scope = 'thread'
|
||||||
if (a.scope === 'thread' && !a.thread_id) {
|
if (a.scope === 'thread' && !a.thread_id) {
|
||||||
a.thread_id = args.threadId
|
a.thread_id = args.threadId
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user