From 79b22a97e3f94a73aed9ff92448cbdf9f98c0805 Mon Sep 17 00:00:00 2001 From: Zach McNair Date: Tue, 16 Dec 2025 15:04:42 -0600 Subject: [PATCH 1/4] feat(app): improve UI styling and accessibility - Replace Work Sans font with Inter for better readability - Update dark mode color scheme for better contrast - Improve dropdown styling with proper background colors - Add max-width constraint (640px) to chat messages - Update selected chat item styling in light mode - Reduce sidebar navigation text size by 2pt - Fix orphaned words in setup completion modal - Update logo to use dark mode variant when appropriate --- .changeset/ui-improvements.md | 16 + .../fonts/Inter-VariableFont_slnt,wght.ttf | 1439 +++++++++++++++++ .../icons/think-os-agent-dark-mode-filled.svg | 19 + app/public/icons/think-os-agent-dark-mode.svg | 69 + app/src/SetupWizard.tsx | 5 +- app/src/components/ChatMessageList.tsx | 42 +- app/src/components/ChatSidebar.tsx | 9 +- app/src/components/ModelSelector.tsx | 2 +- app/src/components/Sidebar.tsx | 9 +- app/src/index.css | 40 +- app/src/pages/ChatPage.tsx | 2 +- app/src/pages/HomePage.tsx | 2 +- app/tailwind.config.js | 2 +- .../fonts/Inter-VariableFont_slnt,wght.ttf | 1439 +++++++++++++++++ extension/public/manifest.json | 2 +- extension/src/content.css | 2 +- extension/src/content.tsx | 7 +- extension/tailwind.config.js | 2 +- 18 files changed, 3056 insertions(+), 52 deletions(-) create mode 100644 .changeset/ui-improvements.md create mode 100644 app/public/fonts/Inter-VariableFont_slnt,wght.ttf create mode 100644 app/public/icons/think-os-agent-dark-mode-filled.svg create mode 100644 app/public/icons/think-os-agent-dark-mode.svg create mode 100644 extension/public/fonts/Inter-VariableFont_slnt,wght.ttf diff --git a/.changeset/ui-improvements.md b/.changeset/ui-improvements.md new file mode 100644 index 0000000..0404def --- /dev/null +++ b/.changeset/ui-improvements.md @@ -0,0 +1,16 @@ +--- +"think-app": minor +"think-extension": minor +--- + +Improve UI styling and accessibility across the application: + +- Replace Work Sans font with Inter for better readability +- Update dark mode color scheme for better contrast and accessibility +- Improve dropdown styling with proper background colors +- Add max-width constraint (640px) to chat messages for better readability +- Update selected chat item styling in light mode +- Reduce sidebar navigation text size by 2pt +- Fix orphaned words in setup completion modal +- Update logo to use dark mode variant when appropriate + diff --git a/app/public/fonts/Inter-VariableFont_slnt,wght.ttf b/app/public/fonts/Inter-VariableFont_slnt,wght.ttf new file mode 100644 index 0000000..eb32646 --- /dev/null +++ b/app/public/fonts/Inter-VariableFont_slnt,wght.ttf @@ -0,0 +1,1439 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Page not found · GitHub · GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + +
+ Skip to content + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+ + + + + +
+ + + + + + + + + +
+
+ + + +
+
+ +
+
+ 404 “This is not the web page you are looking for” + + + + + + + + + + + + +
+
+ +
+
+ +
+ + +
+
+ +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + diff --git a/app/public/icons/think-os-agent-dark-mode-filled.svg b/app/public/icons/think-os-agent-dark-mode-filled.svg new file mode 100644 index 0000000..6d0bd8a --- /dev/null +++ b/app/public/icons/think-os-agent-dark-mode-filled.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/public/icons/think-os-agent-dark-mode.svg b/app/public/icons/think-os-agent-dark-mode.svg new file mode 100644 index 0000000..f03cd81 --- /dev/null +++ b/app/public/icons/think-os-agent-dark-mode.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/SetupWizard.tsx b/app/src/SetupWizard.tsx index c8618f4..7c1185d 100644 --- a/app/src/SetupWizard.tsx +++ b/app/src/SetupWizard.tsx @@ -237,8 +237,9 @@ export default function SetupWizard({ onComplete }: Props) {

- Ollama is running and models are ready.
- You can now use Think with local AI. + Ollama is running and models are ready. +
+ You can now use Think with local AI.

diff --git a/app/tailwind.config.js b/app/tailwind.config.js index fa050d3..690495f 100644 --- a/app/tailwind.config.js +++ b/app/tailwind.config.js @@ -9,7 +9,7 @@ export default { extend: { fontFamily: { sans: [ - 'Work Sans', + 'Inter', '-apple-system', 'BlinkMacSystemFont', 'sans-serif' diff --git a/extension/public/fonts/Inter-VariableFont_slnt,wght.ttf b/extension/public/fonts/Inter-VariableFont_slnt,wght.ttf new file mode 100644 index 0000000..eb32646 --- /dev/null +++ b/extension/public/fonts/Inter-VariableFont_slnt,wght.ttf @@ -0,0 +1,1439 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Page not found · GitHub · GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + +
+ Skip to content + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+ + + + + +
+ + + + + + + + + +
+
+ + + +
+
+ +
+
+ 404 “This is not the web page you are looking for” + + + + + + + + + + + + +
+
+ +
+
+ +
+ + +
+
+ +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + diff --git a/extension/public/manifest.json b/extension/public/manifest.json index 1372264..5636c7f 100644 --- a/extension/public/manifest.json +++ b/extension/public/manifest.json @@ -43,7 +43,7 @@ "resources": [ "assets/*", "branding/*", - "fonts/WorkSans-VariableFont_wght.ttf", + "fonts/Inter-VariableFont_slnt,wght.ttf", "fonts/GoudyBookletter1911-Regular.ttf", "popup.css" ], diff --git a/extension/src/content.css b/extension/src/content.css index 870a9a4..e1c576d 100644 --- a/extension/src/content.css +++ b/extension/src/content.css @@ -72,7 +72,7 @@ height: 100vh !important; z-index: 2147483647 !important; box-shadow: -4px 0 12px rgba(0, 0, 0, 0.15); - font-family: 'Work Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; background-color: hsl(var(--background)); diff --git a/extension/src/content.tsx b/extension/src/content.tsx index 0c51cab..1bca5c5 100644 --- a/extension/src/content.tsx +++ b/extension/src/content.tsx @@ -115,15 +115,16 @@ function init() { } // Get font URLs using chrome.runtime.getURL - const workSansUrl = chrome.runtime.getURL('fonts/WorkSans-VariableFont_wght.ttf'); + const interUrl = chrome.runtime.getURL('fonts/Inter-VariableFont_slnt,wght.ttf'); const goudyUrl = chrome.runtime.getURL('fonts/GoudyBookletter1911-Regular.ttf'); // Build @font-face CSS const fontFaceCSS = '@font-face {' + - "font-family: 'Work Sans';" + - "src: url('" + workSansUrl + "') format('truetype');" + + "font-family: 'Inter';" + + "src: url('" + interUrl + "') format('truetype');" + 'font-weight: 100 900;' + + 'font-style: oblique -10deg 0deg;' + 'font-display: swap;' + '}' + '@font-face {' + diff --git a/extension/tailwind.config.js b/extension/tailwind.config.js index 38a0e1b..bcf5b65 100644 --- a/extension/tailwind.config.js +++ b/extension/tailwind.config.js @@ -9,7 +9,7 @@ export default { extend: { fontFamily: { sans: [ - 'Work Sans', + 'Inter', '-apple-system', 'BlinkMacSystemFont', 'sans-serif' From 258f4bf32c4d409cae4ed4acbd1d99658a264652 Mon Sep 17 00:00:00 2001 From: Zach McNair Date: Tue, 16 Dec 2025 15:34:06 -0600 Subject: [PATCH 2/4] feat: add title bar with pin and delete buttons for selected conversations --- app/src/pages/ChatPage.tsx | 69 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 67 insertions(+), 2 deletions(-) diff --git a/app/src/pages/ChatPage.tsx b/app/src/pages/ChatPage.tsx index f6c27e1..42048f4 100644 --- a/app/src/pages/ChatPage.tsx +++ b/app/src/pages/ChatPage.tsx @@ -1,12 +1,15 @@ -import { useState, useEffect, useCallback, useRef } from "react"; +import { useState, useEffect, useCallback, useRef, useMemo } from "react"; +import { FileText, Pin, Trash2 } from "lucide-react"; import { apiFetch } from "@/lib/api"; import { ChatInput } from "@/components/ChatInput"; import { ChatMessageList } from "@/components/ChatMessageList"; import { ChatSidebar } from "@/components/ChatSidebar"; import { ChatSourcesPanel } from "@/components/ChatSourcesPanel"; import { ContextUsageIndicator } from "@/components/ContextUsageIndicator"; +import { Button } from "@/components/ui/button"; import { useConversation } from "@/contexts/ConversationContext"; import { useConversations } from "@/hooks/useConversations"; +import { cn } from "@/lib/utils"; import type { ChatMessage } from "@/types/chat"; export default function ChatPage() { @@ -40,7 +43,7 @@ export default function ChatPage() { updateContextWindow, } = useConversation(); - const { conversations } = useConversations(); + const { conversations, deleteConversation, togglePinConversation } = useConversations(); // Wrapper for starting new chat that prevents auto-load from immediately re-selecting const handleStartNewChat = useCallback(() => { @@ -48,6 +51,27 @@ export default function ChatPage() { startNewChat(); }, [startNewChat]); + // Get current conversation details + const currentConversation = useMemo(() => { + if (!currentConversationId) return null; + return conversations.find((c) => c.id === currentConversationId) || null; + }, [currentConversationId, conversations]); + + // Handle delete conversation + const handleDeleteConversation = useCallback(async () => { + if (!currentConversationId) return; + const success = await deleteConversation(currentConversationId); + if (success) { + handleStartNewChat(); + } + }, [currentConversationId, deleteConversation, handleStartNewChat]); + + // Handle toggle pin + const handleTogglePin = useCallback(async () => { + if (!currentConversation) return; + await togglePinConversation(currentConversation.id, !currentConversation.pinned); + }, [currentConversation, togglePinConversation]); + // Core chat submission logic const submitChat = useCallback(async (messageText: string, conversationId: number | null) => { if (!messageText.trim()) return; @@ -253,6 +277,47 @@ export default function ChatPage() { {/* Chat content */}
+ {/* Title bar - shown when a conversation is selected */} + {currentConversation && ( +
+
+ +
+ + {currentConversation.title || "New conversation"} + +
+
+ + +
+
+
+ )} + {/* Messages area */} {isLoadingMessages ? (
From d058952c555241705ba2ef4654edd9f9784fe68e Mon Sep 17 00:00:00 2001 From: Zach McNair Date: Wed, 17 Dec 2025 15:12:26 -0600 Subject: [PATCH 3/4] feat: design token updates, extension styling sync, logout button, and UI improvements - Replace hardcoded colors with design tokens across app and extension - Sync extension dark mode colors with app (blue-tinted grays) - Update extension popup and sidebar styling to match app design - Fix background gradients to use CSS variables consistently - Add logout button to settings page (floats right of title) - Fix animated background orb colors to use correct blue hue - Add protocol handler (think://) for opening app from extension - Update extension icons for light/dark modes - Improve font loading in extension popup - Fix button width consistency in extension popup --- app/electron/main.js | 74 ++++++ app/src/LockScreen.tsx | 8 +- app/src/SetupWizard.tsx | 8 +- app/src/components/ChatSidebar.tsx | 4 +- app/src/components/ModelSelector.tsx | 2 +- app/src/index.css | 2 +- app/src/pages/SettingsPage.tsx | 39 +++- backend/app/db/__init__.py | 3 +- backend/app/db/core.py | 12 + backend/app/main.py | 2 +- backend/app/native_messaging.py | 10 + backend/app/routes/auth.py | 9 +- extension/popup.html | 58 +++++ .../icons/think-os-agent-dark-mode-filled.svg | 19 ++ .../public/icons/think-os-agent-grey-blue.svg | 15 ++ extension/src/content.css | 32 ++- extension/src/index.css | 44 +++- extension/src/popup.tsx | 215 +++++++++++++----- extension/src/sidebar/ChatSidebar.tsx | 14 +- 19 files changed, 470 insertions(+), 100 deletions(-) create mode 100644 extension/public/icons/think-os-agent-dark-mode-filled.svg create mode 100644 extension/public/icons/think-os-agent-grey-blue.svg diff --git a/app/electron/main.js b/app/electron/main.js index d42a8d1..2c5f75d 100644 --- a/app/electron/main.js +++ b/app/electron/main.js @@ -224,6 +224,38 @@ app.whenReady().then(async () => { // Set app name (for dev mode - production uses productName from package.json) app.setName('Think'); + // Register protocol handler for think:// URLs + if (!app.isDefaultProtocolClient('think')) { + app.setAsDefaultProtocolClient('think'); + } + + // Handle think:// protocol URLs + app.on('open-url', (event, url) => { + event.preventDefault(); + handleProtocolUrl(url); + }); + + // Handle think:// protocol URLs on Windows/Linux (second-instance) + const gotTheLock = app.requestSingleInstanceLock(); + if (!gotTheLock) { + app.quit(); + return; + } + + app.on('second-instance', (event, commandLine) => { + // Someone tried to run a second instance, focus our window instead + if (mainWindow) { + if (mainWindow.isMinimized()) mainWindow.restore(); + mainWindow.focus(); + } + + // Check for protocol URL in command line arguments + const protocolUrl = commandLine.find(arg => arg.startsWith('think://')); + if (protocolUrl) { + handleProtocolUrl(protocolUrl); + } + }); + // Set dock icon on macOS (for dev mode) if (process.platform === 'darwin' && !app.isPackaged) { const iconPath = path.join(__dirname, '../public/icons/think-os-agent.png'); @@ -280,9 +312,51 @@ app.on('activate', () => { mainWindow.webContents.send('backend-ready', { token: APP_TOKEN }); }); } + } else { + // Bring window to front if it exists + if (mainWindow) { + if (mainWindow.isMinimized()) mainWindow.restore(); + mainWindow.focus(); + } } }); +/** + * Handle think:// protocol URLs + */ +function handleProtocolUrl(url) { + // Ensure window is created and visible + if (!mainWindow) { + createWindow(); + } else { + if (mainWindow.isMinimized()) mainWindow.restore(); + mainWindow.focus(); + } + + // Parse URL and send to renderer + // Format: think://memories/123 or think://chats/456 + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.once('did-finish-load', () => { + mainWindow.webContents.send('protocol-url', { url }); + }); + // If already loaded, send immediately + if (mainWindow.webContents.isLoading() === false) { + mainWindow.webContents.send('protocol-url', { url }); + } + } +} + +// Handle protocol URLs passed as command line arguments (Windows/Linux) +if (process.platform !== 'darwin') { + const protocolUrl = process.argv.find(arg => arg.startsWith('think://')); + if (protocolUrl) { + // Will be handled after app.whenReady() + app.once('ready', () => { + handleProtocolUrl(protocolUrl); + }); + } +} + // IPC handlers ipcMain.handle('check-ollama', async () => { // Check if API is responding (running) diff --git a/app/src/LockScreen.tsx b/app/src/LockScreen.tsx index e6622f2..f1e0a27 100644 --- a/app/src/LockScreen.tsx +++ b/app/src/LockScreen.tsx @@ -91,16 +91,16 @@ export default function LockScreen({ needsSetup, onUnlock }: Props) { return (
{/* Animated gradient background orbs */}
-
-
+
+
-
+
{/* Logo */}
+
{/* Animated gradient background orbs */}
-
-
+
+
-
+
Think Think diff --git a/app/src/components/ChatSidebar.tsx b/app/src/components/ChatSidebar.tsx index af0bb74..4c7de3f 100644 --- a/app/src/components/ChatSidebar.tsx +++ b/app/src/components/ChatSidebar.tsx @@ -54,14 +54,14 @@ export function ChatSidebar({ onNewChat }: ChatSidebarProps) { className={cn( "group flex items-center gap-2 px-2 py-2 rounded-md text-sm cursor-pointer transition-colors", currentConversationId === conversation.id - ? "bg-primary/5 dark:bg-[#242633] text-foreground" + ? "bg-primary/5 dark:bg-secondary text-foreground" : "text-muted-foreground hover:text-foreground hover:bg-muted/50" )} > diff --git a/app/src/components/ModelSelector.tsx b/app/src/components/ModelSelector.tsx index 859a384..e8e02c2 100644 --- a/app/src/components/ModelSelector.tsx +++ b/app/src/components/ModelSelector.tsx @@ -189,7 +189,7 @@ export function ModelSelector({ type = "chat", provider, selectedModel, onModelC key={model.name} className={cn( "flex items-center justify-between px-3 py-2.5 hover:bg-muted/50 cursor-pointer", - model.name === displayModel && "bg-muted dark:bg-[#242633] text-foreground" + model.name === displayModel && "bg-muted dark:bg-secondary text-foreground" )} onClick={() => model.is_downloaded && selectModel(model.name)} > diff --git a/app/src/index.css b/app/src/index.css index f19cd45..a305e78 100644 --- a/app/src/index.css +++ b/app/src/index.css @@ -112,7 +112,7 @@ :root.dark body { background-image: - linear-gradient(to bottom right, #0D0D0D 0%, #0A0D21 100%), + linear-gradient(to bottom right, hsl(var(--background)) 0%, hsl(var(--card)) 100%), radial-gradient(at 0% 0%, hsl(192 61% 59% / 0.1) 0%, transparent 50%), radial-gradient(at 100% 100%, hsl(192 61% 59% / 0.1) 0%, transparent 50%); } diff --git a/app/src/pages/SettingsPage.tsx b/app/src/pages/SettingsPage.tsx index befa485..d0afd3c 100644 --- a/app/src/pages/SettingsPage.tsx +++ b/app/src/pages/SettingsPage.tsx @@ -4,7 +4,7 @@ import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Progress } from "@/components/ui/progress"; -import { Check, Circle, Loader2, Monitor, Sun, Moon, AlertTriangle } from "lucide-react"; +import { Check, Circle, Loader2, Monitor, Sun, Moon, AlertTriangle, LogOut } from "lucide-react"; import { Theme, setTheme, getTheme } from "@/hooks/useSystemTheme"; import { apiFetch } from "@/lib/api"; import { ModelSelector } from "@/components/ModelSelector"; @@ -312,10 +312,45 @@ export default function SettingsPage({ onNameChange }: SettingsPageProps) { // Progress from the job hook const progressPercent = reembedJob.progress; + const handleLogout = async () => { + try { + console.log("Logging out..."); + const res = await apiFetch("/api/auth/logout", { + method: "POST", + }); + + if (!res.ok) { + const errorText = await res.text(); + console.error("Logout failed:", res.status, errorText); + alert(`Failed to logout: ${res.status} ${errorText}`); + return; + } + + console.log("Logout successful, reloading..."); + // Reload the app to show the lock screen + window.location.reload(); + } catch (err) { + console.error("Failed to logout:", err); + alert(`Failed to logout: ${err instanceof Error ? err.message : String(err)}`); + } + }; + return ( <>
-

Settings

+
+

Settings

+ +
{/* Profile Section */} diff --git a/backend/app/db/__init__.py b/backend/app/db/__init__.py index 5311cdd..ce54893 100644 --- a/backend/app/db/__init__.py +++ b/backend/app/db/__init__.py @@ -1,4 +1,4 @@ -from .core import init_db, is_db_initialized, db_exists, DB_PATH +from .core import init_db, is_db_initialized, db_exists, DB_PATH, reset_db_connection from .crud import ( create_memory, get_memories, @@ -29,6 +29,7 @@ "is_db_initialized", "db_exists", "DB_PATH", + "reset_db_connection", "create_memory", "get_memories", "get_memory", diff --git a/backend/app/db/core.py b/backend/app/db/core.py index b3a19ec..2e471c9 100644 --- a/backend/app/db/core.py +++ b/backend/app/db/core.py @@ -128,3 +128,15 @@ def is_db_initialized() -> bool: def db_exists() -> bool: """Check if the database file exists (password was set).""" return DB_PATH.exists() + + +def reset_db_connection(): + """Reset the database connection (logout/lock).""" + global _engine, _session_maker, _db_key + + if _engine: + _engine.dispose() + + _engine = None + _session_maker = None + _db_key = None diff --git a/backend/app/main.py b/backend/app/main.py index 37a1be8..a721446 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -61,7 +61,7 @@ async def lifespan(app: FastAPI): ) # Paths that don't require unlock -PUBLIC_PATHS = {"/health", "/api/auth/status", "/api/auth/setup", "/api/auth/unlock"} +PUBLIC_PATHS = {"/health", "/api/auth/status", "/api/auth/setup", "/api/auth/unlock", "/api/auth/logout"} @app.middleware("http") diff --git a/backend/app/native_messaging.py b/backend/app/native_messaging.py index 4198bc0..1dea611 100644 --- a/backend/app/native_messaging.py +++ b/backend/app/native_messaging.py @@ -137,6 +137,8 @@ async def _route_request(self, request: dict) -> dict: result = await self._save_conversation(params) elif method == "chat.summarize": result = await self._summarize_chat(params) + elif method == "app.open": + result = await self._open_app(params) else: return { "id": request_id, @@ -495,6 +497,14 @@ async def _summarize_chat(self, params: dict) -> dict: return {"memory_id": result["id"], "title": title, "summary": summary} + async def _open_app(self, params: dict) -> dict: + """Handle app.open request - opens/shows the app window.""" + # This is handled by the Electron app via IPC + # The native messaging stub will forward this to Electron + # For now, just return success - the actual window opening + # happens in the Electron main process + return {"success": True} + # Global server instances _unix_server: NativeMessagingServer | None = None diff --git a/backend/app/routes/auth.py b/backend/app/routes/auth.py index 825b34a..28fc108 100644 --- a/backend/app/routes/auth.py +++ b/backend/app/routes/auth.py @@ -1,7 +1,7 @@ from fastapi import APIRouter, HTTPException from ..config import reload_settings -from ..db import init_db, is_db_initialized, db_exists +from ..db import init_db, is_db_initialized, db_exists, reset_db_connection from ..services.secrets import derive_db_key, set_api_key, get_api_key, delete_api_key from ..schemas import SetPasswordRequest, UnlockRequest, ApiKeyRequest @@ -66,3 +66,10 @@ async def remove_api_key(provider: str): """Remove an API key from the database.""" await delete_api_key(provider) return {"success": True} + + +@router.post("/auth/logout") +async def logout(): + """Logout by resetting the database connection (locks the app).""" + reset_db_connection() + return {"success": True} diff --git a/extension/popup.html b/extension/popup.html index 3dc5c5f..36e8f33 100644 --- a/extension/popup.html +++ b/extension/popup.html @@ -9,6 +9,64 @@ if (window.matchMedia('(prefers-color-scheme: dark)').matches) { document.documentElement.classList.add('dark'); } + + // Inject fonts immediately before React renders + (function() { + if (typeof chrome !== 'undefined' && chrome.runtime) { + const goudyUrl = chrome.runtime.getURL('fonts/GoudyBookletter1911-Regular.ttf'); + const interUrl = chrome.runtime.getURL('fonts/Inter-VariableFont_slnt,wght.ttf'); + + const fontFaceCSS = ` + @font-face { + font-family: 'Goudy Bookletter 1911'; + src: url('${goudyUrl}') format('truetype'); + font-weight: 400; + font-display: swap; + } + @font-face { + font-family: 'Inter'; + src: url('${interUrl}') format('truetype'); + font-weight: 100 900; + font-style: oblique -10deg 0deg; + font-display: swap; + } + `; + + const styleEl = document.createElement('style'); + styleEl.id = 'think-fonts'; + styleEl.textContent = fontFaceCSS; + document.head.appendChild(styleEl); + } else { + // Fallback: try again after a short delay + setTimeout(function() { + if (typeof chrome !== 'undefined' && chrome.runtime) { + const goudyUrl = chrome.runtime.getURL('fonts/GoudyBookletter1911-Regular.ttf'); + const interUrl = chrome.runtime.getURL('fonts/Inter-VariableFont_slnt,wght.ttf'); + + const fontFaceCSS = ` + @font-face { + font-family: 'Goudy Bookletter 1911'; + src: url('${goudyUrl}') format('truetype'); + font-weight: 400; + font-display: swap; + } + @font-face { + font-family: 'Inter'; + src: url('${interUrl}') format('truetype'); + font-weight: 100 900; + font-style: oblique -10deg 0deg; + font-display: swap; + } + `; + + const styleEl = document.createElement('style'); + styleEl.id = 'think-fonts'; + styleEl.textContent = fontFaceCSS; + document.head.appendChild(styleEl); + } + }, 10); + } + })(); diff --git a/extension/public/icons/think-os-agent-dark-mode-filled.svg b/extension/public/icons/think-os-agent-dark-mode-filled.svg new file mode 100644 index 0000000..6d0bd8a --- /dev/null +++ b/extension/public/icons/think-os-agent-dark-mode-filled.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/extension/public/icons/think-os-agent-grey-blue.svg b/extension/public/icons/think-os-agent-grey-blue.svg new file mode 100644 index 0000000..a1d75bd --- /dev/null +++ b/extension/public/icons/think-os-agent-grey-blue.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/extension/src/content.css b/extension/src/content.css index e1c576d..7d348ec 100644 --- a/extension/src/content.css +++ b/extension/src/content.css @@ -34,25 +34,25 @@ --radius: 0.5rem; } -/* Dark mode for Shadow DOM */ +/* Dark mode for Shadow DOM - matches app blue-tinted grays */ :host(.dark) { - --background: 0 0% 3.9%; - --foreground: 0 0% 98%; - --card: 0 0% 3.9%; + --background: 0 0% 5%; /* #0D0D0D - Dark gray base */ + --foreground: 0 0% 98%; /* Warm white text */ + --card: 232 53% 8%; /* #0A0D21 - Dark blue-gray cards (matches app gradient) */ --card-foreground: 0 0% 98%; - --popover: 0 0% 3.9%; + --popover: 0 0% 5%; /* #0C0C0C - Dark dropdown background */ --popover-foreground: 0 0% 98%; - --primary: 192 61% 59%; + --primary: 192 61% 59%; /* Think Turquoise - CTA color */ --primary-foreground: 0 0% 100%; - --secondary: 0 0% 14.9%; + --secondary: 232 17% 17%; /* #242633 - Matches active selection */ --secondary-foreground: 0 0% 98%; - --muted: 0 0% 14.9%; + --muted: 232 17% 17%; /* #242633 - Matches active selection for hover */ --muted-foreground: 0 0% 63.9%; - --accent: 0 0% 14.9%; + --accent: 232 17% 17%; /* #242633 - Matches active selection for hover */ --accent-foreground: 0 0% 98%; - --border: 0 0% 14.9%; - --input: 0 0% 14.9%; - --ring: 192 61% 59%; + --border: 232 8% 20%; /* #303138 - Accessible border color for dark mode */ + --input: 232 8% 20%; /* #303138 - Accessible input border for dark mode */ + --ring: 192 61% 59%; /* Think Turquoise */ --destructive: 0 62.8% 30.6%; --destructive-foreground: 0 0% 98%; } @@ -81,6 +81,14 @@ radial-gradient(at 100% 100%, hsl(192 61% 59% / 0.1) 0%, transparent 50%); } +/* Dark mode gradient for sidebar - matches app */ +:host(.dark) #think-sidebar-wrapper { + background-image: + linear-gradient(to bottom right, hsl(var(--background)) 0%, hsl(var(--card)) 100%), + radial-gradient(at 0% 0%, hsl(192 61% 59% / 0.1) 0%, transparent 50%), + radial-gradient(at 100% 100%, hsl(192 61% 59% / 0.1) 0%, transparent 50%); +} + #think-sidebar-wrapper.hidden { display: none !important; } diff --git a/extension/src/index.css b/extension/src/index.css index d209a2e..c42be98 100644 --- a/extension/src/index.css +++ b/extension/src/index.css @@ -1,3 +1,5 @@ +/* Fonts will be injected via JavaScript in popup.tsx */ + @tailwind base; @tailwind components; @tailwind utilities; @@ -6,7 +8,7 @@ /* Light mode - for both :root (popup) and :host (shadow DOM) */ :root, :host { /* Backgrounds */ - --background: 40 100% 99%; /* #FFFEFC - Warm white */ + --background: 0 0% 99.4%; /* #FDFEFD - Matches app background */ --card: 60 1% 99.8%; /* #FFFFFE - Pure white cards */ --popover: 60 1% 99.8%; @@ -16,7 +18,7 @@ /* Brand Primary */ --primary: 192 61% 59%; /* #58BED7 - Think Turquoise */ - --primary-foreground: 210 40% 98%; /* Text on primary */ + --primary-foreground: 0 0% 100%; /* White text on primary */ --ring: 192 61% 59%; /* Focus ring */ /* UI Elements */ @@ -27,6 +29,9 @@ --accent-foreground: 222.2 47.4% 11.2%; --border: 214.3 31.8% 91.4%; /* #E5E7EB equivalent */ --input: 40 100% 99%; /* #FFFEFC */ + + /* Chat button background - light gray from Figma */ + --chat-button-bg: 0 0% 92.5%; /* #ECECEC */ /* Status Colors */ --destructive: 0 84% 60%; /* Red for errors/delete */ @@ -37,25 +42,28 @@ /* Dark mode - for both .dark (popup) and :host(.dark) (shadow DOM) */ .dark, :host(.dark) { - --background: 0 0% 5%; /* Near black */ - --foreground: 48 86% 97%; /* Warm white text */ - --card: 230 30% 12%; /* Dark blue-gray cards */ - --card-foreground: 48 86% 97%; + --background: 0 0% 5%; /* #0D0D0D - Dark base from Figma */ + --foreground: 40 100% 99%; /* #FFFEFA - Warm white text from Figma */ + --card: 232 53% 8%; /* #0A0D21 - Dark blue-gray cards (matches app) */ + --card-foreground: 40 100% 99%; --popover: 230 30% 12%; - --popover-foreground: 48 86% 97%; + --popover-foreground: 40 100% 99%; --primary: 192 61% 59%; /* Same Think Turquoise */ - --primary-foreground: 210 40% 98%; - --secondary: 240 9% 15%; - --secondary-foreground: 48 86% 97%; + --primary-foreground: 0 0% 5%; /* Dark text on primary in dark mode */ + --secondary: 232 17% 17%; /* #242633 - Chat button background from Figma */ + --secondary-foreground: 40 100% 99%; --muted: 230 55% 8%; --muted-foreground: 235 9% 49%; --accent: 230 22% 12%; - --accent-foreground: 48 86% 97%; + --accent-foreground: 40 100% 99%; --border: 235 26% 16%; --input: 235 26% 16%; --ring: 192 61% 59%; /* Same Think Turquoise */ --destructive: 0 62.8% 30.6%; --destructive-foreground: 210 40% 98%; + + /* Chat button background - dark gray from Figma */ + --chat-button-bg: 232 17% 17%; /* #242633 */ } } @@ -67,7 +75,7 @@ @apply bg-background text-foreground; width: 320px; min-height: 200px; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; @@ -75,4 +83,16 @@ radial-gradient(at 0% 0%, hsl(192 61% 59% / 0.1) 0%, transparent 50%), radial-gradient(at 100% 100%, hsl(192 61% 59% / 0.1) 0%, transparent 50%); } + + /* Dark mode gradient background - uses CSS variables */ + :root.dark body { + background-image: + linear-gradient(to bottom right, hsl(var(--background)) 0%, hsl(var(--card)) 100%), + radial-gradient(at 0% 0%, hsl(192 61% 59% / 0.1) 0%, transparent 50%), + radial-gradient(at 100% 100%, hsl(192 61% 59% / 0.1) 0%, transparent 50%); + } + + h1 { + font-family: 'Goudy Bookletter 1911', Georgia, serif; + } } \ No newline at end of file diff --git a/extension/src/popup.tsx b/extension/src/popup.tsx index 460066d..aaf6dd9 100644 --- a/extension/src/popup.tsx +++ b/extension/src/popup.tsx @@ -1,9 +1,9 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import ReactDOM from 'react-dom/client'; import { Button } from '@/components/ui/button'; import { saveMemory, updateMemory, type SaveMemoryResult } from './native-client'; import { useSystemTheme } from '@/hooks/useSystemTheme'; -import { Bookmark, MessageCircle, Sparkles, RefreshCw, X } from 'lucide-react'; +import { Bookmark, RefreshCw, X } from 'lucide-react'; import './index.css'; interface DuplicateInfo { @@ -18,23 +18,59 @@ interface PendingMemory { content: string; } -function PopupHeader() { - return ( -
- Think - Think -
- ); -} - function Popup() { const [status, setStatus] = useState(''); const [saving, setSaving] = useState(false); const [duplicate, setDuplicate] = useState(null); const [pending, setPending] = useState(null); + const [pageTitle, setPageTitle] = useState(''); useSystemTheme(); + // Ensure fonts are loaded + useEffect(() => { + if (typeof chrome !== 'undefined' && chrome.runtime && !document.getElementById('think-fonts')) { + const goudyUrl = chrome.runtime.getURL('fonts/GoudyBookletter1911-Regular.ttf'); + const interUrl = chrome.runtime.getURL('fonts/Inter-VariableFont_slnt,wght.ttf'); + + const fontFaceCSS = ` + @font-face { + font-family: 'Goudy Bookletter 1911'; + src: url('${goudyUrl}') format('truetype'); + font-weight: 400; + font-display: swap; + } + @font-face { + font-family: 'Inter'; + src: url('${interUrl}') format('truetype'); + font-weight: 100 900; + font-style: oblique -10deg 0deg; + font-display: swap; + } + `; + + const styleEl = document.createElement('style'); + styleEl.id = 'think-fonts'; + styleEl.textContent = fontFaceCSS; + document.head.appendChild(styleEl); + } + }, []); + + // Fetch current page title + useEffect(() => { + const fetchPageTitle = async () => { + try { + const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); + if (tab.title) { + setPageTitle(tab.title); + } + } catch (err) { + console.error('Failed to fetch page title:', err); + } + }; + fetchPageTitle(); + }, []); + const savePage = async () => { setSaving(true); setStatus('Saving...'); @@ -103,15 +139,32 @@ function Popup() { if (duplicate) { const date = new Date(duplicate.created_at).toLocaleDateString(); return ( -
- -
- -

Page already saved

-

Saved on {date}

+
+ {/* Icon */} +
+ Think + Think
-
- -
+ + {/* Footer */} +

+ Powered by + +

); } @@ -172,43 +240,86 @@ function Popup() { }; return ( -
- - - {/* Intro section */} -
- -

- Save pages to your knowledge base or chat with AI about the content. -

+
+ {/* Icon */} +
+ Think + Think
-
- - - {status && ( -
- {status} + {/* Title */} +

+ + What would you like to do? + +

+ + {/* Page Title */} + {pageTitle && ( +
+

+ + {pageTitle} + +

+
+ )} + + {/* Buttons */} +
+ +
+ + {/* Status message */} + {status && ( +
+ {status} +
+ )} + + {/* Footer */} +

+ Powered by + +

); } diff --git a/extension/src/sidebar/ChatSidebar.tsx b/extension/src/sidebar/ChatSidebar.tsx index d7f9056..c9bbff0 100644 --- a/extension/src/sidebar/ChatSidebar.tsx +++ b/extension/src/sidebar/ChatSidebar.tsx @@ -229,7 +229,7 @@ export function ChatSidebar({ pageContent, pageUrl, pageTitle, onClose }: ChatSi return (
{/* Header */} -
+
Think Think @@ -245,7 +245,7 @@ export function ChatSidebar({ pageContent, pageUrl, pageTitle, onClose }: ChatSi {showActions && ( -
+