diff --git a/nginx.conf b/nginx.conf index ba83fc9..3b41585 100644 --- a/nginx.conf +++ b/nginx.conf @@ -18,7 +18,9 @@ server { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header Connection ""; + # Variable proxy_pass plus keepalive reuses a socket that is still + # writing SSE. The next chat is written onto that socket and both die. + proxy_set_header Connection "close"; proxy_buffering off; proxy_cache off; proxy_read_timeout 3600s; diff --git a/public/brands/didar.png b/public/brands/didar.png new file mode 100644 index 0000000..b619abd Binary files /dev/null and b/public/brands/didar.png differ diff --git a/src/app/nav.test.ts b/src/app/nav.test.ts index 0d8bc57..504634e 100644 --- a/src/app/nav.test.ts +++ b/src/app/nav.test.ts @@ -4,7 +4,12 @@ import { isNavActive, pageTitleFromPath, PRIMARY_NAV } from './nav' describe('app navigation', () => { it('names primary surfaces including the skills catalog', () => { expect(pageTitleFromPath('/assistant')).toBe('دستیار هوشمند') - expect(pageTitleFromPath('/databases')).toBe('پایگاه داده') + expect(pageTitleFromPath('/data')).toBe('داشبورد داده') + expect(pageTitleFromPath('/data/files')).toBe('فایل‌ها') + expect(pageTitleFromPath('/data/databases')).toBe('دیتابیس') + expect(pageTitleFromPath('/data/connectors')).toBe('اتصالات') + expect(pageTitleFromPath('/data/map')).toBe('نقشه سازمان') + expect(pageTitleFromPath('/databases')).toBe('دیتابیس') expect(pageTitleFromPath('/knowledge')).toBe('دانش') expect(pageTitleFromPath('/skills')).toBe('مهارت‌ها') expect(pageTitleFromPath('/settings')).toBe('تنظیمات') @@ -14,4 +19,15 @@ describe('app navigation', () => { expect(PRIMARY_NAV.some((item) => item.id === 'skills' && item.to === '/skills')).toBe(true) expect(isNavActive('/skills', { to: '/skills', match: ['/skills'] })).toBe(true) }) + + it('keeps sources, connections, and the data map under one داده item', () => { + const data = PRIMARY_NAV.find((item) => item.id === 'data') + expect(data?.to).toBe('/data') + expect(data && isNavActive('/data', data)).toBe(true) + expect(data && isNavActive('/data/files', data)).toBe(true) + expect(data && isNavActive('/data/databases', data)).toBe(true) + expect(data && isNavActive('/data/connectors', data)).toBe(true) + expect(data && isNavActive('/data/map', data)).toBe(true) + expect(data && isNavActive('/data-map', data)).toBe(true) + }) }) diff --git a/src/app/nav.ts b/src/app/nav.ts index c5931f9..fa51bc8 100644 --- a/src/app/nav.ts +++ b/src/app/nav.ts @@ -2,7 +2,7 @@ export const SIDEBAR_EXPANDED = 260 export const SIDEBAR_COLLAPSED = 52 export const SIDEBAR_COLLAPSED_STORAGE_KEY = 'kamidso.sidebarCollapsed' -export type NavId = 'assistant' | 'data-map' | 'databases' | 'knowledge' | 'skills' | 'settings' +export type NavId = 'assistant' | 'data' | 'knowledge' | 'skills' | 'settings' export type NavItem = { id: NavId @@ -14,8 +14,7 @@ export type NavItem = { export const PRIMARY_NAV: NavItem[] = [ { id: 'assistant', to: '/assistant', label: 'دستیار هوشمند', match: ['/assistant'] }, - { id: 'data-map', to: '/data-map', label: 'نقشه داده', match: ['/data-map'] }, - { id: 'databases', to: '/databases', label: 'پایگاه داده', match: ['/databases'] }, + { id: 'data', to: '/data', label: 'داده', match: ['/data', '/data-map', '/databases'] }, { id: 'knowledge', to: '/knowledge', label: 'دانش', match: ['/knowledge'] }, { id: 'skills', to: '/skills', label: 'مهارت‌ها', match: ['/skills'] }, ] @@ -29,8 +28,11 @@ export const SETTINGS_NAV: NavItem = { export function pageTitleFromPath(pathname: string): string { if (pathname === '/home' || pathname.startsWith('/assistant')) return 'دستیار هوشمند' - if (pathname.startsWith('/data-map')) return 'نقشه داده' - if (pathname.startsWith('/databases')) return 'پایگاه داده' + if (pathname.startsWith('/data/map') || pathname.startsWith('/data-map')) return 'نقشه سازمان' + if (pathname.startsWith('/data/files') || pathname.startsWith('/data/sources')) return 'فایل‌ها' + if (pathname.startsWith('/data/databases') || pathname.startsWith('/data/connections') || pathname.startsWith('/databases')) return 'دیتابیس' + if (pathname.startsWith('/data/connectors') || pathname.startsWith('/settings/connectors')) return 'اتصالات' + if (pathname.startsWith('/data')) return 'داشبورد داده' if (pathname.startsWith('/knowledge')) return 'دانش' if (pathname.startsWith('/skills')) return 'مهارت‌ها' if (pathname.startsWith('/settings')) return 'تنظیمات' diff --git a/src/app/providers/FeatureFlagProvider.tsx b/src/app/providers/FeatureFlagProvider.tsx index 6f4021b..6392ae1 100644 --- a/src/app/providers/FeatureFlagProvider.tsx +++ b/src/app/providers/FeatureFlagProvider.tsx @@ -23,6 +23,7 @@ export type FeatureFlags = { widgets: boolean radar: boolean new_agent_workspace: boolean + connectors: boolean } const DEFAULT_FLAGS: FeatureFlags = { @@ -38,6 +39,7 @@ const DEFAULT_FLAGS: FeatureFlags = { widgets: false, radar: false, new_agent_workspace: true, + connectors: false, } type FeatureFlagContextValue = { @@ -101,3 +103,7 @@ export function useFeatureFlags() { if (!ctx) throw new Error('useFeatureFlags must be used within FeatureFlagProvider') return ctx } + +export function useFeatureFlagsOptional() { + return useContext(FeatureFlagContext) +} diff --git a/src/app/router.tsx b/src/app/router.tsx index 975db78..9f6cae3 100644 --- a/src/app/router.tsx +++ b/src/app/router.tsx @@ -1,14 +1,18 @@ import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom' import { AppShell } from '../components/layout/AppShell' import { AssistantPage } from '../pages/AssistantPage' -import { DatabasesPage } from '../pages/DatabasesPage' import { DataMapPage } from '../features/data-map/DataMapPage' +import { ConnectionsPage } from '../features/data/ConnectionsPage' +import { DataDashboardPage } from '../features/data/DataDashboardPage' +import { DataHubLayout } from '../features/data/DataHubLayout' +import { SourcesPage } from '../features/data/SourcesPage' import { KnowledgePage } from '../pages/KnowledgePage' import { KnowledgeDetailPage } from '../pages/KnowledgeDetailPage' import { SkillsPage } from '../pages/SkillsPage' import { SettingsPage } from '../pages/SettingsPage' import { OrganizationProfilePage } from '../features/settings/OrganizationProfilePage' import { SkillSettingsPage } from '../features/settings/SkillSettingsPage' +import { ConnectorsPage } from '../features/connectors/ConnectorsPage' import { LandingPage } from '../features/landing/pages/LandingPage' import { ShareReportPage } from '../pages/ShareReportPage' import { AuthProvider } from '../features/auth/AuthContext' @@ -37,13 +41,23 @@ export function AppRouter() { } /> } /> - } /> - } /> + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + } /> + } /> } /> } /> } /> } /> } /> + } /> } /> } /> } /> diff --git a/src/components/layout/AppSidebar.tsx b/src/components/layout/AppSidebar.tsx index d4cb6a3..58a4703 100644 --- a/src/components/layout/AppSidebar.tsx +++ b/src/components/layout/AppSidebar.tsx @@ -3,7 +3,6 @@ import { useCallback, useEffect, useRef } from 'react' import { BookOpen, Database, - Network, Loader2, LogOut, MessageSquarePlus, @@ -19,8 +18,7 @@ import { useLogoutConfirm } from '../../features/auth/useLogoutConfirm' const ICONS = { assistant: Sparkles, - 'data-map': Network, - databases: Database, + data: Database, knowledge: BookOpen, skills: WandSparkles, } as const @@ -117,7 +115,10 @@ export function AppSidebar({ ) : null} { + event.preventDefault() + goNewChat() + }} className={`flex min-w-0 items-center gap-2.5 rounded-xl px-1 py-1 ${compact ? 'justify-center' : 'flex-1'}`} > diff --git a/src/components/layout/MobileNav.tsx b/src/components/layout/MobileNav.tsx index 414116d..667f6f2 100644 --- a/src/components/layout/MobileNav.tsx +++ b/src/components/layout/MobileNav.tsx @@ -1,25 +1,24 @@ -import { BookOpen, Database, House, Network, Settings, Sparkles, WandSparkles } from 'lucide-react' +import { BookOpen, Database, House, Settings, Sparkles, WandSparkles } from 'lucide-react' import { NavLink, useLocation } from 'react-router-dom' import { isNavActive, PRIMARY_NAV } from '../../app/nav' const ICONS = { home: House, assistant: Sparkles, - 'data-map': Network, - databases: Database, + data: Database, knowledge: BookOpen, skills: WandSparkles, settings: Settings, } as const -const MOBILE_IDS = ['assistant', 'data-map', 'databases', 'knowledge', 'skills'] as const +const MOBILE_IDS = ['assistant', 'data', 'knowledge', 'skills'] as const export function MobileNav() { const { pathname } = useLocation() const items = PRIMARY_NAV.filter((item) => MOBILE_IDS.includes(item.id as (typeof MOBILE_IDS)[number])) return ( -