Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { userIdFromExtensionToken } from "@/lib/auth/extension-token";
import { createChatMessage, CreateChatMessageParams } from "@/lib/workspace/workspace";
import { getUserSettings } from "@/lib/auth/user";
import { NextRequest, NextResponse } from "next/server";


Expand Down Expand Up @@ -28,8 +29,12 @@ export async function POST(req: NextRequest) {
const body = await req.json();
const { prompt } = body;

// Get user settings to access SecureBuild preference
const userSettings = await getUserSettings(userId);

const createChatMessageParams: CreateChatMessageParams = {
prompt,
useSecureBuildImages: userSettings.useSecureBuildImages,
};

const chatMessage = await createChatMessage(userId, workspaceId, createChatMessageParams);
Expand Down
19 changes: 19 additions & 0 deletions chartsmith-app/app/hooks/useSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,27 @@ export const useSession = (redirectIfNotLoggedIn: boolean = false) => {
validate(token);
}, [router, redirectIfNotLoggedIn]);

const refreshSession = useCallback(async () => {
const token = document.cookie
.split("; ")
.find((cookie) => cookie.startsWith("session="))
?.split("=")[1];

if (!token) {
return;
}

try {
const sess = await validateSession(token);
setSession(sess);
} catch (error) {
logger.error("Session refresh failed:", error);
}
}, []);

return {
isLoading,
session,
refreshSession,
};
};
262 changes: 231 additions & 31 deletions chartsmith-app/components/SettingsModal.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client"

import React, { useState, useEffect } from 'react';
import { X, Trash2, Key, Check, Loader2 } from 'lucide-react';
import { X, Trash2, Key, Check, Loader2, Lock, Shield } from 'lucide-react';
import { useTheme, Theme } from '@/contexts/ThemeContext';
import { useAuth } from '@/contexts/AuthContext';
import { updateUserSettingAction } from '@/lib/auth/actions/update-user-setting';
Expand All @@ -15,22 +15,31 @@ interface SettingsModalProps {
}

interface SettingsSection {
id: 'general' | 'replicated' | 'appearance' | 'editor' | 'changes';
id: 'general' | 'replicated' | 'appearance' | 'editor' | 'changes' | 'images';
label: string;
icon: React.ReactNode;
content: React.ReactNode;
}

