initial layout

This commit is contained in:
Dinh Long Nguyen 2025-10-07 10:36:45 +07:00
parent b309d34274
commit a72c74dbf9
13 changed files with 338 additions and 7 deletions

View File

@ -10,6 +10,7 @@ export const route = {
model_providers: '/settings/providers', model_providers: '/settings/providers',
providers: '/settings/providers/$providerName', providers: '/settings/providers/$providerName',
general: '/settings/general', general: '/settings/general',
attachments: '/settings/attachments',
appearance: '/settings/appearance', appearance: '/settings/appearance',
privacy: '/settings/privacy', privacy: '/settings/privacy',
shortcuts: '/settings/shortcuts', shortcuts: '/settings/shortcuts',

View File

@ -73,6 +73,12 @@ const SettingsMenu = () => {
hasSubMenu: false, hasSubMenu: false,
isEnabled: true, isEnabled: true,
}, },
{
title: 'common:attachments',
route: route.settings.attachments,
hasSubMenu: false,
isEnabled: PlatformFeatures[PlatformFeature.ATTACHMENTS],
},
{ {
title: 'common:appearance', title: 'common:appearance',
route: route.settings.appearance, route: route.settings.appearance,

View File

@ -0,0 +1,45 @@
import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'
import { localStorageKey } from '@/constants/localStorage'
export type AttachmentsSettings = {
enabled: boolean
maxFileSizeMB: number
retrievalLimit: number
retrievalThreshold: number
chunkSizeTokens: number
overlapTokens: number
}
type AttachmentsStore = AttachmentsSettings & {
setEnabled: (v: boolean) => void
setMaxFileSizeMB: (v: number) => void
setRetrievalLimit: (v: number) => void
setRetrievalThreshold: (v: number) => void
setChunkSizeTokens: (v: number) => void
setOverlapTokens: (v: number) => void
}
export const useAttachments = create<AttachmentsStore>()(
persist(
(set) => ({
enabled: true,
maxFileSizeMB: 20,
retrievalLimit: 3,
retrievalThreshold: 0.5,
chunkSizeTokens: 512,
overlapTokens: 64,
setEnabled: (v) => set({ enabled: v }),
setMaxFileSizeMB: (v) => set({ maxFileSizeMB: Math.max(1, Math.min(200, Math.floor(v))) }),
setRetrievalLimit: (v) => set({ retrievalLimit: Math.max(1, Math.min(20, Math.floor(v))) }),
setRetrievalThreshold: (v) => set({ retrievalThreshold: Math.max(0, Math.min(1, v)) }),
setChunkSizeTokens: (v) => set({ chunkSizeTokens: Math.max(64, Math.min(8192, Math.floor(v))) }),
setOverlapTokens: (v) => set({ overlapTokens: Math.max(0, Math.min(1024, Math.floor(v))) }),
}),
{
name: `${localStorageKey.settingGeneral}-attachments`,
storage: createJSONStorage(() => localStorage),
}
)
)

View File

@ -5,10 +5,14 @@ import { useAppState } from './useAppState'
import { useToolAvailable } from './useToolAvailable' import { useToolAvailable } from './useToolAvailable'
import { ExtensionManager } from '@/lib/extension' import { ExtensionManager } from '@/lib/extension'
import { ExtensionTypeEnum, MCPExtension } from '@janhq/core' import { ExtensionTypeEnum, MCPExtension } from '@janhq/core'
import { useAttachments } from './useAttachments'
import { PlatformFeatures } from '@/lib/platform/const'
import { PlatformFeature } from '@/lib/platform/types'
export const useTools = () => { export const useTools = () => {
const updateTools = useAppState((state) => state.updateTools) const updateTools = useAppState((state) => state.updateTools)
const { isDefaultsInitialized, setDefaultDisabledTools, markDefaultsAsInitialized } = useToolAvailable() const { isDefaultsInitialized, setDefaultDisabledTools, markDefaultsAsInitialized } = useToolAvailable()
const attachmentsEnabled = useAttachments((s) => s.enabled)
useEffect(() => { useEffect(() => {
async function setTools() { async function setTools() {
@ -19,11 +23,19 @@ export const useTools = () => {
) )
// Fetch tools // Fetch tools
const data = await getServiceHub().mcp().getTools() const [mcpTools, ragTools] = await Promise.all([
updateTools(data) getServiceHub().mcp().getTools(),
// Only include RAG tools when attachments feature is enabled
useAttachments.getState().enabled && PlatformFeatures[PlatformFeature.ATTACHMENTS]
? getServiceHub().rag().getTools()
: Promise.resolve([]),
])
const combined = [...mcpTools, ...ragTools]
updateTools(combined)
// Initialize default disabled tools for new users (only once) // Initialize default disabled tools for new users (only once)
if (!isDefaultsInitialized() && data.length > 0 && mcpExtension?.getDefaultDisabledTools) { if (!isDefaultsInitialized() && combined.length > 0 && mcpExtension?.getDefaultDisabledTools) {
const defaultDisabled = await mcpExtension.getDefaultDisabledTools() const defaultDisabled = await mcpExtension.getDefaultDisabledTools()
if (defaultDisabled.length > 0) { if (defaultDisabled.length > 0) {
setDefaultDisabledTools(defaultDisabled) setDefaultDisabledTools(defaultDisabled)
@ -31,7 +43,7 @@ export const useTools = () => {
} }
} }
} catch (error) { } catch (error) {
console.error('Failed to fetch MCP tools:', error) console.error('Failed to fetch tools:', error)
} }
} }
setTools() setTools()
@ -41,9 +53,14 @@ export const useTools = () => {
// Unsubscribe from the event when the component unmounts // Unsubscribe from the event when the component unmounts
unsubscribe = unsub unsubscribe = unsub
}).catch((error) => { }).catch((error) => {
console.error('Failed to set up MCP update listener:', error) console.error('Failed to set up tools update listener:', error)
}) })
return unsubscribe return unsubscribe
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [])
// Refresh tools when attachments feature toggles
useEffect(() => {
getServiceHub().events().emit(SystemEvent.MCP_UPDATE)
}, [attachmentsEnabled])
} }

