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
2 changes: 2 additions & 0 deletions .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ HTTP_PORT=8080
LOG_LEVEL=info
LOG_FORMAT=json
AUTO_MIGRATE=true
PREFERENCES_DEFAULT_HIDE_CONNECTORS=true
PREFERENCES_DEFAULT_HIDE_ARTIFACTS=true

# ============================================================================
# Authentication (Keycloak)
Expand Down
39 changes: 26 additions & 13 deletions apps/web/src/components/profile/profile-page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from "react";
import { useEffect, useState } from "react";
import { AppSidebar } from "@/components/sidebar/app-sidebar";
import { SidebarInset } from "@/components/sidebar/sidebar";
import { NavHeader } from "@/components/sidebar/nav-header";
Expand All @@ -7,11 +7,20 @@ import { UserTab } from "./user-tab";
import { UsageTab } from "./usage-tab";
import { ConnectorsTab } from "./connectors-tab";
import { cn } from "@/lib/utils";
import { useProfile } from "@/stores/profile-store";

export function ProfilePage() {
const [activeTab, setActiveTab] = useState<
"user" | "api-keys" | "usage" | "connectors"
>("user");
const preferences = useProfile((state) => state.preferences);
const hideConnectors = preferences?.preferences?.hide_connectors ?? false;

useEffect(() => {
if (hideConnectors && activeTab === "connectors") {
setActiveTab("user");
}
}, [hideConnectors, activeTab]);

return (
<>
Expand Down Expand Up @@ -57,24 +66,28 @@ export function ProfilePage() {
>
Usage
</button>
<button
onClick={() => setActiveTab("connectors")}
className={cn(
"whitespace-nowrap border-b-2 py-4 px-1 text-sm font-medium transition-colors",
activeTab === "connectors"
? "border-primary text-foreground"
: "border-transparent text-muted-foreground hover:border-border hover:text-foreground",
)}
>
Connectors
</button>
{!hideConnectors && (
<button
onClick={() => setActiveTab("connectors")}
className={cn(
"whitespace-nowrap border-b-2 py-4 px-1 text-sm font-medium transition-colors",
activeTab === "connectors"
? "border-primary text-foreground"
: "border-transparent text-muted-foreground hover:border-border hover:text-foreground",
)}
>
Connectors
</button>
)}
</nav>
</div>

{activeTab === "user" && <UserTab />}
{activeTab === "api-keys" && <ApiKeysTab />}
{activeTab === "usage" && <UsageTab />}
{activeTab === "connectors" && <ConnectorsTab />}
{!hideConnectors && activeTab === "connectors" && (
<ConnectorsTab />
)}
</div>
</div>
</SidebarInset>
Expand Down
24 changes: 17 additions & 7 deletions apps/web/src/components/sidebar/app-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { Jan } from "@janhq/interfaces/svgs/jan";
import { StaggeredAnimationProvider } from "@/hooks/useStaggeredFadeIn";
import { useProjects } from "@/stores/projects-store";
import { useConversations } from "@/stores/conversation-store";
import { useProfile } from "@/stores/profile-store";

export const AppSidebar = memo(function AppSidebar({
...props
Expand All @@ -28,15 +29,24 @@ export const AppSidebar = memo(function AppSidebar({
const getProjects = useProjects((state) => state.getProjects);
const getConversations = useConversations((state) => state.getConversations);
const projects = useProjects((state) => state.projects);
const fetchPreferences = useProfile((state) => state.fetchPreferences);
const preferences = useProfile((state) => state.preferences);
const [isTransitionComplete, setIsTransitionComplete] = useState(
state === "collapsed",
);

// Calculate animation indices:
// NavMain: 0, 1, 2, 3 (4 items: New Chat, New Project, Connectors, Search)
// NavProjects: starts at 4
// NavChats: starts after projects (4 + 1 label + N projects, or 4 if no projects)
const chatsStartIndex = projects.length > 0 ? 4 + 1 + projects.length : 4;
// NavMain: 0..N (New Chat, New Project, Artifacts?, Connectors?, Search)
// NavProjects: starts at navMainCount
// NavChats: starts after projects (navMainCount + 1 label + N projects, or navMainCount)
const hideConnectors = preferences?.preferences?.hide_connectors ?? false;
const hideArtifacts = preferences?.preferences?.hide_artifacts ?? false;
const navMainCount =
3 + (hideArtifacts ? 0 : 1) + (hideConnectors ? 0 : 1);
const chatsStartIndex =
projects.length > 0
? navMainCount + 1 + projects.length
: navMainCount;

useEffect(() => {
if (state === "collapsed") {
Expand All @@ -55,14 +65,14 @@ export const AppSidebar = memo(function AppSidebar({

useEffect(() => {
const loadData = async () => {
await Promise.all([getProjects(), getConversations()]);
await Promise.all([getProjects(), getConversations(), fetchPreferences()]);
// Wait for next frame to ensure store updates have propagated
requestAnimationFrame(() => {
setIsReady(true);
});
};
loadData();
}, [getProjects, getConversations]);
}, [getProjects, getConversations, fetchPreferences]);

return (
<Sidebar className="border-r-0" {...props} variant={variant}>
Expand Down Expand Up @@ -98,7 +108,7 @@ export const AppSidebar = memo(function AppSidebar({
<NavMain />
</SidebarHeader>
<SidebarContent className="mask-b-from-95% mask-t-from-98%">
<NavProjects startIndex={4} />
<NavProjects startIndex={navMainCount} />
<NavChats startIndex={chatsStartIndex} />
</SidebarContent>
<SidebarFooter className={cn(!isOpen && "pb-4")}>
Expand Down
33 changes: 22 additions & 11 deletions apps/web/src/components/sidebar/nav-main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ import { SidebarMenu, useSidebar } from "@/components/sidebar/sidebar";
import { AnimatedMenuItem, type NavMainItem } from "@/components/sidebar/items";
import { URL_PARAM, URL_PARAM_VALUE } from "@/constants";
import { cn } from "@janhq/interfaces/lib";
import { useProfile } from "@/stores/profile-store";

export function NavMain() {
const router = useRouter();
const preferences = useProfile((state) => state.preferences);
const hideConnectors = preferences?.preferences?.hide_connectors ?? false;
const hideArtifacts = preferences?.preferences?.hide_artifacts ?? false;

const handleNewProject = () => {
const url = new URL(window.location.href);
Expand All @@ -35,23 +39,30 @@ export function NavMain() {
icon: FolderPenIcon,
onClick: handleNewProject,
},
{
];

if (!hideArtifacts) {
navMain.push({
title: "Artifacts",
url: "/artifacts",
icon: LayoutGrid,
},
{
});
}

if (!hideConnectors) {
navMain.push({
title: "Connectors",
url: "/connectors",
icon: Plug,
},
{
title: "Search",
url: "#",
icon: Search,
onClick: handleSearch,
},
];
});
}

navMain.push({
title: "Search",
url: "#",
icon: Search,
onClick: handleSearch,
});

const { isMobile, setOpenMobile, state } = useSidebar();
const [isTransitionComplete, setIsTransitionComplete] = React.useState(
Expand Down
18 changes: 17 additions & 1 deletion apps/web/src/routes/artifacts/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,26 @@
import { createFileRoute } from "@tanstack/react-router";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { useEffect } from "react";
import { AppSidebar } from "@/components/sidebar/app-sidebar";
import { SidebarInset, SidebarProvider } from "@/components/sidebar/sidebar";
import { NavHeader } from "@/components/sidebar/nav-header";
import { ArtifactGallery } from "@/components/artifacts/artifact-gallery";
import { useProfile } from "@/stores/profile-store";

function ArtifactsPage() {
const navigate = useNavigate();
const preferences = useProfile((state) => state.preferences);
const hideArtifacts = preferences?.preferences?.hide_artifacts ?? false;

useEffect(() => {
if (hideArtifacts) {
navigate({ to: "/" });
}
}, [hideArtifacts, navigate]);

if (hideArtifacts) {
return null;
}

return (
<SidebarProvider>
<AppSidebar />
Expand Down
18 changes: 17 additions & 1 deletion apps/web/src/routes/connectors/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,24 @@
import { createFileRoute } from "@tanstack/react-router";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { useEffect } from "react";
import { SidebarProvider } from "@/components/sidebar/sidebar";
import { ConnectorsPage } from "@/components/connectors/connectors-page";
import { useProfile } from "@/stores/profile-store";

function ConnectorsRoute() {
const navigate = useNavigate();
const preferences = useProfile((state) => state.preferences);
const hideConnectors = preferences?.preferences?.hide_connectors ?? false;

useEffect(() => {
if (hideConnectors) {
navigate({ to: "/" });
}
}, [hideConnectors, navigate]);

if (hideConnectors) {
return null;
}

return (
<SidebarProvider>
<ConnectorsPage />
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/types/user.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ interface Preferences {
enable_image_generation: boolean;
enable_agent_mode: boolean;
selected_model: string;
hide_connectors?: boolean;
hide_artifacts?: boolean;
theme_color?: string;
theme_config?: ThemeConfig;
}
2 changes: 2 additions & 0 deletions docs/configuration/env-var-mapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ This document maps centralized configuration (`pkg/config/types.go`) environment
| `DOCLING_ENABLED` | bool | `false` | `DOCLING_ENABLED` | New |
| `DOCLING_PROVIDER_URL` | string | (empty) | `DOCLING_PROVIDER_URL` | New |
| `DOCLING_API_KEY` | string | (secret) | `DOCLING_API_KEY` | New |
| `PREFERENCES_DEFAULT_HIDE_CONNECTORS` | bool | `true` | `PREFERENCES_DEFAULT_HIDE_CONNECTORS` | New |
| `PREFERENCES_DEFAULT_HIDE_ARTIFACTS` | bool | `true` | `PREFERENCES_DEFAULT_HIDE_ARTIFACTS` | New |

**Provider Config:**
| Centralized Env Var | Type | Default | Current Var | Status |
Expand Down
2 changes: 1 addition & 1 deletion services/llm-api/cmd/server/wire_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 18 additions & 14 deletions services/llm-api/internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ type Config struct {
AutoMigrate bool `env:"AUTO_MIGRATE" envDefault:"true"`
EnableSwagger bool `env:"ENABLE_SWAGGER" envDefault:"true"`

// User preference defaults (UI visibility)
PreferencesDefaultHideConnectors bool `env:"PREFERENCES_DEFAULT_HIDE_CONNECTORS" envDefault:"true"`
PreferencesDefaultHideArtifacts bool `env:"PREFERENCES_DEFAULT_HIDE_ARTIFACTS" envDefault:"true"`

// Media integration
MediaResolveURL string `env:"MEDIA_RESOLVE_URL" envDefault:"http://media-api:8285/v1/media"`
MediaIngestURL string `env:"MEDIA_INGEST_URL" envDefault:"http://kong:8000/media/v1/media"`
Expand Down Expand Up @@ -126,20 +130,20 @@ type Config struct {
DocumentSupportedTypes string `env:"DOCUMENT_SUPPORTED_TYPES" envDefault:"application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.openxmlformats-officedocument.presentationml.presentation,text/plain,text/markdown,text/html,application/rtf"`

// Connectors (OAuth integrations)
GitHubClientID string `env:"GITHUB_CLIENT_ID"`
GitHubClientSecret string `env:"GITHUB_CLIENT_SECRET"`
GitHubConnectorEnabled bool `env:"GITHUB_CONNECTOR_ENABLED" envDefault:"false"`
GoogleClientID string `env:"GOOGLE_CLIENT_ID"`
GoogleClientSecret string `env:"GOOGLE_CLIENT_SECRET"`
GoogleConnectorEnabled bool `env:"GOOGLE_CONNECTOR_ENABLED" envDefault:"false"`
ConnectorTokenEncryptionKey string `env:"CONNECTOR_TOKEN_ENCRYPTION_KEY"`
ConnectorTokenEncryptionKeyID string `env:"CONNECTOR_TOKEN_ENCRYPTION_KEY_ID" envDefault:"v1"`
ConnectorTokenEncryptionKeyPrevious string `env:"CONNECTOR_TOKEN_ENCRYPTION_KEY_PREVIOUS"`
ConnectorTokenEncryptionKeyIDPrevious string `env:"CONNECTOR_TOKEN_ENCRYPTION_KEY_ID_PREVIOUS"`
OAuthStateSecret string `env:"OAUTH_STATE_SECRET"`
OAuthStateExpiration time.Duration `env:"OAUTH_STATE_EXPIRATION" envDefault:"5m"`
OAuthRedirectBaseURL string `env:"OAUTH_REDIRECT_BASE_URL" envDefault:"http://localhost:8000"`
OAuthFrontendURL string `env:"OAUTH_FRONTEND_URL" envDefault:"http://localhost:3001"`
GitHubClientID string `env:"GITHUB_CLIENT_ID"`
GitHubClientSecret string `env:"GITHUB_CLIENT_SECRET"`
GitHubConnectorEnabled bool `env:"GITHUB_CONNECTOR_ENABLED" envDefault:"false"`
GoogleClientID string `env:"GOOGLE_CLIENT_ID"`
GoogleClientSecret string `env:"GOOGLE_CLIENT_SECRET"`
GoogleConnectorEnabled bool `env:"GOOGLE_CONNECTOR_ENABLED" envDefault:"false"`
ConnectorTokenEncryptionKey string `env:"CONNECTOR_TOKEN_ENCRYPTION_KEY"`
ConnectorTokenEncryptionKeyID string `env:"CONNECTOR_TOKEN_ENCRYPTION_KEY_ID" envDefault:"v1"`
ConnectorTokenEncryptionKeyPrevious string `env:"CONNECTOR_TOKEN_ENCRYPTION_KEY_PREVIOUS"`
ConnectorTokenEncryptionKeyIDPrevious string `env:"CONNECTOR_TOKEN_ENCRYPTION_KEY_ID_PREVIOUS"`
OAuthStateSecret string `env:"OAUTH_STATE_SECRET"`
OAuthStateExpiration time.Duration `env:"OAUTH_STATE_EXPIRATION" envDefault:"5m"`
OAuthRedirectBaseURL string `env:"OAUTH_REDIRECT_BASE_URL" envDefault:"http://localhost:8000"`
OAuthFrontendURL string `env:"OAUTH_FRONTEND_URL" envDefault:"http://localhost:3001"`

// Analytics (PostHog + OTel)
AnalyticsEnabled bool `env:"ANALYTICS_ENABLED" envDefault:"true"`
Expand Down
23 changes: 21 additions & 2 deletions services/llm-api/internal/domain/usersettings/user_settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"context"
"encoding/json"
"time"

"jan-server/services/llm-api/internal/config"
)

// UserSettings represents user preferences and feature toggles.
Expand Down Expand Up @@ -116,6 +118,8 @@ func DefaultPreferences() map[string]interface{} {
"enable_thinking": false,
"enable_search": true,
"enable_browser": false,
"hide_connectors": true,
"hide_artifacts": true,
}
}

Expand Down Expand Up @@ -212,14 +216,28 @@ type ModelProvider interface {
type Service struct {
repo Repository
modelProvider ModelProvider
cfg *config.Config
}

// NewService constructs a Service with required dependencies.
func NewService(repo Repository, modelProvider ModelProvider) *Service {
func NewService(repo Repository, modelProvider ModelProvider, cfg *config.Config) *Service {
return &Service{
repo: repo,
modelProvider: modelProvider,
cfg: cfg,
}
}

func (s *Service) defaultPreferences() map[string]interface{} {
defaults := DefaultPreferences()
if s == nil || s.cfg == nil {
return defaults
}

defaults["hide_connectors"] = s.cfg.PreferencesDefaultHideConnectors
defaults["hide_artifacts"] = s.cfg.PreferencesDefaultHideArtifacts

return defaults
}

// GetOrCreateSettings retrieves existing settings or creates defaults for a user.
Expand All @@ -232,6 +250,7 @@ func (s *Service) GetOrCreateSettings(ctx context.Context, userID uint) (*UserSe
// Create default settings if none exist
if settings == nil {
defaults := DefaultUserSettings(userID)
defaults.Preferences = s.defaultPreferences()

// Set default selected_model from first active model
if s.modelProvider != nil {
Expand All @@ -256,7 +275,7 @@ func (s *Service) ensureDefaultPreferences(ctx context.Context, settings *UserSe
settings.Preferences = make(map[string]interface{})
}

defaults := DefaultPreferences()
defaults := s.defaultPreferences()

// Fill in missing preference keys with defaults
for key, defaultValue := range defaults {
Expand Down
Loading