export function SettingsModal({ isOpen, onClose, session }: SettingsModalProps) {
const { refreshSession } = useSession();
const { theme, setTheme } = useTheme();
const [activeSection, setActiveSection] = useState<'general' | 'replicated' | 'appearance' | 'editor' | 'changes'>('general');
const [autoAcceptChanges, setAutoAcceptChanges] = useState(session.user?.settings?.automaticallyAcceptPatches || false);
const [validateBeforeAccept, setValidateBeforeAccept] = useState(session.user?.settings?.evalBeforeAccept || false);
const [activeSection, setActiveSection] = useState<'general' | 'replicated' | 'appearance' | 'editor' | 'changes' | 'images'>('general');
const [autoAcceptChanges, setAutoAcceptChanges] = useState(session.user?.settings?.automaticallyAcceptPatches ?? false);
const [validateBeforeAccept, setValidateBeforeAccept] = useState(session.user?.settings?.evalBeforeAccept ?? false);
const [isDeleting, setIsDeleting] = useState(false);
const [apiToken, setApiToken] = useState('');
const [savingAutoAccept, setSavingAutoAccept] = useState(false);
const [savingValidate, setSavingValidate] = useState(false);
const [showMinimap, setShowMinimap] = useState(session.user?.settings?.showMinimap ?? false);
const [tabSize, setTabSize] = useState(session.user?.settings?.tabSize ?? '2 spaces');
const [localTheme, setLocalTheme] = useState(session.user?.settings?.theme ?? 'auto');
const [savingMinimap, setSavingMinimap] = useState(false);
const [savingTabSize, setSavingTabSize] = useState(false);
const [isChangingTheme, setIsChangingTheme] = useState(false);
const [publicEnv, setPublicEnv] = useState<Record<string, string>>({});
const [useSecureBuildImages, setUseSecureBuildImages] = useState(session.user?.settings?.useSecureBuildImages ?? false);
const [savingSecureBuildImages, setSavingSecureBuildImages] = useState(false);

useEffect(() => {
const fetchConfig = async () => {
Expand All @@ -49,11 +58,33 @@ export function SettingsModal({ isOpen, onClose, session }: SettingsModalProps)

useEffect(() => {
if (session.user?.settings) {
setAutoAcceptChanges(session.user.settings.automaticallyAcceptPatches);
setValidateBeforeAccept(session.user.settings.evalBeforeAccept);
setAutoAcceptChanges(session.user.settings.automaticallyAcceptPatches ?? false);
setValidateBeforeAccept(session.user.settings.evalBeforeAccept ?? false);
setShowMinimap(session.user.settings.showMinimap ?? false);
setTabSize(session.user.settings.tabSize ?? '2 spaces');
setUseSecureBuildImages(session.user.settings.useSecureBuildImages ?? false);

// Only sync theme from database if there's a significant difference and we're not changing themes
// Remove automatic sync to prevent hydration conflicts - let user manually change theme
const dbTheme = session.user.settings.theme ?? 'auto';
setLocalTheme(dbTheme);
}
}, [session.user?.settings]);

// Sync localTheme only when modal opens (isOpen changes from false to true)
useEffect(() => {
if (isOpen && session.user?.settings) {
const dbTheme = session.user.settings.theme ?? 'auto';
console.log('Modal opened, syncing localTheme to:', dbTheme);
setLocalTheme(dbTheme);
}
}, [isOpen, session.user?.settings?.theme]);

// Debug localTheme changes
useEffect(() => {
console.log('localTheme state changed to:', localTheme);
}, [localTheme]);

if (!isOpen) return null;

const handleDeleteChats = () => {
Expand Down Expand Up @@ -83,26 +114,136 @@ export function SettingsModal({ isOpen, onClose, session }: SettingsModalProps)

const handleAutoAcceptChange = async (checked: boolean) => {
if (!session.user) return;
console.log('handleAutoAcceptChange called', { checked, currentValue: session.user.settings.automaticallyAcceptPatches });
setSavingAutoAccept(true);
try {
setAutoAcceptChanges(checked);
await updateUserSettingAction(session, 'automatically_accept_patches', checked.toString());
console.log('About to save automatically_accept_patches to database');
const result = await updateUserSettingAction(session, 'automatically_accept_patches', checked.toString());
console.log('Database save result:', result);
// Update session locally to reflect the change immediately
session.user.settings.automaticallyAcceptPatches = checked;
console.log('Updated local session state:', session.user.settings.automaticallyAcceptPatches);
} finally {
setSavingAutoAccept(false);
}
};

const handleValidateBeforeAcceptChange = async (checked: boolean) => {
if (!session.user) return;
console.log('handleValidateBeforeAcceptChange called', { checked, currentValue: session.user.settings.evalBeforeAccept });
setSavingValidate(true);
try {
setValidateBeforeAccept(checked);
await updateUserSettingAction(session, 'eval_before_accept', checked.toString());
console.log('About to save eval_before_accept to database');
const result = await updateUserSettingAction(session, 'eval_before_accept', checked.toString());
console.log('Database save result:', result);
// Update session locally to reflect the change immediately
session.user.settings.evalBeforeAccept = checked;
console.log('Updated local session state:', session.user.settings.evalBeforeAccept);
} finally {
setSavingValidate(false);
}
};


const handleThemeChange = async (newTheme: string) => {
if (!session.user) return;
console.log('handleThemeChange called', {
newTheme,
currentValue: session.user.settings.theme,
currentLocalTheme: localTheme,
currentContextTheme: theme
});
try {
// Set flag to prevent automatic theme sync from interfering
setIsChangingTheme(true);

// Update local theme state immediately for UI responsiveness
console.log('Setting localTheme to:', newTheme);
setLocalTheme(newTheme);

// Update theme context (this will also set the cookie)
console.log('Setting theme context to:', newTheme);
setTheme(newTheme as Theme);

console.log('About to save theme to database');
const result = await updateUserSettingAction(session, 'theme', newTheme);
console.log('Database save result:', result);

// Update session locally with the result from database
if (result.theme) {
session.user.settings.theme = result.theme;
console.log('Updated local session state:', session.user.settings.theme);
}

console.log('After all updates:', {
localTheme,
contextTheme: theme,
sessionTheme: session.user.settings.theme
});
} catch (error) {
console.error('Failed to save theme:', error);
// Revert both local theme and context theme on error
setLocalTheme(session.user.settings.theme);
setTheme(session.user.settings.theme as Theme);
} finally {
// Clear the flag after a short delay to allow state updates to settle
setTimeout(() => setIsChangingTheme(false), 500);
}
};

const handleTabSizeChange = async (newTabSize: string) => {
if (!session.user) return;
console.log('handleTabSizeChange called', { newTabSize, currentValue: session.user.settings.tabSize });
setSavingTabSize(true);
try {
setTabSize(newTabSize);
console.log('About to save tab_size to database');
const result = await updateUserSettingAction(session, 'tab_size', newTabSize);
console.log('Database save result:', result);
// Update session locally to reflect the change immediately
session.user.settings.tabSize = newTabSize;
console.log('Updated local session state:', session.user.settings.tabSize);
} finally {
setSavingTabSize(false);
}
};

const handleMinimapChange = async (checked: boolean) => {
if (!session.user) return;
console.log('handleMinimapChange called', { checked, currentValue: session.user.settings.showMinimap });
setSavingMinimap(true);
try {
setShowMinimap(checked);
console.log('About to save show_minimap to database');
const result = await updateUserSettingAction(session, 'show_minimap', checked.toString());
console.log('Database save result:', result);
// Update session locally to reflect the change immediately
session.user.settings.showMinimap = checked;
console.log('Updated local session state:', session.user.settings.showMinimap);
} finally {
setSavingMinimap(false);
}
};

const handleSecureBuildImagesChange = async (checked: boolean) => {
if (!session.user) return;
console.log('handleSecureBuildImagesChange called', { checked, currentValue: session.user.settings.useSecureBuildImages });
setSavingSecureBuildImages(true);
try {
setUseSecureBuildImages(checked);
console.log('About to save use_secure_build_images to database');
const result = await updateUserSettingAction(session, 'use_secure_build_images', checked.toString());
console.log('Database save result:', result);
// Update session locally to reflect the change immediately
session.user.settings.useSecureBuildImages = checked;
console.log('Updated local session state:', session.user.settings.useSecureBuildImages);
} finally {
setSavingSecureBuildImages(false);
}
};

const sections: SettingsSection[] = [
{
id: 'general',
Expand Down Expand Up @@ -199,9 +340,16 @@ export function SettingsModal({ isOpen, onClose, session }: SettingsModalProps)
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Theme
</label> <select
value={theme}
onChange={(e) => setTheme(e.target.value as Theme)}
</label>
<select
value={localTheme}
onChange={(e) => {
console.log('Select onChange triggered:', {
selectedValue: e.target.value,
currentLocalTheme: localTheme
});
handleThemeChange(e.target.value);
}}
className={`w-full px-3 py-2 rounded-lg transition-colors ${
theme === 'dark'
? 'bg-dark border-dark-border text-gray-300'
Expand Down Expand Up @@ -229,26 +377,42 @@ export function SettingsModal({ isOpen, onClose, session }: SettingsModalProps)
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Tab Size
</label>
<select className={`w-full px-3 py-2 rounded-lg transition-colors ${
theme === 'dark'
? 'bg-dark border-dark-border text-gray-300'
: 'bg-white border-gray-300 text-gray-900'
} border focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent`}>
<option>2 spaces</option>
<option>4 spaces</option>
<option>8 spaces</option>
</select>
{savingTabSize ? (
<div className="flex items-center gap-2">
<Loader2 className="w-4 h-4 animate-spin text-primary" />
<span className="text-sm text-gray-500">Saving...</span>
</div>
) : (
<select
value={tabSize}
onChange={(e) => handleTabSizeChange(e.target.value)}
className={`w-full px-3 py-2 rounded-lg transition-colors ${
theme === 'dark'
? 'bg-dark border-dark-border text-gray-300'
: 'bg-white border-gray-300 text-gray-900'
} border focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent`}>
<option value="2 spaces">2 spaces</option>
<option value="4 spaces">4 spaces</option>
<option value="8 spaces">8 spaces</option>
</select>
)}
</div>
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="minimap"
className={`rounded border transition-colors ${
theme === 'dark'
? 'border-dark-border bg-dark text-primary'
: 'border-gray-300 bg-white text-primary'
} focus:ring-primary`}
/>
{savingMinimap ? (
<Loader2 className="w-4 h-4 animate-spin text-primary" />
) : (
<input
type="checkbox"
id="minimap"
checked={showMinimap}
onChange={(e) => handleMinimapChange(e.target.checked)}
className={`rounded border transition-colors ${
theme === 'dark'
? 'border-dark-border bg-dark text-primary'
: 'border-gray-300 bg-white text-primary'
} focus:ring-primary`}
/>
)}
<label htmlFor="minimap" className={`text-sm ${
theme === 'dark' ? 'text-gray-300' : 'text-gray-700'
}`}>
Expand Down Expand Up @@ -317,7 +481,43 @@ export function SettingsModal({ isOpen, onClose, session }: SettingsModalProps)
</div>
</div>
),
}
},
{
id: 'images' as const,
label: 'Images',
icon: <Shield className="w-4 h-4" />,
content: (
<div className="space-y-4">
<div className="flex items-center space-x-2">
{savingSecureBuildImages ? (
<Loader2 className="w-4 h-4 animate-spin text-primary" />
) : (
<input
type="checkbox"
id="secure-build-images"
checked={useSecureBuildImages}
onChange={(e) => handleSecureBuildImagesChange(e.target.checked)}
className={`rounded border transition-colors ${
theme === 'dark'
? 'border-dark-border bg-dark text-primary'
: 'border-gray-300 bg-white text-primary'
} focus:ring-primary`}
/>
)}
<label htmlFor="secure-build-images" className={`text-sm ${
theme === 'dark' ? 'text-gray-300' : 'text-gray-700'
}`}>
Use SecureBuild images (cve0.io)
</label>
</div>
<p className={`text-xs ${
theme === 'dark' ? 'text-gray-400' : 'text-gray-500'
}`}>
When enabled, ChartSmith will prefer container images from the SecureBuild cve0.io registry, which provides vulnerability-free container images for enhanced security.
</p>
</div>
),
},
];

return (
Expand Down
Loading