View File

@ -78,6 +78,10 @@ export const PlatformFeatures: Record<PlatformFeature, boolean> = {
// First message persisted thread - enabled for web only // First message persisted thread - enabled for web only
[PlatformFeature.FIRST_MESSAGE_PERSISTED_THREAD]: !isPlatformTauri(), [PlatformFeature.FIRST_MESSAGE_PERSISTED_THREAD]: !isPlatformTauri(),
// Temporary chat mode - enabled for web only // Temporary chat mode - enabled for web only
[PlatformFeature.TEMPORARY_CHAT]: !isPlatformTauri(), [PlatformFeature.TEMPORARY_CHAT]: !isPlatformTauri(),
// Attachments/RAG UI and tooling - desktop only for now
[PlatformFeature.ATTACHMENTS]:
isPlatformTauri() && !isPlatformIOS() && !isPlatformAndroid(),
} }

View File

@ -71,4 +71,7 @@ export enum PlatformFeature {
// Temporary chat mode - web-only feature for ephemeral conversations like ChatGPT // Temporary chat mode - web-only feature for ephemeral conversations like ChatGPT
TEMPORARY_CHAT = 'temporaryChat', TEMPORARY_CHAT = 'temporaryChat',
// Attachments/RAG UI and tooling (desktop-only for now)
ATTACHMENTS = 'attachments',
} }

View File

@ -5,6 +5,7 @@
"local_api_server": "Local API Server", "local_api_server": "Local API Server",
"https_proxy": "HTTPS Proxy", "https_proxy": "HTTPS Proxy",
"extensions": "Extensions", "extensions": "Extensions",
"attachments": "Attachments",
"general": "General", "general": "General",
"settings": "Settings", "settings": "Settings",
"modelProviders": "Model Providers", "modelProviders": "Model Providers",
@ -369,4 +370,3 @@
} }
} }
} }

View File

