+
{isHighRisk ? (
<>
- ⚠️ WARNING: This operation may be irreversible and will permanently modify your database.
+ ⚠️ WARNING: This operation may be irreversible and will permanently modify your database.
>
) : (
<>This operation will make changes to your database. Please review carefully before confirming.>
@@ -88,7 +100,7 @@ const ChatMessage = ({ type, content, steps, queryData, analysisInfo, confirmati
Cancel
@@ -96,7 +108,7 @@ const ChatMessage = ({ type, content, steps, queryData, analysisInfo, confirmati
Confirm {operationType}
@@ -116,15 +128,15 @@ const ChatMessage = ({ type, content, steps, queryData, analysisInfo, confirmati
-
+
- {content}
+ {content}
-
+
-
+
{(user?.name || user?.email || 'U').charAt(0).toUpperCase()}
@@ -141,46 +153,61 @@ const ChatMessage = ({ type, content, steps, queryData, analysisInfo, confirmati
-
+
QW
-
+
-
-
+
+
{hasSQL ? 'Generated SQL Query' : 'Query Analysis'}
{hasSQL && (
-
- {content}
-
+
+
+ {copied ? (
+
+ ) : (
+
+ )}
+
+
+ {content}
+
+
)}
{!isValid && (
{analysisInfo?.explanation && (
-
-
Explanation:
-
{analysisInfo.explanation}
+
+
Explanation:
+
{analysisInfo.explanation}
)}
{analysisInfo?.missing && (
-
-
Missing Information:
-
{analysisInfo.missing}
+
+
Missing Information:
+
{analysisInfo.missing}
)}
{analysisInfo?.ambiguities && (
-
-
Ambiguities:
-
{analysisInfo.ambiguities}
+
+
Ambiguities:
+
{analysisInfo.ambiguities}
)}
@@ -198,28 +225,28 @@ const ChatMessage = ({ type, content, steps, queryData, analysisInfo, confirmati
-
+
QW
-
+
-
- Query Results
+
+ Query Results
{queryData?.length || 0} rows
{queryData && queryData.length > 0 && (
-
+
-
-
+
+
{Object.keys(queryData[0]).map((column) => (
-
+
{column}
))}
@@ -227,9 +254,9 @@ const ChatMessage = ({ type, content, steps, queryData, analysisInfo, confirmati
{queryData.map((row, index) => (
-
+
{Object.values(row).map((value: any, cellIndex) => (
-
+
{String(value)}
))}
@@ -253,12 +280,12 @@ const ChatMessage = ({ type, content, steps, queryData, analysisInfo, confirmati
-
+
QW
-
@@ -272,21 +299,21 @@ const ChatMessage = ({ type, content, steps, queryData, analysisInfo, confirmati
-
+
QW
-
+
{steps?.map((step, index) => (
-
-
- {step.icon === 'search' && }
- {step.icon === 'database' && }
- {step.icon === 'code' && }
- {step.icon === 'message' && }
+
+
+ {step.icon === 'search' && }
+ {step.icon === 'database' && }
+ {step.icon === 'code' && }
+ {step.icon === 'message' && }
{step.text}
@@ -294,7 +321,7 @@ const ChatMessage = ({ type, content, steps, queryData, analysisInfo, confirmati
{progress !== undefined && (
-
{progress}% complete
+
{progress}% complete
)}
diff --git a/app/src/components/chat/QueryInput.tsx b/app/src/components/chat/QueryInput.tsx
index 670dd20b..f38cdda6 100644
--- a/app/src/components/chat/QueryInput.tsx
+++ b/app/src/components/chat/QueryInput.tsx
@@ -27,7 +27,7 @@ const QueryInput = ({ onSubmit, placeholder = "Ask me anything about your databa
onChange={(e) => setQuery(e.target.value)}
placeholder={placeholder}
disabled={disabled}
- className="min-h-[60px] bg-gray-800 border-gray-600 text-gray-200 placeholder-gray-500 resize-none pr-12 focus:border-purple-500 focus:ring-purple-500/20 disabled:opacity-50 disabled:cursor-not-allowed"
+ className="min-h-[60px] bg-card border-border text-foreground placeholder-muted-foreground resize-none pr-12 focus-visible:border-purple-500 focus-visible:ring-purple-500 disabled:opacity-50 disabled:cursor-not-allowed"
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey && !disabled) {
e.preventDefault();
diff --git a/app/src/components/layout/Header.tsx b/app/src/components/layout/Header.tsx
index bfd2fffa..7c08dff3 100644
--- a/app/src/components/layout/Header.tsx
+++ b/app/src/components/layout/Header.tsx
@@ -85,7 +85,7 @@ const Header = ({ onConnectDatabase, onUploadSchema }: HeaderProps) => {
href="https://github.com/FalkorDB/QueryWeaver"
target="_blank"
rel="noopener noreferrer"
- className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-gray-800 hover:bg-gray-700 transition-colors text-gray-300 hover:text-white"
+ className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-card hover:bg-muted transition-colors text-muted-foreground hover:text-foreground"
title="View QueryWeaver on GitHub"
>
void;
+ onSettingsClick?: () => void;
}
const SidebarIcon = ({ icon: Icon, label, active, onClick, href, testId }: {
@@ -42,7 +44,7 @@ const SidebarIcon = ({ icon: Icon, label, active, onClick, href, testId }: {
className={`flex h-10 w-10 items-center justify-center rounded-lg transition-colors ${
active
? 'bg-purple-600 text-white'
- : 'text-gray-400 hover:bg-gray-800 hover:text-white'
+ : 'text-muted-foreground hover:bg-card hover:text-foreground'
}`}
data-testid={testId}
>
@@ -57,7 +59,7 @@ const SidebarIcon = ({ icon: Icon, label, active, onClick, href, testId }: {
className={`flex h-10 w-10 items-center justify-center rounded-lg transition-colors ${
active
? 'bg-purple-600 text-white'
- : 'text-gray-400 hover:bg-gray-800 hover:text-white'
+ : 'text-muted-foreground hover:bg-card hover:text-foreground'
}`}
data-testid={testId}
>
@@ -70,7 +72,7 @@ const SidebarIcon = ({ icon: Icon, label, active, onClick, href, testId }: {
className={`flex h-10 w-10 items-center justify-center rounded-lg transition-colors ${
active
? 'bg-purple-600 text-white'
- : 'text-gray-400 hover:bg-gray-800 hover:text-white'
+ : 'text-muted-foreground hover:bg-card hover:text-foreground'
}`}
data-testid={testId}
>
@@ -85,12 +87,28 @@ const SidebarIcon = ({ icon: Icon, label, active, onClick, href, testId }: {
);
-const Sidebar = ({ className, onSchemaClick, isSchemaOpen, isCollapsed = false, onToggleCollapse }: SidebarProps) => {
+const Sidebar = ({ className, onSchemaClick, isSchemaOpen, isCollapsed = false, onToggleCollapse, onSettingsClick }: SidebarProps) => {
const isMobile = useIsMobile();
+ const navigate = useNavigate();
+ const location = useLocation();
+
+ const isSettingsOpen = location.pathname === '/settings';
+
+ const handleSettingsClick = () => {
+ if (onSettingsClick) {
+ onSettingsClick();
+ }
+ if (isSettingsOpen) {
+ navigate('/');
+ } else {
+ navigate('/settings');
+ }
+ };
+
return (
<>
@@ -119,13 +137,13 @@ const Sidebar = ({ className, onSchemaClick, isSchemaOpen, isCollapsed = false,
-
+
+
- {/* */}
>
diff --git a/app/src/components/modals/DatabaseModal.tsx b/app/src/components/modals/DatabaseModal.tsx
index e02e6e3d..5442cf61 100644
--- a/app/src/components/modals/DatabaseModal.tsx
+++ b/app/src/components/modals/DatabaseModal.tsx
@@ -107,7 +107,27 @@ const DatabaseModal = ({ open, onOpenChange }: DatabaseModalProps) => {
});
if (!response.ok) {
- throw new Error(`Network response was not ok (${response.status})`);
+ // Try to parse error message from server for all error responses
+ try {
+ const errorData = await response.json();
+ if (errorData.error) {
+ throw new Error(errorData.error);
+ }
+ } catch (jsonError) {
+ // If JSON parsing fails, fall back to status-based messages
+ }
+
+ // Fallback error messages by status code
+ const errorMessages: Record = {
+ 400: 'Invalid database connection URL.',
+ 401: 'Not authenticated. Please sign in to connect databases.',
+ 403: 'Access denied. You do not have permission to connect databases.',
+ 409: 'Conflict with existing database connection.',
+ 422: 'Invalid database connection parameters.',
+ 500: 'Server error. Please try again later.',
+ };
+
+ throw new Error(errorMessages[response.status] || `Failed to connect to database (${response.status})`);
}
// Process streaming response
@@ -217,7 +237,15 @@ const DatabaseModal = ({ open, onOpenChange }: DatabaseModalProps) => {
Connect to Database
- Connect to PostgreSQL or MySQL database using a connection URL or manual entry
+ Connect to PostgreSQL or MySQL database using a connection URL or manual entry.{" "}
+
+ Privacy Policy
+
@@ -229,18 +257,18 @@ const DatabaseModal = ({ open, onOpenChange }: DatabaseModalProps) => {
-
+
-
+
-
+
MySQL
@@ -256,7 +284,7 @@ const DatabaseModal = ({ open, onOpenChange }: DatabaseModalProps) => {
setConnectionMode('url')}
data-testid="connection-mode-url"
>
@@ -265,7 +293,7 @@ const DatabaseModal = ({ open, onOpenChange }: DatabaseModalProps) => {
setConnectionMode('manual')}
data-testid="connection-mode-manual"
>
@@ -289,7 +317,7 @@ const DatabaseModal = ({ open, onOpenChange }: DatabaseModalProps) => {
}
value={connectionUrl}
onChange={(e) => setConnectionUrl(e.target.value)}
- className="bg-muted border-border font-mono text-sm"
+ className="bg-muted border-border font-mono text-sm focus-visible:ring-purple-500"
/>
Enter your database connection string
@@ -301,57 +329,57 @@ const DatabaseModal = ({ open, onOpenChange }: DatabaseModalProps) => {
<>
Host
- setHost(e.target.value)}
- className="bg-muted border-border"
+ className="bg-muted border-border focus-visible:ring-purple-500"
/>
-
+
Port
- setPort(e.target.value)}
- className="bg-muted border-border"
+ className="bg-muted border-border focus-visible:ring-purple-500"
/>
-
+
Database Name
- setDatabase(e.target.value)}
- className="bg-muted border-border"
+ className="bg-muted border-border focus-visible:ring-purple-500"
/>
-
+
Username
- setUsername(e.target.value)}
- className="bg-muted border-border"
+ className="bg-muted border-border focus-visible:ring-purple-500"
/>
-
+
Password
- setPassword(e.target.value)}
- className="bg-muted border-border"
+ className="bg-muted border-border focus-visible:ring-purple-500"
/>
>
@@ -387,6 +415,7 @@ const DatabaseModal = ({ open, onOpenChange }: DatabaseModalProps) => {
variant="outline"
onClick={() => onOpenChange(false)}
disabled={isConnecting}
+ className="hover:bg-purple-500/20 hover:text-foreground"
data-testid="cancel-database-button"
>
Cancel
@@ -394,7 +423,7 @@ const DatabaseModal = ({ open, onOpenChange }: DatabaseModalProps) => {
{isConnecting ? "Connecting..." : "Connect"}
diff --git a/app/src/components/modals/DeleteDatabaseModal.tsx b/app/src/components/modals/DeleteDatabaseModal.tsx
index f82bcf8a..7ba09ddb 100644
--- a/app/src/components/modals/DeleteDatabaseModal.tsx
+++ b/app/src/components/modals/DeleteDatabaseModal.tsx
@@ -32,7 +32,7 @@ const DeleteDatabaseModal = ({
return (
@@ -40,7 +40,7 @@ const DeleteDatabaseModal = ({
Delete Database
-
+
{isDemo ? (
Demo databases cannot be deleted.
@@ -54,7 +54,7 @@ const DeleteDatabaseModal = ({
Are you sure you want to delete "{databaseName}"?
-
+
This action cannot be undone. All data and schema information
for this database will be permanently removed.
@@ -67,7 +67,7 @@ const DeleteDatabaseModal = ({
onOpenChange(false)}
- className="bg-gray-800 border-gray-600 text-gray-300 hover:bg-gray-700"
+ className="bg-card border-border text-muted-foreground hover:bg-muted"
data-testid="delete-modal-cancel"
>
Cancel
diff --git a/app/src/components/modals/SettingsModal.tsx b/app/src/components/modals/SettingsModal.tsx
new file mode 100644
index 00000000..993a237e
--- /dev/null
+++ b/app/src/components/modals/SettingsModal.tsx
@@ -0,0 +1,109 @@
+import { useState, useEffect } from "react";
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogDescription,
+} from "@/components/ui/dialog";
+import { Button } from "@/components/ui/button";
+import { Textarea } from "@/components/ui/textarea";
+import { Label } from "@/components/ui/label";
+import { Save, X } from "lucide-react";
+
+interface SettingsModalProps {
+ open: boolean;
+ onClose: () => void;
+ initialRules?: string;
+ onSave: (rules: string) => void;
+}
+
+const SettingsModal = ({ open, onClose, initialRules = "", onSave }: SettingsModalProps) => {
+ const [rules, setRules] = useState(initialRules);
+
+ // Sync with prop changes
+ useEffect(() => {
+ setRules(initialRules);
+ }, [initialRules]);
+
+ const handleSave = () => {
+ onSave(rules);
+ onClose();
+ };
+
+ const handleClear = () => {
+ setRules("");
+ };
+
+ return (
+
+
+
+ Query Settings
+
+ Define custom rules and specifications for SQL generation. These rules will be applied to all your queries.
+
+
+
+
+
+
+ User Rules & Specifications
+
+
+
+
+
+ 💡 Tip: These rules help the AI understand your preferences and constraints when generating SQL queries.
+ You can specify formatting preferences, business rules, or technical requirements.
+
+
+
+
+
+
+
+ Clear Rules
+
+
+
+ Cancel
+
+
+
+ Save Rules
+
+
+
+
+
+ );
+};
+
+export default SettingsModal;
diff --git a/app/src/components/modals/TokensModal.tsx b/app/src/components/modals/TokensModal.tsx
index 46d66aa7..3580f694 100644
--- a/app/src/components/modals/TokensModal.tsx
+++ b/app/src/components/modals/TokensModal.tsx
@@ -152,10 +152,10 @@ const TokensModal: React.FC = ({ open, onOpenChange }) => {
return (
<>
-
+
- API Tokens
-
+ API Tokens
+
API tokens allow you to authenticate with the QueryWeaver API without using OAuth.
Keep your tokens secure and don't share them publicly.
@@ -178,9 +178,9 @@ const TokensModal: React.FC = ({ open, onOpenChange }) => {
{newToken && (
-
+
Token Generated Successfully!
-
+
Important: This is the only time you'll see this token. Copy it now and store it securely.
@@ -189,14 +189,14 @@ const TokensModal: React.FC
= ({ open, onOpenChange }) => {
type={showToken ? 'text' : 'password'}
value={newToken}
readOnly
- className="bg-gray-900 border-gray-600 text-gray-100 font-mono text-xs sm:text-sm"
+ className="bg-background border-border text-foreground font-mono text-xs sm:text-sm"
data-testid="new-token-input"
/>
setShowToken(!showToken)}
- className="bg-gray-700 border-gray-600 text-gray-200 hover:bg-gray-600 flex-shrink-0"
+ className="bg-muted border-border text-foreground hover:bg-muted/80 flex-shrink-0"
data-testid="toggle-token-visibility"
>
{showToken ? : }
@@ -206,7 +206,7 @@ const TokensModal: React.FC = ({ open, onOpenChange }) => {
variant="outline"
size="sm"
onClick={handleCopyToken}
- className="bg-gray-700 border-gray-600 text-gray-200 hover:bg-gray-600"
+ className="bg-muted border-border text-foreground hover:bg-muted/80"
data-testid="copy-token-btn"
>
@@ -219,28 +219,28 @@ const TokensModal: React.FC = ({ open, onOpenChange }) => {
{/* Tokens List */}
-
Your Tokens
+
Your Tokens
{loading ? (
-
Loading tokens...
+
Loading tokens...
) : tokens.length === 0 ? (
-
You don't have any API tokens yet.
+
You don't have any API tokens yet.
) : (
-
- Token
- Created
- Actions
+
+ Token
+ Created
+ Actions
{tokens.map((token) => (
-
-
+
+
****{token.token_id}
-
+
{formatDate(token.created_at)}
@@ -268,10 +268,10 @@ const TokensModal: React.FC = ({ open, onOpenChange }) => {
{/* Delete Confirmation Dialog */}
!open && setDeleteTokenId(null)}>
-
+
- Delete Token
-
+ Delete Token
+
Are you sure you want to delete this token? This action cannot be undone.
@@ -280,7 +280,7 @@ const TokensModal: React.FC = ({ open, onOpenChange }) => {
diff --git a/app/src/components/schema/SchemaViewer.tsx b/app/src/components/schema/SchemaViewer.tsx
index fb5fb575..863bde17 100644
--- a/app/src/components/schema/SchemaViewer.tsx
+++ b/app/src/components/schema/SchemaViewer.tsx
@@ -271,7 +271,7 @@ const SchemaViewer = ({ isOpen, onClose, onWidthChange, sidebarWidth = 64 }: Sch
// Theme-aware colors
const isLight = theme === 'light';
const textColor = isLight ? '#111' : '#f5f5f5';
- const fillColor = isLight ? '#ffffff' : '#1f2937';
+ const fillColor = isLight ? '#ffffff' : '#191919';
const strokeColor = isLight ? '#d1d5db' : '#374151';
const columnTextColor = isLight ? '#111' : '#e5e7eb';
const typeTextColor = isLight ? '#6b7280' : '#9ca3af';
@@ -383,7 +383,7 @@ const SchemaViewer = ({ isOpen, onClose, onWidthChange, sidebarWidth = 64 }: Sch
// Get theme-aware colors
const getBackgroundColor = () => {
- return theme === 'light' ? '#ffffff' : '#030712';
+ return theme === 'light' ? '#ffffff' : '#191919';
};
const getLinkColor = () => {
@@ -403,7 +403,7 @@ const SchemaViewer = ({ isOpen, onClose, onWidthChange, sidebarWidth = 64 }: Sch
{/* Schema Viewer */}
{/* Header */}
-
-
Database Schema
+
+
Database Schema
{/* Controls */}
-
+
@@ -443,7 +443,7 @@ const SchemaViewer = ({ isOpen, onClose, onWidthChange, sidebarWidth = 64 }: Sch
variant="outline"
size="sm"
onClick={handleZoomOut}
- className="h-8 w-8 p-0 bg-gray-800 border-gray-600 text-gray-300 hover:bg-gray-700"
+ className="h-8 w-8 p-0 bg-card border-border text-muted-foreground hover:bg-muted"
title="Zoom Out"
>
@@ -452,7 +452,7 @@ const SchemaViewer = ({ isOpen, onClose, onWidthChange, sidebarWidth = 64 }: Sch
variant="outline"
size="sm"
onClick={handleCenter}
- className="h-8 w-8 p-0 bg-gray-800 border-gray-600 text-gray-300 hover:bg-gray-700"
+ className="h-8 w-8 p-0 bg-card border-border text-muted-foreground hover:bg-muted"
title="Center"
>
@@ -460,10 +460,10 @@ const SchemaViewer = ({ isOpen, onClose, onWidthChange, sidebarWidth = 64 }: Sch
{/* Graph Container */}
-
+
{loading && (
-
Loading schema...
+
Loading schema...
)}
{!loading && schemaData && schemaData.nodes && schemaData.nodes.length > 0 && (
@@ -521,7 +521,7 @@ const SchemaViewer = ({ isOpen, onClose, onWidthChange, sidebarWidth = 64 }: Sch
}}
>
-
+
diff --git a/app/src/components/ui/dialog.tsx b/app/src/components/ui/dialog.tsx
index 17069031..d251d260 100644
--- a/app/src/components/ui/dialog.tsx
+++ b/app/src/components/ui/dialog.tsx
@@ -36,7 +36,7 @@ const DialogContent = React.forwardRef<
,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+Switch.displayName = SwitchPrimitives.Root.displayName
+
+export { Switch }
diff --git a/app/src/components/ui/theme-toggle.tsx b/app/src/components/ui/theme-toggle.tsx
index 2a8d6d72..494ef862 100644
--- a/app/src/components/ui/theme-toggle.tsx
+++ b/app/src/components/ui/theme-toggle.tsx
@@ -12,8 +12,9 @@ type Theme = "light" | "dark";
const ThemeToggle = () => {
const [theme, setTheme] = useState(() => {
try {
- const savedTheme = localStorage.getItem("theme") as Theme;
- return savedTheme || "dark";
+ const savedTheme = localStorage.getItem("theme");
+ // Normalize: only accept "light" or "dark", default to "dark"
+ return (savedTheme === "light" || savedTheme === "dark") ? savedTheme : "dark";
} catch {
return "dark";
}
@@ -47,7 +48,7 @@ const ThemeToggle = () => {
diff --git a/app/src/components/ui/tooltip.tsx b/app/src/components/ui/tooltip.tsx
index 8490d748..0cd9d605 100644
--- a/app/src/components/ui/tooltip.tsx
+++ b/app/src/components/ui/tooltip.tsx
@@ -18,7 +18,7 @@ const TooltipContent = React.forwardRef<
sideOffset={sideOffset}
onPointerDownOutside={(e) => e.preventDefault()}
className={cn(
- "!z-[9999] overflow-hidden rounded-md bg-gray-800 border border-gray-600 px-3 py-2 text-sm text-gray-100 shadow-lg animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
+ "!z-[9999] overflow-hidden rounded-md bg-card border border-border px-3 py-2 text-sm text-foreground shadow-lg animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
diff --git a/app/src/contexts/ChatContext.tsx b/app/src/contexts/ChatContext.tsx
new file mode 100644
index 00000000..b027f218
--- /dev/null
+++ b/app/src/contexts/ChatContext.tsx
@@ -0,0 +1,99 @@
+import React, { createContext, useContext, useState, useRef, useCallback, useEffect } from 'react';
+import { useDatabase } from '@/contexts/DatabaseContext';
+import type { ConversationMessage } from '@/types/api';
+
+interface ChatMessageData {
+ id: string;
+ type: 'user' | 'ai' | 'ai-steps' | 'sql-query' | 'query-result' | 'confirmation';
+ content: string;
+ steps?: Array<{
+ icon: 'search' | 'database' | 'code' | 'message';
+ text: string;
+ }>;
+ queryData?: any[];
+ analysisInfo?: {
+ confidence?: number;
+ missing?: string;
+ ambiguities?: string;
+ explanation?: string;
+ isValid?: boolean;
+ };
+ confirmationData?: {
+ sqlQuery: string;
+ operationType: string;
+ message: string;
+ chatHistory: string[];
+ };
+ timestamp: Date;
+}
+
+interface ChatContextType {
+ messages: ChatMessageData[];
+ setMessages: React.Dispatch>;
+ conversationHistory: React.MutableRefObject;
+ isProcessing: boolean;
+ setIsProcessing: React.Dispatch>;
+ resetChat: () => void;
+}
+
+const initialMessage: ChatMessageData = {
+ id: "1",
+ type: "ai",
+ content: "Hello! Describe what you'd like to ask your database",
+ timestamp: new Date(),
+};
+
+const ChatContext = createContext(undefined);
+
+export const ChatProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
+ const { selectedGraph } = useDatabase();
+ const [messages, setMessages] = useState([initialMessage]);
+ const [isProcessing, setIsProcessing] = useState(false);
+ const conversationHistory = useRef([]);
+ const previousGraphIdRef = useRef(undefined);
+
+ // Reset conversation when the selected graph changes to avoid leaking
+ // conversation history between different databases.
+ useEffect(() => {
+ // Only reset if the graph actually changed (not on initial mount with same graph)
+ if (previousGraphIdRef.current !== undefined && previousGraphIdRef.current !== selectedGraph?.id) {
+ conversationHistory.current = [];
+ setMessages([{
+ ...initialMessage,
+ id: Date.now().toString(),
+ timestamp: new Date(),
+ }]);
+ }
+ previousGraphIdRef.current = selectedGraph?.id;
+ }, [selectedGraph?.id]);
+
+ const resetChat = useCallback(() => {
+ conversationHistory.current = [];
+ setMessages([{
+ ...initialMessage,
+ id: Date.now().toString(),
+ timestamp: new Date(),
+ }]);
+ }, []);
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useChat = () => {
+ const context = useContext(ChatContext);
+ if (context === undefined) {
+ throw new Error('useChat must be used within a ChatProvider');
+ }
+ return context;
+};
diff --git a/app/src/index.css b/app/src/index.css
index af96b885..96196294 100644
--- a/app/src/index.css
+++ b/app/src/index.css
@@ -1,296 +1,274 @@
-@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Fira+Code:wght@400;500;600&display=swap');
+@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Merriweather:wght@300;400;700&family=JetBrains+Mono:wght@400;500;600&display=swap');
@tailwind base;
@tailwind components;
@tailwind utilities;
-/* Definition of the design system. All colors, gradients, fonts, etc should be defined here.
-All colors MUST be HSL.
+/* Definition of the design system - Clean Slate Theme.
+Based on shadcn/ui Clean Slate theme with OKLch color space for perceptual uniformity.
*/
@layer base {
:root {
- /* QueryWeaver Dark Theme */
- --background: 222 84% 5%;
- --foreground: 210 40% 98%;
-
- --card: 224 71% 8%;
- --card-foreground: 210 40% 98%;
-
- --popover: 224 71% 8%;
- --popover-foreground: 210 40% 98%;
-
- /* Purple primary matching QueryWeaver branding */
- --primary: 262 83% 58%;
- --primary-foreground: 210 40% 98%;
-
- --primary-glow: 262 83% 70%;
- --primary-dark: 262 83% 45%;
-
- --secondary: 215 28% 17%;
- --secondary-foreground: 210 40% 98%;
-
- --muted: 215 28% 17%;
- --muted-foreground: 215 20% 65%;
-
- --accent: 216 34% 17%;
- --accent-foreground: 210 40% 98%;
-
- --destructive: 0 84% 60%;
- --destructive-foreground: 210 40% 98%;
-
- --border: 215 28% 17%;
- --input: 215 28% 17%;
- --ring: 262 83% 58%;
-
- /* QueryWeaver specific colors */
- --sidebar-bg: 222 84% 4%;
- --chat-bg: 220 13% 18%;
- --modal-overlay: 222 84% 4% / 0.8;
-
- /* Button specific colors */
- --google-blue: 217 91% 60%;
- --github-dark: 210 11% 15%;
-
+ /* Light Mode Colors - OKLch */
+ --background: oklch(0.9842 0.0034 247.8575);
+ --foreground: oklch(0.2795 0.0368 260.0310);
+
+ --card: oklch(1.0000 0 0);
+ --card-foreground: oklch(0.2795 0.0368 260.0310);
+
+ --popover: oklch(1.0000 0 0);
+ --popover-foreground: oklch(0.2795 0.0368 260.0310);
+
+ --primary: oklch(0.5531 0.2471 301.88);
+ --primary-foreground: oklch(1.0000 0 0);
+
+ --secondary: oklch(0.9276 0.0058 264.5313);
+ --secondary-foreground: oklch(0.3729 0.0306 259.7328);
+
+ --muted: oklch(0.9670 0.0029 264.5419);
+ --muted-foreground: oklch(0.5510 0.0234 264.3637);
+
+ --accent: oklch(0.9299 0.0334 272.7879);
+ --accent-foreground: oklch(0.3729 0.0306 259.7328);
+
+ --destructive: oklch(0.6368 0.2078 25.3313);
+ --destructive-foreground: oklch(1.0000 0 0);
+
+ --border: oklch(0.8717 0.0093 258.3382);
+ --input: oklch(0.8717 0.0093 258.3382);
+ --ring: oklch(0.5531 0.2471 301.88);
+
+ /* Chart colors */
+ --chart-1: oklch(0.5531 0.2471 301.88);
+ --chart-2: oklch(0.5106 0.2301 276.9656);
+ --chart-3: oklch(0.4568 0.2146 277.0229);
+ --chart-4: oklch(0.3984 0.1773 277.3662);
+ --chart-5: oklch(0.3588 0.1354 278.6973);
+
+ /* Semantic state colors */
+ --success: oklch(0.6500 0.1800 145.0000);
+ --success-foreground: oklch(1.0000 0 0);
+ --warning: oklch(0.7500 0.1500 90.0000);
+ --warning-foreground: oklch(0.2000 0.0300 90.0000);
+ --error: oklch(0.6368 0.2078 25.3313);
+ --error-foreground: oklch(1.0000 0 0);
+ --info: oklch(0.6500 0.1800 240.0000);
+ --info-foreground: oklch(1.0000 0 0);
+
+ /* Sidebar colors */
+ --sidebar: oklch(0.9670 0.0029 264.5419);
+ --sidebar-foreground: oklch(0.2795 0.0368 260.0310);
+ --sidebar-primary: oklch(0.5531 0.2471 301.88);
+ --sidebar-primary-foreground: oklch(1.0000 0 0);
+ --sidebar-accent: oklch(0.9299 0.0334 272.7879);
+ --sidebar-accent-foreground: oklch(0.3729 0.0306 259.7328);
+ --sidebar-border: oklch(0.8717 0.0093 258.3382);
+ --sidebar-ring: oklch(0.5531 0.2471 301.88);
+
+ /* Typography */
+ --font-sans: Inter, sans-serif;
+ --font-serif: Merriweather, serif;
+ --font-mono: JetBrains Mono, monospace;
+
+ /* Spacing and radius */
--radius: 0.5rem;
-
- --sidebar-background: 0 0% 98%;
-
- --sidebar-foreground: 240 5.3% 26.1%;
-
- --sidebar-primary: 240 5.9% 10%;
-
- --sidebar-primary-foreground: 0 0% 98%;
-
- --sidebar-accent: 240 4.8% 95.9%;
-
- --sidebar-accent-foreground: 240 5.9% 10%;
-
- --sidebar-border: 220 13% 91%;
-
- --sidebar-ring: 217.2 91.2% 59.8%;
- }
-
- .dark {
- /* Keep dark theme consistent */
- --background: 222 84% 5%;
- --foreground: 210 40% 98%;
- --card: 224 71% 8%;
- --card-foreground: 210 40% 98%;
- --popover: 224 71% 8%;
- --popover-foreground: 210 40% 98%;
- --primary: 262 83% 58%;
- --primary-foreground: 210 40% 98%;
- --secondary: 215 28% 17%;
- --secondary-foreground: 210 40% 98%;
- --muted: 215 28% 17%;
- --muted-foreground: 215 20% 65%;
- --accent: 216 34% 17%;
- --accent-foreground: 210 40% 98%;
- --destructive: 0 84% 60%;
- --destructive-foreground: 210 40% 98%;
- --border: 215 28% 17%;
- --input: 215 28% 17%;
- --ring: 262 83% 58%;
+ --spacing: 0.25rem;
+
+ /* Shadow system */
+ --shadow-x: 0px;
+ --shadow-y: 4px;
+ --shadow-blur: 8px;
+ --shadow-spread: -1px;
+ --shadow-opacity: 0.1;
+ --shadow-color: hsl(0 0% 0%);
+ --shadow-2xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05);
+ --shadow-xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05);
+ --shadow-sm: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 1px 2px -2px hsl(0 0% 0% / 0.10);
+ --shadow: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 1px 2px -2px hsl(0 0% 0% / 0.10);
+ --shadow-md: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 2px 4px -2px hsl(0 0% 0% / 0.10);
+ --shadow-lg: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 4px 6px -2px hsl(0 0% 0% / 0.10);
+ --shadow-xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 8px 10px -2px hsl(0 0% 0% / 0.10);
+ --shadow-2xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.25);
+
+ /* Legacy compatibility */
+ --tracking-normal: 0em;
}
- /* Light Theme */
[data-theme="light"] {
- --background: 0 0% 100%;
- --foreground: 222 84% 5%;
-
- --card: 0 0% 100%;
- --card-foreground: 222 84% 5%;
-
- --popover: 0 0% 100%;
- --popover-foreground: 222 84% 5%;
-
- --primary: 262 83% 58%;
- --primary-foreground: 210 40% 98%;
-
- --primary-glow: 262 83% 70%;
- --primary-dark: 262 83% 45%;
-
- --secondary: 210 40% 96%;
- --secondary-foreground: 222 84% 5%;
-
- --muted: 210 40% 96%;
- --muted-foreground: 215 20% 35%;
-
- --accent: 210 40% 96%;
- --accent-foreground: 222 84% 5%;
-
- --destructive: 0 84% 60%;
- --destructive-foreground: 210 40% 98%;
-
- --border: 214 32% 91%;
- --input: 214 32% 91%;
- --ring: 262 83% 58%;
-
- --sidebar-bg: 0 0% 98%;
- --chat-bg: 0 0% 95%;
- --modal-overlay: 0 0% 0% / 0.5;
- }
+ /* Light Mode Colors - OKLch (same as :root) */
+ --background: oklch(0.9842 0.0034 247.8575);
+ --foreground: oklch(0.2795 0.0368 260.0310);
+
+ --card: oklch(1.0000 0 0);
+ --card-foreground: oklch(0.2795 0.0368 260.0310);
+
+ --popover: oklch(1.0000 0 0);
+ --popover-foreground: oklch(0.2795 0.0368 260.0310);
+
+ --primary: oklch(0.5531 0.2471 301.88);
+ --primary-foreground: oklch(1.0000 0 0);
+
+ --secondary: oklch(0.9276 0.0058 264.5313);
+ --secondary-foreground: oklch(0.3729 0.0306 259.7328);
+
+ --muted: oklch(0.9670 0.0029 264.5419);
+ --muted-foreground: oklch(0.5510 0.0234 264.3637);
+
+ --accent: oklch(0.9299 0.0334 272.7879);
+ --accent-foreground: oklch(0.3729 0.0306 259.7328);
+
+ --destructive: oklch(0.6368 0.2078 25.3313);
+ --destructive-foreground: oklch(1.0000 0 0);
+
+ --border: oklch(0.8717 0.0093 258.3382);
+ --input: oklch(0.8717 0.0093 258.3382);
+ --ring: oklch(0.5531 0.2471 301.88);
+
+ --chart-1: oklch(0.5531 0.2471 301.88);
+ --chart-2: oklch(0.5106 0.2301 276.9656);
+ --chart-3: oklch(0.4568 0.2146 277.0229);
+ --chart-4: oklch(0.3984 0.1773 277.3662);
+ --chart-5: oklch(0.3588 0.1354 278.6973);
+
+ /* Semantic state colors */
+ --success: oklch(0.6500 0.1800 145.0000);
+ --success-foreground: oklch(1.0000 0 0);
+ --warning: oklch(0.7500 0.1500 90.0000);
+ --warning-foreground: oklch(0.2000 0.0300 90.0000);
+ --error: oklch(0.6368 0.2078 25.3313);
+ --error-foreground: oklch(1.0000 0 0);
+ --info: oklch(0.6500 0.1800 240.0000);
+ --info-foreground: oklch(1.0000 0 0);
+
+ --sidebar: oklch(0.9670 0.0029 264.5419);
+ --sidebar-foreground: oklch(0.2795 0.0368 260.0310);
+ --sidebar-primary: oklch(0.5531 0.2471 301.88);
+ --sidebar-primary-foreground: oklch(1.0000 0 0);
+ --sidebar-accent: oklch(0.9299 0.0334 272.7879);
+ --sidebar-accent-foreground: oklch(0.3729 0.0306 259.7328);
+ --sidebar-border: oklch(0.8717 0.0093 258.3382);
+ --sidebar-ring: oklch(0.5531 0.2471 301.88);
+
+ --font-sans: Inter, sans-serif;
+ --font-serif: Merriweather, serif;
+ --font-mono: JetBrains Mono, monospace;
- /* System Theme - follows OS preference */
- [data-theme="system"] {
- /* Use dark as default, will be overridden by media query */
- --background: 222 84% 5%;
- --foreground: 210 40% 98%;
- --card: 224 71% 8%;
- --card-foreground: 210 40% 98%;
- --popover: 224 71% 8%;
- --popover-foreground: 210 40% 98%;
- --primary: 262 83% 58%;
- --primary-foreground: 210 40% 98%;
- --secondary: 215 28% 17%;
- --secondary-foreground: 210 40% 98%;
- --muted: 215 28% 17%;
- --muted-foreground: 215 20% 65%;
- --accent: 216 34% 17%;
- --accent-foreground: 210 40% 98%;
- --destructive: 0 84% 60%;
- --destructive-foreground: 210 40% 98%;
- --border: 215 28% 17%;
- --input: 215 28% 17%;
- --ring: 262 83% 58%;
- }
+ --radius: 0.5rem;
+ --spacing: 0.25rem;
+
+ --shadow-x: 0px;
+ --shadow-y: 4px;
+ --shadow-blur: 8px;
+ --shadow-spread: -1px;
+ --shadow-opacity: 0.1;
+ --shadow-color: hsl(0 0% 0%);
+ --shadow-2xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05);
+ --shadow-xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05);
+ --shadow-sm: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 1px 2px -2px hsl(0 0% 0% / 0.10);
+ --shadow: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 1px 2px -2px hsl(0 0% 0% / 0.10);
+ --shadow-md: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 2px 4px -2px hsl(0 0% 0% / 0.10);
+ --shadow-lg: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 4px 6px -2px hsl(0 0% 0% / 0.10);
+ --shadow-xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 8px 10px -2px hsl(0 0% 0% / 0.10);
+ --shadow-2xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.25);
+
+ --tracking-normal: 0em;
+ }
+
+ [data-theme="dark"] {
+ /* Dark Mode Colors - OKLch */
+ --background: oklch(0.1158 0 0);
+ --foreground: oklch(0.9288 0.0126 255.5078);
+
+ --card: oklch(0.1158 0 0);
+ --card-foreground: oklch(0.9288 0.0126 255.5078);
+
+ --popover: oklch(0.1158 0 0);
+ --popover-foreground: oklch(0.9288 0.0126 255.5078);
+
+ --primary: oklch(0.6801 0.1583 276.9349);
+ --primary-foreground: oklch(0.2077 0.0398 265.7549);
+
+ --secondary: oklch(0.3351 0.0331 260.9120);
+ --secondary-foreground: oklch(0.8717 0.0093 258.3382);
+
+ --muted: oklch(0.1800 0 0);
+ --muted-foreground: oklch(0.7137 0.0192 261.3246);
+
+ --accent: oklch(0.2400 0 0);
+ --accent-foreground: oklch(0.8717 0.0093 258.3382);
+
+ --destructive: oklch(0.6368 0.2078 25.3313);
+ --destructive-foreground: oklch(0.2077 0.0398 265.7549);
+
+ --border: oklch(0.4461 0.0263 256.8018);
+ --input: oklch(0.4461 0.0263 256.8018);
+ --ring: oklch(0.6801 0.1583 276.9349);
+
+ /* Chart colors */
+ --chart-1: oklch(0.6801 0.1583 276.9349);
+ --chart-2: oklch(0.5854 0.2041 277.1173);
+ --chart-3: oklch(0.5106 0.2301 276.9656);
+ --chart-4: oklch(0.4568 0.2146 277.0229);
+ --chart-5: oklch(0.3984 0.1773 277.3662);
+
+ /* Semantic state colors */
+ --success: oklch(0.6500 0.1800 145.0000);
+ --success-foreground: oklch(0.1158 0 0);
+ --warning: oklch(0.7500 0.1500 90.0000);
+ --warning-foreground: oklch(0.1158 0 0);
+ --error: oklch(0.6368 0.2078 25.3313);
+ --error-foreground: oklch(0.1158 0 0);
+ --info: oklch(0.6500 0.1800 240.0000);
+ --info-foreground: oklch(0.1158 0 0);
+
+ /* Sidebar colors */
+ --sidebar: oklch(0.1158 0 0);
+ --sidebar-foreground: oklch(0.9288 0.0126 255.5078);
+ --sidebar-primary: oklch(0.6801 0.1583 276.9349);
+ --sidebar-primary-foreground: oklch(0.2077 0.0398 265.7549);
+ --sidebar-accent: oklch(0.3729 0.0306 259.7328);
+ --sidebar-accent-foreground: oklch(0.8717 0.0093 258.3382);
+ --sidebar-border: oklch(0.4461 0.0263 256.8018);
+ --sidebar-ring: oklch(0.6801 0.1583 276.9349);
+
+ /* Typography */
+ --font-sans: Inter, sans-serif;
+ --font-serif: Merriweather, serif;
+ --font-mono: JetBrains Mono, monospace;
+
+ /* Spacing and radius */
+ --radius: 0.5rem;
- /* System theme in light mode */
- @media (prefers-color-scheme: light) {
- [data-theme="system"] {
- --background: 0 0% 100%;
- --foreground: 222 84% 5%;
- --card: 0 0% 100%;
- --card-foreground: 222 84% 5%;
- --popover: 0 0% 100%;
- --popover-foreground: 222 84% 5%;
- --primary: 262 83% 58%;
- --primary-foreground: 210 40% 98%;
- --secondary: 210 40% 96%;
- --secondary-foreground: 222 84% 5%;
- --muted: 210 40% 96%;
- --muted-foreground: 215 20% 35%;
- --accent: 210 40% 96%;
- --accent-foreground: 222 84% 5%;
- --destructive: 0 84% 60%;
- --destructive-foreground: 210 40% 98%;
- --border: 214 32% 91%;
- --input: 214 32% 91%;
- --ring: 262 83% 58%;
- --sidebar-bg: 0 0% 98%;
- --chat-bg: 0 0% 95%;
- --modal-overlay: 0 0% 0% / 0.5;
- }
+ /* Shadow system */
+ --shadow-x: 0px;
+ --shadow-y: 4px;
+ --shadow-blur: 8px;
+ --shadow-spread: -1px;
+ --shadow-opacity: 0.1;
+ --shadow-color: hsl(0 0% 0%);
+ --shadow-2xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05);
+ --shadow-xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05);
+ --shadow-sm: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 1px 2px -2px hsl(0 0% 0% / 0.10);
+ --shadow: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 1px 2px -2px hsl(0 0% 0% / 0.10);
+ --shadow-md: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 2px 4px -2px hsl(0 0% 0% / 0.10);
+ --shadow-lg: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 4px 6px -2px hsl(0 0% 0% / 0.10);
+ --shadow-xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 8px 10px -2px hsl(0 0% 0% / 0.10);
+ --shadow-2xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.25);
}
}
+/* Removed @theme inline block - incompatible with Tailwind v3 */
+
@layer base {
* {
@apply border-border;
}
body {
- @apply bg-background text-foreground font-sans;
- }
-
- /* Theme-specific body background overrides */
- [data-theme="dark"] body {
- background-color: hsl(222 84% 5%);
- color: hsl(210 40% 98%);
- }
-
- [data-theme="light"] body {
- background-color: hsl(0 0% 100%);
- color: hsl(222 84% 5%);
- }
-
- [data-theme="system"] body {
- background-color: hsl(222 84% 5%);
- color: hsl(210 40% 98%);
- }
-
- @media (prefers-color-scheme: light) {
- [data-theme="system"] body {
- background-color: hsl(0 0% 100%);
- color: hsl(222 84% 5%);
- }
- }
-
- /* Override gray backgrounds for light theme */
- [data-theme="light"] .bg-gray-900,
- [data-theme="light"] [class*="bg-gray-9"] {
- background-color: hsl(0 0% 100%) !important;
- }
-
- [data-theme="light"] .bg-gray-800,
- [data-theme="light"] [class*="bg-gray-8"] {
- background-color: hsl(0 0% 98%) !important;
- }
-
- [data-theme="light"] .bg-gray-700 {
- background-color: hsl(210 40% 96%) !important;
- }
-
- [data-theme="light"] .text-gray-100,
- [data-theme="light"] .text-white {
- color: hsl(222 84% 5%) !important;
- }
-
- [data-theme="light"] .text-gray-200 {
- color: hsl(222 84% 10%) !important;
- }
-
- [data-theme="light"] .text-gray-300 {
- color: hsl(215 20% 25%) !important;
- }
-
- [data-theme="light"] .text-gray-400 {
- color: hsl(215 20% 35%) !important;
- }
-
- [data-theme="light"] .border-gray-700,
- [data-theme="light"] .border-gray-600 {
- border-color: hsl(214 32% 91%) !important;
- }
-
- /* System theme with light preference */
- @media (prefers-color-scheme: light) {
- [data-theme="system"] .bg-gray-900,
- [data-theme="system"] [class*="bg-gray-9"] {
- background-color: hsl(0 0% 100%) !important;
- }
-
- [data-theme="system"] .bg-gray-800,
- [data-theme="system"] [class*="bg-gray-8"] {
- background-color: hsl(0 0% 98%) !important;
- }
-
- [data-theme="system"] .bg-gray-700 {
- background-color: hsl(210 40% 96%) !important;
- }
-
- [data-theme="system"] .text-gray-100,
- [data-theme="system"] .text-white {
- color: hsl(222 84% 5%) !important;
- }
-
- [data-theme="system"] .text-gray-200 {
- color: hsl(222 84% 10%) !important;
- }
-
- [data-theme="system"] .text-gray-300 {
- color: hsl(215 20% 25%) !important;
- }
-
- [data-theme="system"] .text-gray-400 {
- color: hsl(215 20% 35%) !important;
- }
-
- [data-theme="system"] .border-gray-700,
- [data-theme="system"] .border-gray-600 {
- border-color: hsl(214 32% 91%) !important;
- }
+ background-color: var(--background);
+ color: var(--foreground);
+ font-family: var(--font-sans);
}
}
@@ -351,26 +329,26 @@ All colors MUST be HSL.
}
/* Dark mode scrollbar colors */
- .dark .scrollbar-visible {
+ [data-theme="dark"] .scrollbar-visible {
scrollbar-color: #6B7280 #374151;
}
- .dark .scrollbar-visible::-webkit-scrollbar-track {
+ [data-theme="dark"] .scrollbar-visible::-webkit-scrollbar-track {
background: #374151;
border: 2px solid #374151;
}
- .dark .scrollbar-visible::-webkit-scrollbar-thumb {
+ [data-theme="dark"] .scrollbar-visible::-webkit-scrollbar-thumb {
background: #6B7280;
border-radius: 6px;
border: 2px solid #374151;
}
- .dark .scrollbar-visible::-webkit-scrollbar-thumb:hover {
+ [data-theme="dark"] .scrollbar-visible::-webkit-scrollbar-thumb:hover {
background: #9CA3AF;
}
- .dark .scrollbar-visible::-webkit-scrollbar-corner {
+ [data-theme="dark"] .scrollbar-visible::-webkit-scrollbar-corner {
background: #374151;
}
}
diff --git a/app/src/main.tsx b/app/src/main.tsx
index 53280ea9..70052fa0 100644
--- a/app/src/main.tsx
+++ b/app/src/main.tsx
@@ -4,8 +4,14 @@ import "./index.css";
// Initialize theme on page load
try {
- const savedTheme = localStorage.getItem("theme") || "dark";
- document.documentElement.setAttribute("data-theme", savedTheme);
+ const savedTheme = localStorage.getItem("theme");
+ // Normalize: only accept "light" or "dark", default to "dark"
+ const theme = (savedTheme === "light" || savedTheme === "dark") ? savedTheme : "dark";
+ document.documentElement.setAttribute("data-theme", theme);
+ // Update localStorage if we normalized the value
+ if (savedTheme !== theme) {
+ localStorage.setItem("theme", theme);
+ }
} catch {
document.documentElement.setAttribute("data-theme", "dark");
}
diff --git a/app/src/pages/Index.tsx b/app/src/pages/Index.tsx
index 8a4d50b5..7ac8bcd4 100644
--- a/app/src/pages/Index.tsx
+++ b/app/src/pages/Index.tsx
@@ -32,6 +32,23 @@ const Index = () => {
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [showSchemaViewer, setShowSchemaViewer] = useState(false);
const [showTokensModal, setShowTokensModal] = useState(false);
+ // userRulesSpec is now fetched from the graph database per query
+ const [useMemory, setUseMemory] = useState(() => {
+ // Load from localStorage on init, default to true
+ if (typeof window !== 'undefined') {
+ const saved = localStorage.getItem('queryweaver_use_memory');
+ return saved === null ? true : saved === 'true';
+ }
+ return true;
+ });
+ const [useRulesFromDatabase, setUseRulesFromDatabase] = useState(() => {
+ // Load from localStorage on init, default to false
+ if (typeof window !== 'undefined') {
+ const saved = localStorage.getItem('queryweaver_use_rules_from_database');
+ return saved === null ? false : saved === 'true';
+ }
+ return false;
+ });
const [isRefreshingSchema, setIsRefreshingSchema] = useState(false);
const [isChatProcessing, setIsChatProcessing] = useState(false);
const [sidebarCollapsed, setSidebarCollapsed] = useState(() =>
@@ -111,6 +128,8 @@ const Index = () => {
});
}, []);
+ // No need to fetch rules - we just pass the toggle state to backend
+
// Show login modal when not authenticated after loading completes
useEffect(() => {
// Only auto-open the login modal once per user/session to avoid locking
@@ -312,7 +331,7 @@ const Index = () => {
};
return (
-
+
{/* Hidden file input for schema upload */}
{
{/* Main Content */}
{/* Header */}
-
+
{/* Sub-header for controls */}
-
+
{
{
{selectedGraph?.name || 'Select Database'}
-
+
{graphs.map((graph) => {
const isDemo = graph.id.startsWith('general_');
return (
{ if (!isRefreshingSchema && !isChatProcessing) selectGraph(graph.id); }}
disabled={isRefreshingSchema || isChatProcessing}
data-testid={`database-option-${graph.id}`}
@@ -574,7 +593,7 @@ const Index = () => {
);
})}
{graphs.length === 0 && (
-
+
No databases available
)}
@@ -593,7 +612,7 @@ const Index = () => {
e.preventDefault()}
@@ -610,6 +629,8 @@ const Index = () => {
diff --git a/app/src/pages/Settings.tsx b/app/src/pages/Settings.tsx
new file mode 100644
index 00000000..8cab37ed
--- /dev/null
+++ b/app/src/pages/Settings.tsx
@@ -0,0 +1,607 @@
+import { useState, useEffect, useRef } from "react";
+import { useNavigate } from "react-router-dom";
+import { Button } from "@/components/ui/button";
+import { Textarea } from "@/components/ui/textarea";
+import { Label } from "@/components/ui/label";
+import { Switch } from "@/components/ui/switch";
+import { Badge } from "@/components/ui/badge";
+import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
+import { ArrowLeft, Star, PanelLeft } from "lucide-react";
+import { useToast } from "@/components/ui/use-toast";
+import { useAuth } from "@/contexts/AuthContext";
+import { useDatabase } from "@/contexts/DatabaseContext";
+import { databaseService } from "@/services/database";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+ DropdownMenuSeparator,
+} from "@/components/ui/dropdown-menu";
+import Sidebar from "@/components/layout/Sidebar";
+import SchemaViewer from "@/components/schema";
+
+const Settings = () => {
+ const navigate = useNavigate();
+ const { toast } = useToast();
+ const { isAuthenticated, logout, user } = useAuth();
+ const { selectedGraph } = useDatabase();
+ const [githubStars, setGithubStars] = useState
('-');
+ const [rules, setRules] = useState('');
+ const [isLoadingRules, setIsLoadingRules] = useState(true);
+ const [initialRulesLoaded, setInitialRulesLoaded] = useState(false);
+ const loadedRulesRef = useRef(''); // Track the originally loaded rules
+ const currentRulesRef = useRef(''); // Track the current rules value
+ const currentGraphIdRef = useRef(null); // Track current database ID for unmount save
+ const useRulesFromDatabaseRef = useRef(true); // Track toggle state for unmount
+ const initialRulesLoadedRef = useRef(false); // Track loaded state for unmount
+ const [sidebarCollapsed, setSidebarCollapsed] = useState(() =>
+ typeof window !== 'undefined' ? window.innerWidth < 768 : false
+ );
+ const [windowWidth, setWindowWidth] = useState(typeof window !== 'undefined' ? window.innerWidth : 1024);
+ const [showSchemaViewer, setShowSchemaViewer] = useState(false);
+ const [schemaViewerWidth, setSchemaViewerWidth] = useState(() =>
+ typeof window !== "undefined" ? Math.floor(window.innerWidth * 0.4) : 0,
+ );
+ const [useMemory, setUseMemory] = useState(() => {
+ // Load from localStorage on init, default to true
+ if (typeof window !== 'undefined') {
+ const saved = localStorage.getItem('queryweaver_use_memory');
+ return saved === null ? true : saved === 'true';
+ }
+ return true;
+ });
+ const [initialUseMemory] = useState(() => {
+ if (typeof window !== 'undefined') {
+ const saved = localStorage.getItem('queryweaver_use_memory');
+ return saved === null ? true : saved === 'true';
+ }
+ return true;
+ });
+ const [useRulesFromDatabase, setUseRulesFromDatabase] = useState(() => {
+ // Load from localStorage on init, default to false
+ if (typeof window !== 'undefined') {
+ const saved = localStorage.getItem('queryweaver_use_rules_from_database');
+ return saved === null ? false : saved === 'true';
+ }
+ return false;
+ });
+ const [initialUseRulesFromDatabase] = useState(() => {
+ if (typeof window !== 'undefined') {
+ const saved = localStorage.getItem('queryweaver_use_rules_from_database');
+ return saved === null ? false : saved === 'true';
+ }
+ return false;
+ });
+
+ // Fetch GitHub stars
+ useEffect(() => {
+ fetch('https://api.github.com/repos/FalkorDB/QueryWeaver')
+ .then(response => response.json())
+ .then(data => {
+ if (typeof data.stargazers_count === 'number') {
+ setGithubStars(data.stargazers_count.toLocaleString());
+ }
+ })
+ .catch(error => {
+ console.log('Failed to fetch GitHub stars:', error);
+ });
+ }, []);
+
+ // Handle window resize to update layout
+ useEffect(() => {
+ const handleResize = () => {
+ setWindowWidth(window.innerWidth);
+ };
+
+ window.addEventListener('resize', handleResize);
+ return () => window.removeEventListener('resize', handleResize);
+ }, []);
+
+ // Auto-collapse sidebar when switching to mobile view
+ useEffect(() => {
+ const isMobile = windowWidth < 768;
+ if (isMobile) {
+ setSidebarCollapsed(true);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [windowWidth]); // Only run when windowWidth changes, not on manual toggle
+
+ // Calculate sidebar width based on collapsed state
+ // On desktop: sidebar is always visible (64px), on mobile: can be collapsed (0px)
+ const getSidebarWidth = () => {
+ const isMobile = windowWidth < 768;
+ if (isMobile) {
+ return sidebarCollapsed ? 0 : 64;
+ }
+ return 64; // Always visible on desktop
+ };
+
+ const sidebarWidth = getSidebarWidth();
+
+ // Calculate main content margin and width
+ // On mobile: ignore schema viewer (it's an overlay), only account for sidebar
+ // On desktop: account for both sidebar and schema viewer
+ const getMainContentStyles = () => {
+ const isMobile = windowWidth < 768;
+
+ if (isMobile) {
+ return {
+ marginLeft: `${sidebarWidth}px`,
+ width: `calc(100% - ${sidebarWidth}px)`
+ };
+ }
+
+ // Desktop
+ const totalOffset = showSchemaViewer ? schemaViewerWidth + sidebarWidth : sidebarWidth;
+ return {
+ marginLeft: `${totalOffset}px`,
+ width: `calc(100% - ${totalOffset}px)`
+ };
+ };
+
+ // Fetch user rules from backend when component mounts or database changes (only if using database rules)
+ useEffect(() => {
+ const loadRules = async () => {
+ if (!selectedGraph) {
+ setRules('');
+ setIsLoadingRules(false);
+ setInitialRulesLoaded(false);
+ loadedRulesRef.current = '';
+ currentGraphIdRef.current = null;
+ return;
+ }
+
+ // Update the ref to track current database
+ currentGraphIdRef.current = selectedGraph.id;
+
+ // Only fetch from database if toggle is enabled
+ if (!useRulesFromDatabase) {
+ setIsLoadingRules(false);
+ setInitialRulesLoaded(true);
+ return;
+ }
+
+ try {
+ setIsLoadingRules(true);
+ setInitialRulesLoaded(false);
+ const userRules = await databaseService.getUserRules(selectedGraph.id);
+ const rulesValue = userRules || '';
+ setRules(rulesValue);
+ loadedRulesRef.current = rulesValue; // Store the loaded value
+ } catch (error) {
+ console.error('Failed to load user rules:', error);
+ toast({
+ title: "Error",
+ description: "Failed to load user rules from database",
+ variant: "destructive",
+ });
+ } finally {
+ setIsLoadingRules(false);
+ setInitialRulesLoaded(true);
+ initialRulesLoadedRef.current = true;
+ }
+ };
+
+ loadRules();
+ }, [selectedGraph?.id, toast, useRulesFromDatabase]);
+
+ // Update refs when toggle or rules change
+ useEffect(() => {
+ useRulesFromDatabaseRef.current = useRulesFromDatabase;
+ }, [useRulesFromDatabase]);
+
+ useEffect(() => {
+ currentRulesRef.current = rules;
+ }, [rules]);
+
+ // Save rules when component unmounts (user navigates away from Settings)
+ useEffect(() => {
+ return () => {
+ // At unmount time, check if there are unsaved changes and save them
+ const graphId = currentGraphIdRef.current;
+ const loadedRules = loadedRulesRef.current;
+ const currentRules = currentRulesRef.current;
+ const shouldUseDb = useRulesFromDatabaseRef.current;
+ const isLoaded = initialRulesLoadedRef.current;
+
+ if (graphId && shouldUseDb && currentRules !== loadedRules && isLoaded) {
+ // Save immediately on unmount if there are unsaved changes
+ console.log('Saving rules on unmount...', { graphId, length: currentRules.length });
+ databaseService.updateUserRules(graphId, currentRules)
+ .then(() => console.log('User rules saved on unmount to:', graphId))
+ .catch(err => console.error('Failed to save rules on unmount:', err));
+ }
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []); // Empty array - effect runs once on mount, cleanup runs once on unmount
+
+ // Auto-save to localStorage whenever useMemory changes
+ useEffect(() => {
+ if (typeof window !== 'undefined') {
+ localStorage.setItem('queryweaver_use_memory', String(useMemory));
+ }
+ }, [useMemory]);
+
+ // Auto-save to localStorage whenever useRulesFromDatabase changes
+ useEffect(() => {
+ if (typeof window !== 'undefined') {
+ localStorage.setItem('queryweaver_use_rules_from_database', String(useRulesFromDatabase));
+ }
+ }, [useRulesFromDatabase]);
+
+ const handleLogout = async () => {
+ try {
+ await logout();
+ toast({
+ title: "Logged Out",
+ description: "You have been successfully logged out",
+ });
+ window.location.reload();
+ } catch (error) {
+ toast({
+ title: "Logout Failed",
+ description: error instanceof Error ? error.message : "Failed to logout",
+ variant: "destructive",
+ });
+ }
+ };
+
+ // Check if there are any unsaved changes
+ const hasChanges = useMemory !== initialUseMemory ||
+ useRulesFromDatabase !== initialUseRulesFromDatabase ||
+ (useRulesFromDatabase && rules !== loadedRulesRef.current);
+
+ const handleBackClick = async () => {
+ // Save rules before navigating away if there are unsaved changes
+ const graphId = currentGraphIdRef.current;
+ const loadedRules = loadedRulesRef.current;
+ const currentRules = currentRulesRef.current;
+
+ if (graphId && useRulesFromDatabase && currentRules !== loadedRules && initialRulesLoaded) {
+ try {
+ await databaseService.updateUserRules(graphId, currentRules);
+ loadedRulesRef.current = currentRules; // Update to prevent unmount from saving again
+ } catch (error) {
+ toast({
+ title: "Error",
+ description: error instanceof Error ? error.message : "Failed to save user rules",
+ variant: "destructive",
+ });
+ }
+ }
+
+ navigate('/');
+ };
+
+ return (
+
+ {/* Left Sidebar */}
+
setShowSchemaViewer(!showSchemaViewer)}
+ isSchemaOpen={showSchemaViewer}
+ isCollapsed={sidebarCollapsed}
+ onToggleCollapse={() => setSidebarCollapsed(!sidebarCollapsed)}
+ onSettingsClick={() => {}}
+ />
+
+ {/* Schema Viewer */}
+ setShowSchemaViewer(false)}
+ onWidthChange={setSchemaViewerWidth}
+ sidebarWidth={sidebarWidth}
+ />
+
+ {/* Main Content */}
+
+ {/* Top Header Bar */}
+
+
+ {/* Settings Header */}
+
+
+
+
+
+ Back
+
+
+
Query Settings
+
+ Define custom rules and specifications for SQL generation. Changes are saved automatically.
+
+
+
+
+
+
+ {/* Content */}
+
+
+ {/* Memory Toggle */}
+
+
+
+
+ Use Memory Context
+
+
+ Enable AI to remember previous interactions, preferences, and query patterns for more personalized results
+
+
+
+
+
+
+ {/* Database Rules Toggle */}
+
+
+
+
+ Use Database Rules
+
+
+ Store rules in the database and use for all sessions. When disabled, rules are sent with each request
+
+
+
+
+
+
+ {/* User Rules - only show when database rules are enabled */}
+ {useRulesFromDatabase && (
+
+
+
+ User Rules & Specifications
+
+
+ {rules.length}/5000 characters
+
+
+
+ )}
+
+
+
+
+ );
+};
+
+export default Settings;
diff --git a/app/src/services/chat.ts b/app/src/services/chat.ts
index 445c9b2d..f5a1323b 100644
--- a/app/src/services/chat.ts
+++ b/app/src/services/chat.ts
@@ -17,11 +17,19 @@ export class ChatService {
const endpoint = `/graphs/${encodeURIComponent(request.database)}`;
// Transform conversation history to backend format
- // Backend expects: chat: ["user msg", "ai msg", "user msg", ...]
+ // Backend expects:
+ // - chat: list of user queries only (strings)
+ // - result: list of AI responses (strings)
const chatHistory: string[] = [];
+ const resultHistory: string[] = [];
+
if (request.history && request.history.length > 0) {
for (const msg of request.history) {
- chatHistory.push(msg.content);
+ if (msg.role === 'user') {
+ chatHistory.push(msg.content);
+ } else if (msg.role === 'assistant') {
+ resultHistory.push(msg.content);
+ }
}
}
// Add current query to the chat array
@@ -34,9 +42,13 @@ export class ChatService {
},
body: JSON.stringify({
chat: chatHistory,
- // Optional fields the backend supports:
- // result: [], // Previous results if needed
- // instructions: "" // Additional instructions if needed
+ result: resultHistory.length > 0 ? resultHistory : undefined,
+ ...(request.use_user_rules !== undefined && {
+ use_user_rules: request.use_user_rules
+ }),
+ ...(request.use_memory !== undefined && {
+ use_memory: request.use_memory
+ })
}),
credentials: 'include',
});
diff --git a/app/src/services/database.ts b/app/src/services/database.ts
index 15ca9ceb..1e069df1 100644
--- a/app/src/services/database.ts
+++ b/app/src/services/database.ts
@@ -190,8 +190,21 @@ export class DatabaseService {
});
if (!response.ok) {
+ const errorMessages: Record = {
+ 401: 'Not authenticated. Please sign in to connect databases.',
+ 403: 'Access denied. You do not have permission to connect databases.',
+ 500: 'Server error. Please try again later.',
+ };
+
+ // For 400, try to get server error message first
+ if (response.status === 400) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(errorData.error || 'Invalid database connection URL.');
+ }
+
+ // Try to get error from response body, fallback to status message
const errorData = await response.json().catch(() => ({}));
- throw new Error(errorData.error || 'Failed to connect to database');
+ throw new Error(errorData.error || errorMessages[response.status] || `Failed to connect to database (${response.status})`);
}
const data = await response.json();
@@ -235,8 +248,21 @@ export class DatabaseService {
});
if (!response.ok) {
+ const errorMessages: Record = {
+ 401: 'Not authenticated. Please sign in to connect databases.',
+ 403: 'Access denied. You do not have permission to connect databases.',
+ 500: 'Server error. Please try again later.',
+ };
+
+ // For 400, try to get server error message first
+ if (response.status === 400) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(errorData.error || 'Invalid database connection URL.');
+ }
+
+ // Try to get error from response body, fallback to status message
const errorData = await response.json().catch(() => ({}));
- throw new Error(errorData.error || 'Failed to connect to database');
+ throw new Error(errorData.error || errorMessages[response.status] || `Failed to connect to database (${response.status})`);
}
const data = await response.json();
@@ -250,5 +276,66 @@ export class DatabaseService {
throw error;
}
}
+
+ /**
+ * Get user rules for a specific database
+ */
+ static async getUserRules(graphId: string): Promise {
+ try {
+ const url = buildApiUrl(`${API_CONFIG.ENDPOINTS.GRAPHS}/${graphId}/user-rules`);
+
+ const response = await fetch(url, {
+ credentials: 'include',
+ });
+
+ if (!response.ok) {
+ if (response.status === 401 || response.status === 403) {
+ return '';
+ }
+ throw new Error('Failed to fetch user rules');
+ }
+
+ const data = await response.json();
+ return data.user_rules || '';
+ } catch (error) {
+ console.error('Error fetching user rules:', error);
+ return '';
+ }
+ }
+
+ /**
+ * Update user rules for a specific database
+ */
+ static async updateUserRules(graphId: string, userRules: string): Promise {
+ try {
+ console.log('Updating user rules for graph:', graphId, 'Length:', userRules.length);
+ const url = buildApiUrl(`${API_CONFIG.ENDPOINTS.GRAPHS}/${graphId}/user-rules`);
+ console.log('PUT request to:', url);
+
+ const response = await fetch(url, {
+ method: 'PUT',
+ credentials: 'include',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ user_rules: userRules }),
+ });
+
+ console.log('Update user rules response status:', response.status);
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ console.error('Failed to update user rules:', errorData);
+ throw new Error(errorData.error || 'Failed to update user rules');
+ }
+
+ const result = await response.json();
+ console.log('User rules updated successfully:', result);
+ } catch (error) {
+ console.error('Error updating user rules:', error);
+ throw error;
+ }
+ }
}
+export const databaseService = DatabaseService;
diff --git a/app/src/types/api.ts b/app/src/types/api.ts
index 04e46ede..ec96132c 100644
--- a/app/src/types/api.ts
+++ b/app/src/types/api.ts
@@ -37,6 +37,8 @@ export interface ChatRequest {
query: string;
database: string;
history?: ConversationMessage[];
+ use_user_rules?: boolean; // If true, backend fetches rules from database
+ use_memory?: boolean;
}
export interface ConversationMessage {
@@ -87,6 +89,7 @@ export interface ConfirmRequest {
sql_query: string; // The SQL query to execute
confirmation: string; // "CONFIRM" or "" (empty for cancel)
chat: string[]; // Conversation history
+ use_user_rules?: boolean; // If true, backend fetches rules from database
}
// Upload types
diff --git a/app/tailwind.config.ts b/app/tailwind.config.ts
index 7ef8e59a..c344154f 100644
--- a/app/tailwind.config.ts
+++ b/app/tailwind.config.ts
@@ -1,7 +1,7 @@
import type { Config } from "tailwindcss";
export default {
- darkMode: ["class"],
+ darkMode: ["selector", "[data-theme='dark']"],
content: [
"./index.html",
"./src/**/*.{ts,tsx,js,jsx}",
@@ -21,53 +21,65 @@ export default {
mono: ['Fira Code', 'monospace'],
},
colors: {
- border: "hsl(var(--border))",
- input: "hsl(var(--input))",
- ring: "hsl(var(--ring))",
- background: "hsl(var(--background))",
- foreground: "hsl(var(--foreground))",
+ border: "var(--border)",
+ input: "var(--input)",
+ ring: "var(--ring)",
+ background: "var(--background)",
+ foreground: "var(--foreground)",
primary: {
- DEFAULT: "hsl(var(--primary))",
- foreground: "hsl(var(--primary-foreground))",
- glow: "hsl(var(--primary-glow))",
- dark: "hsl(var(--primary-dark))",
+ DEFAULT: "var(--primary)",
+ foreground: "var(--primary-foreground)",
},
secondary: {
- DEFAULT: "hsl(var(--secondary))",
- foreground: "hsl(var(--secondary-foreground))",
+ DEFAULT: "var(--secondary)",
+ foreground: "var(--secondary-foreground)",
},
destructive: {
- DEFAULT: "hsl(var(--destructive))",
- foreground: "hsl(var(--destructive-foreground))",
+ DEFAULT: "var(--destructive)",
+ foreground: "var(--destructive-foreground)",
},
muted: {
- DEFAULT: "hsl(var(--muted))",
- foreground: "hsl(var(--muted-foreground))",
+ DEFAULT: "var(--muted)",
+ foreground: "var(--muted-foreground)",
+ },
+ success: {
+ DEFAULT: "var(--success)",
+ foreground: "var(--success-foreground)",
+ },
+ warning: {
+ DEFAULT: "var(--warning)",
+ foreground: "var(--warning-foreground)",
+ },
+ error: {
+ DEFAULT: "var(--error)",
+ foreground: "var(--error-foreground)",
+ },
+ info: {
+ DEFAULT: "var(--info)",
+ foreground: "var(--info-foreground)",
},
accent: {
- DEFAULT: "hsl(var(--accent))",
- foreground: "hsl(var(--accent-foreground))",
+ DEFAULT: "var(--accent)",
+ foreground: "var(--accent-foreground)",
},
popover: {
- DEFAULT: "hsl(var(--popover))",
- foreground: "hsl(var(--popover-foreground))",
+ DEFAULT: "var(--popover)",
+ foreground: "var(--popover-foreground)",
},
card: {
- DEFAULT: "hsl(var(--card))",
- foreground: "hsl(var(--card-foreground))",
+ DEFAULT: "var(--card)",
+ foreground: "var(--card-foreground)",
},
sidebar: {
- DEFAULT: "hsl(var(--sidebar-background))",
- foreground: "hsl(var(--sidebar-foreground))",
- primary: "hsl(var(--sidebar-primary))",
- "primary-foreground": "hsl(var(--sidebar-primary-foreground))",
- accent: "hsl(var(--sidebar-accent))",
- "accent-foreground": "hsl(var(--sidebar-accent-foreground))",
- border: "hsl(var(--sidebar-border))",
- ring: "hsl(var(--sidebar-ring))",
+ DEFAULT: "var(--sidebar)",
+ foreground: "var(--sidebar-foreground)",
+ primary: "var(--sidebar-primary)",
+ "primary-foreground": "var(--sidebar-primary-foreground)",
+ accent: "var(--sidebar-accent)",
+ "accent-foreground": "var(--sidebar-accent-foreground)",
+ border: "var(--sidebar-border)",
+ ring: "var(--sidebar-ring)",
},
- "google-blue": "hsl(var(--google-blue))",
- "github-dark": "hsl(var(--github-dark))",
},
borderRadius: {
lg: "var(--radius)",
diff --git a/e2e/tests/chat.spec.ts b/e2e/tests/chat.spec.ts
index 1fb92b67..239288c4 100644
--- a/e2e/tests/chat.spec.ts
+++ b/e2e/tests/chat.spec.ts
@@ -230,14 +230,15 @@ test.describe('Chat Feature Tests', () => {
// Ensure database is connected
await homePage.ensureDatabaseConnected(apiCall);
- // Generate random username to avoid conflicts
+ // Generate random username and email to avoid conflicts
const randomUsername = `testuser${Date.now()}`;
+ const randomEmail = `${randomUsername}@test.com`;
// Send INSERT query
- await homePage.sendQuery(`add one user "${randomUsername}"`);
+ await homePage.sendQuery(`add one user "${randomUsername}" with email "${randomEmail}"`);
- // Wait for confirmation message to appear
- const confirmationAppeared = await homePage.waitForConfirmationMessage(15000);
+ // Wait for confirmation message to appear (increased timeout for slow CI)
+ const confirmationAppeared = await homePage.waitForConfirmationMessage(20000);
expect(confirmationAppeared).toBeTruthy();
// Verify confirmation message is visible
@@ -271,20 +272,25 @@ test.describe('Chat Feature Tests', () => {
// Ensure database is connected
await homePage.ensureDatabaseConnected(apiCall);
const randomUsername = `testuser${Date.now()}`;
+ const randomEmail = `${randomUsername}@test.com`;
+
// First insertion - should succeed
- await homePage.sendQuery(`add one user "${randomUsername}"`);
- await homePage.waitForConfirmationMessage(10000);
+ await homePage.sendQuery(`add one user "${randomUsername}" with email "${randomEmail}"`);
+ const confirmationAppeared1 = await homePage.waitForConfirmationMessage(20000);
+ expect(confirmationAppeared1).toBeTruthy();
await homePage.clickConfirmButton();
await homePage.waitForProcessingToComplete();
// Second insertion attempt - should fail with duplicate error
- await homePage.sendQuery(`add one user "${randomUsername}"`);
- await homePage.waitForConfirmationMessage(10000);
+ await homePage.sendQuery(`add one user "${randomUsername}" with email "${randomEmail}"`);
+ const confirmationAppeared2 = await homePage.waitForConfirmationMessage(20000);
+ expect(confirmationAppeared2).toBeTruthy();
await homePage.clickConfirmButton();
await homePage.waitForProcessingToComplete();
- // Verify error message contains user-friendly text
+ // Verify error message indicates a duplicate/conflict occurred
const lastAIMessage = await homePage.getLastAIMessageText();
- expect(lastAIMessage).toContain(`"${randomUsername}" already exists`);
+ const hasErrorIndicator = lastAIMessage.toLowerCase().includes('already exists');
+ expect(hasErrorIndicator).toBeTruthy();
});
});
diff --git a/e2e/tests/userProfile.spec.ts b/e2e/tests/userProfile.spec.ts
index db62cb9a..bd4de876 100644
--- a/e2e/tests/userProfile.spec.ts
+++ b/e2e/tests/userProfile.spec.ts
@@ -72,7 +72,10 @@ test.describe('User Profile Tests', () => {
const isLogoutVisible = await userProfile.isLogoutMenuItemVisible();
expect(isLogoutVisible).toBeTruthy();
+ // We need to wait for the navigation event that occurs when the page reloads
+ const navigationPromise = page.waitForEvent('load', { timeout: 10000 });
await userProfile.clickOnLogout();
+ await navigationPromise;
// Verify user is logged out - user menu should not be visible
const isUserMenuVisible = await userProfile.isUserMenuVisible();