@ -253,6 +253,25 @@
"extensions": { "extensions": {
"title": "Extensions" "title": "Extensions"
}, },
"attachments": {
"subtitle": "Configure document attachments, size limits, and retrieval behavior.",
"featureTitle": "Feature",
"enable": "Enable Attachments",
"enableDesc": "Allow uploading and indexing documents for retrieval.",
"limitsTitle": "Limits",
"maxFile": "Max File Size (MB)",
"maxFileDesc": "Maximum size per file. Enforced at upload and processing time.",
"retrievalTitle": "Retrieval",
"topK": "Top-K",
"topKDesc": "Maximum citations to return.",
"threshold": "Affinity Threshold",
"thresholdDesc": "Minimum similarity score to consider relevant (0-1).",
"chunkingTitle": "Chunking",
"chunkSize": "Chunk Size (tokens)",
"chunkSizeDesc": "Approximate max tokens per chunk for embeddings.",
"chunkOverlap": "Overlap (tokens)",
"chunkOverlapDesc": "Token overlap between consecutive chunks."
},
"dialogs": { "dialogs": {
"changeDataFolder": { "changeDataFolder": {
"title": "Change Data Folder Location", "title": "Change Data Folder Location",

View File

@ -26,6 +26,7 @@ import { Route as SettingsHttpsProxyImport } from './routes/settings/https-proxy
import { Route as SettingsHardwareImport } from './routes/settings/hardware' import { Route as SettingsHardwareImport } from './routes/settings/hardware'
import { Route as SettingsGeneralImport } from './routes/settings/general' import { Route as SettingsGeneralImport } from './routes/settings/general'
import { Route as SettingsExtensionsImport } from './routes/settings/extensions' import { Route as SettingsExtensionsImport } from './routes/settings/extensions'
import { Route as SettingsAttachmentsImport } from './routes/settings/attachments'
import { Route as SettingsAppearanceImport } from './routes/settings/appearance' import { Route as SettingsAppearanceImport } from './routes/settings/appearance'
import { Route as ProjectProjectIdImport } from './routes/project/$projectId' import { Route as ProjectProjectIdImport } from './routes/project/$projectId'
import { Route as LocalApiServerLogsImport } from './routes/local-api-server/logs' import { Route as LocalApiServerLogsImport } from './routes/local-api-server/logs'
@ -126,6 +127,12 @@ const SettingsExtensionsRoute = SettingsExtensionsImport.update({
getParentRoute: () => rootRoute, getParentRoute: () => rootRoute,
} as any) } as any)
const SettingsAttachmentsRoute = SettingsAttachmentsImport.update({
id: '/settings/attachments',
path: '/settings/attachments',
getParentRoute: () => rootRoute,
} as any)
const SettingsAppearanceRoute = SettingsAppearanceImport.update({ const SettingsAppearanceRoute = SettingsAppearanceImport.update({
id: '/settings/appearance', id: '/settings/appearance',
path: '/settings/appearance', path: '/settings/appearance',
@ -229,6 +236,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof SettingsAppearanceImport preLoaderRoute: typeof SettingsAppearanceImport
parentRoute: typeof rootRoute parentRoute: typeof rootRoute
} }
'/settings/attachments': {
id: '/settings/attachments'
path: '/settings/attachments'
fullPath: '/settings/attachments'
preLoaderRoute: typeof SettingsAttachmentsImport
parentRoute: typeof rootRoute
}
'/settings/extensions': { '/settings/extensions': {
id: '/settings/extensions' id: '/settings/extensions'
path: '/settings/extensions' path: '/settings/extensions'
@ -341,6 +355,7 @@ export interface FileRoutesByFullPath {
'/local-api-server/logs': typeof LocalApiServerLogsRoute '/local-api-server/logs': typeof LocalApiServerLogsRoute
'/project/$projectId': typeof ProjectProjectIdRoute '/project/$projectId': typeof ProjectProjectIdRoute
'/settings/appearance': typeof SettingsAppearanceRoute '/settings/appearance': typeof SettingsAppearanceRoute
'/settings/attachments': typeof SettingsAttachmentsRoute
'/settings/extensions': typeof SettingsExtensionsRoute '/settings/extensions': typeof SettingsExtensionsRoute
'/settings/general': typeof SettingsGeneralRoute '/settings/general': typeof SettingsGeneralRoute
'/settings/hardware': typeof SettingsHardwareRoute '/settings/hardware': typeof SettingsHardwareRoute
@ -366,6 +381,7 @@ export interface FileRoutesByTo {
'/local-api-server/logs': typeof LocalApiServerLogsRoute '/local-api-server/logs': typeof LocalApiServerLogsRoute
'/project/$projectId': typeof ProjectProjectIdRoute '/project/$projectId': typeof ProjectProjectIdRoute
'/settings/appearance': typeof SettingsAppearanceRoute '/settings/appearance': typeof SettingsAppearanceRoute
'/settings/attachments': typeof SettingsAttachmentsRoute
'/settings/extensions': typeof SettingsExtensionsRoute '/settings/extensions': typeof SettingsExtensionsRoute
'/settings/general': typeof SettingsGeneralRoute '/settings/general': typeof SettingsGeneralRoute
'/settings/hardware': typeof SettingsHardwareRoute '/settings/hardware': typeof SettingsHardwareRoute
@ -392,6 +408,7 @@ export interface FileRoutesById {
'/local-api-server/logs': typeof LocalApiServerLogsRoute '/local-api-server/logs': typeof LocalApiServerLogsRoute
'/project/$projectId': typeof ProjectProjectIdRoute '/project/$projectId': typeof ProjectProjectIdRoute
'/settings/appearance': typeof SettingsAppearanceRoute '/settings/appearance': typeof SettingsAppearanceRoute
'/settings/attachments': typeof SettingsAttachmentsRoute
'/settings/extensions': typeof SettingsExtensionsRoute '/settings/extensions': typeof SettingsExtensionsRoute
'/settings/general': typeof SettingsGeneralRoute '/settings/general': typeof SettingsGeneralRoute
'/settings/hardware': typeof SettingsHardwareRoute '/settings/hardware': typeof SettingsHardwareRoute
@ -419,6 +436,7 @@ export interface FileRouteTypes {
| '/local-api-server/logs' | '/local-api-server/logs'
| '/project/$projectId' | '/project/$projectId'
| '/settings/appearance' | '/settings/appearance'
| '/settings/attachments'
| '/settings/extensions' | '/settings/extensions'
| '/settings/general' | '/settings/general'
| '/settings/hardware' | '/settings/hardware'
@ -443,6 +461,7 @@ export interface FileRouteTypes {
| '/local-api-server/logs' | '/local-api-server/logs'
| '/project/$projectId' | '/project/$projectId'
| '/settings/appearance' | '/settings/appearance'
| '/settings/attachments'
| '/settings/extensions' | '/settings/extensions'
| '/settings/general' | '/settings/general'
| '/settings/hardware' | '/settings/hardware'
@ -467,6 +486,7 @@ export interface FileRouteTypes {
| '/local-api-server/logs' | '/local-api-server/logs'
| '/project/$projectId' | '/project/$projectId'
| '/settings/appearance' | '/settings/appearance'
| '/settings/attachments'
| '/settings/extensions' | '/settings/extensions'
| '/settings/general' | '/settings/general'
| '/settings/hardware' | '/settings/hardware'
@ -493,6 +513,7 @@ export interface RootRouteChildren {
LocalApiServerLogsRoute: typeof LocalApiServerLogsRoute LocalApiServerLogsRoute: typeof LocalApiServerLogsRoute
ProjectProjectIdRoute: typeof ProjectProjectIdRoute ProjectProjectIdRoute: typeof ProjectProjectIdRoute
SettingsAppearanceRoute: typeof SettingsAppearanceRoute SettingsAppearanceRoute: typeof SettingsAppearanceRoute
SettingsAttachmentsRoute: typeof SettingsAttachmentsRoute
SettingsExtensionsRoute: typeof SettingsExtensionsRoute SettingsExtensionsRoute: typeof SettingsExtensionsRoute
SettingsGeneralRoute: typeof SettingsGeneralRoute SettingsGeneralRoute: typeof SettingsGeneralRoute
SettingsHardwareRoute: typeof SettingsHardwareRoute SettingsHardwareRoute: typeof SettingsHardwareRoute
@ -518,6 +539,7 @@ const rootRouteChildren: RootRouteChildren = {
LocalApiServerLogsRoute: LocalApiServerLogsRoute, LocalApiServerLogsRoute: LocalApiServerLogsRoute,
ProjectProjectIdRoute: ProjectProjectIdRoute, ProjectProjectIdRoute: ProjectProjectIdRoute,
SettingsAppearanceRoute: SettingsAppearanceRoute, SettingsAppearanceRoute: SettingsAppearanceRoute,
SettingsAttachmentsRoute: SettingsAttachmentsRoute,
SettingsExtensionsRoute: SettingsExtensionsRoute, SettingsExtensionsRoute: SettingsExtensionsRoute,
SettingsGeneralRoute: SettingsGeneralRoute, SettingsGeneralRoute: SettingsGeneralRoute,
SettingsHardwareRoute: SettingsHardwareRoute, SettingsHardwareRoute: SettingsHardwareRoute,
@ -552,6 +574,7 @@ export const routeTree = rootRoute
"/local-api-server/logs", "/local-api-server/logs",
"/project/$projectId", "/project/$projectId",
"/settings/appearance", "/settings/appearance",
"/settings/attachments",
"/settings/extensions", "/settings/extensions",
"/settings/general", "/settings/general",
"/settings/hardware", "/settings/hardware",
@ -592,6 +615,9 @@ export const routeTree = rootRoute
"/settings/appearance": { "/settings/appearance": {
"filePath": "settings/appearance.tsx" "filePath": "settings/appearance.tsx"
}, },
"/settings/attachments": {
"filePath": "settings/attachments.tsx"
},
"/settings/extensions": { "/settings/extensions": {
"filePath": "settings/extensions.tsx" "filePath": "settings/extensions.tsx"
}, },

View File

@ -0,0 +1,185 @@
import { createFileRoute } from '@tanstack/react-router'
import { route } from '@/constants/routes'
import SettingsMenu from '@/containers/SettingsMenu'
import HeaderPage from '@/containers/HeaderPage'
import { Card, CardItem } from '@/containers/Card'
import { Switch } from '@/components/ui/switch'
import { Input } from '@/components/ui/input'
import { useAttachments } from '@/hooks/useAttachments'
import { useTranslation } from '@/i18n/react-i18next-compat'
import { PlatformGuard } from '@/lib/platform/PlatformGuard'
import { PlatformFeature } from '@/lib/platform/types'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const Route = createFileRoute(route.settings.attachments as any)({
component: AttachmentsSettings,
})
function NumberInput({
value,
onChange,
min,
max,
step = 1,
}: {
value: number
onChange: (v: number) => void
min?: number
max?: number
step?: number
}) {
return (
<Input
type="number"
value={value}
min={min}
max={max}
step={step}
onChange={(e) => onChange(Number(e.target.value))}
className="w-28"
/>
)
}
function AttachmentsSettings() {
const { t } = useTranslation()
const {
enabled,
maxFileSizeMB,
retrievalLimit,
retrievalThreshold,
chunkSizeTokens,
overlapTokens,
setEnabled,
setMaxFileSizeMB,
setRetrievalLimit,
setRetrievalThreshold,
setChunkSizeTokens,
setOverlapTokens,
} = useAttachments()
return (
<PlatformGuard feature={PlatformFeature.ATTACHMENTS}>
<div className="flex h-full w-full">
<div className="hidden sm:flex">
<SettingsMenu />
</div>
<div className="flex-1">
<HeaderPage
title={t('common:attachments') || 'Attachments'}
subtitle={
t('settings:attachments.subtitle') ||
'Configure document attachments, size limits, and retrieval behavior.'
}
/>
<div className="px-6 py-3 space-y-4">
<Card title={t('settings:attachments.featureTitle') || 'Feature'}>
<CardItem
title={t('settings:attachments.enable') || 'Enable Attachments'}
description={
t('settings:attachments.enableDesc') ||
'Allow uploading and indexing documents for retrieval.'
}
actions={
<Switch checked={enabled} onCheckedChange={setEnabled} />
}
/>
</Card>
<Card title={t('settings:attachments.limitsTitle') || 'Limits'}>
<CardItem
title={t('settings:attachments.maxFile') || 'Max File Size (MB)'}
description={
t('settings:attachments.maxFileDesc') ||
'Maximum size per file. Enforced at upload and processing time.'
}
actions={
<NumberInput
value={maxFileSizeMB}
onChange={setMaxFileSizeMB}
min={1}
max={200}
/>
}
/>
</Card>
<Card title={t('settings:attachments.retrievalTitle') || 'Retrieval'}>
<CardItem
title={t('settings:attachments.topK') || 'Top-K'}
description={
t('settings:attachments.topKDesc') || 'Maximum citations to return.'
}
actions={
<NumberInput
value={retrievalLimit}
onChange={setRetrievalLimit}
min={1}
max={10}
/>
}
/>
<CardItem
title={
t('settings:attachments.threshold') || 'Affinity Threshold'
}
description={
t('settings:attachments.thresholdDesc') ||
'Minimum similarity score to consider relevant (0-1).'
}
actions={
<Input
type="number"
value={retrievalThreshold}
min={0}
max={1}
step={0.01}
onChange={(e) => setRetrievalThreshold(Number(e.target.value))}
className="w-28"
/>
}
/>
</Card>
<Card title={t('settings:attachments.chunkingTitle') || 'Chunking'}>
<CardItem
title={
t('settings:attachments.chunkSize') || 'Chunk Size (tokens)'
}
description={
t('settings:attachments.chunkSizeDesc') ||
'Approximate max tokens per chunk for embeddings.'
}
actions={
<NumberInput
value={chunkSizeTokens}
onChange={setChunkSizeTokens}
min={64}
max={8192}
/>
}
/>
<CardItem
title={
t('settings:attachments.chunkOverlap') || 'Overlap (tokens)'
}
description={
t('settings:attachments.chunkOverlapDesc') ||
'Token overlap between consecutive chunks.'
}
actions={
<NumberInput
value={overlapTokens}
onChange={setOverlapTokens}
min={0}
max={1024}
/>
}
/>
</Card>
</div>
</div>
</div>
</PlatformGuard>
)
}

View File

@ -27,6 +27,8 @@ import { DefaultPathService } from './path/default'
import { DefaultCoreService } from './core/default' import { DefaultCoreService } from './core/default'
import { DefaultDeepLinkService } from './deeplink/default' import { DefaultDeepLinkService } from './deeplink/default'
import { DefaultProjectsService } from './projects/default' import { DefaultProjectsService } from './projects/default'
import { DefaultRAGService } from './rag/default'
import type { RAGService } from './rag/types'
// Import service types // Import service types
import type { ThemeService } from './theme/types' import type { ThemeService } from './theme/types'
@ -70,6 +72,7 @@ export interface ServiceHub {
core(): CoreService core(): CoreService
deeplink(): DeepLinkService deeplink(): DeepLinkService
projects(): ProjectsService projects(): ProjectsService
rag(): RAGService
} }
class PlatformServiceHub implements ServiceHub { class PlatformServiceHub implements ServiceHub {
@ -92,6 +95,7 @@ class PlatformServiceHub implements ServiceHub {
private coreService: CoreService = new DefaultCoreService() private coreService: CoreService = new DefaultCoreService()
private deepLinkService: DeepLinkService = new DefaultDeepLinkService() private deepLinkService: DeepLinkService = new DefaultDeepLinkService()
private projectsService: ProjectsService = new DefaultProjectsService() private projectsService: ProjectsService = new DefaultProjectsService()
private ragService: RAGService = new DefaultRAGService()
private initialized = false private initialized = false
/** /**
@ -302,6 +306,11 @@ class PlatformServiceHub implements ServiceHub {
this.ensureInitialized() this.ensureInitialized()
return this.projectsService return this.projectsService
} }
rag(): RAGService {
this.ensureInitialized()
return this.ragService
}
} }
export async function initializeServiceHub(): Promise<ServiceHub> { export async function initializeServiceHub(): Promise<ServiceHub> {

View File

@ -0,0 +1,9 @@
import type { RAGService } from './types'
import type { MCPTool } from '@janhq/core'
export class DefaultRAGService implements RAGService {
async getTools(): Promise<MCPTool[]> {
// Temporarily return no tools; real tools will be provided by rag-extension later
return []
}
}

View File

@ -0,0 +1,7 @@
import { MCPTool } from '@janhq/core'
export interface RAGService {
// Return tools exposed by RAG-related extensions (e.g., retrieval, list_attachments)
getTools(): Promise<MCPTool[]>
}