From 343f4086614d87743a8a90b327c0ae83d52aea18 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Wed, 3 Dec 2025 22:34:34 -0800 Subject: [PATCH] Add production Traefik labels for jeffemmett.com MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add router rules for jeffemmett.com and www.jeffemmett.com - Keep staging.jeffemmett.com for testing - Preparing for migration from Cloudflare Pages to Docker deployment 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- backlog/tasks/task-012 - dark-mode.md | 22 -- docker-compose.yml | 10 +- src/components/StandardizedToolWrapper.tsx | 73 +++- src/css/style.css | 434 ++++++++++++++++++++- src/shapes/MarkdownShapeUtil.tsx | 97 ++++- src/ui/MycelialIntelligenceBar.tsx | 247 ++++++------ src/ui/UserSettingsModal.tsx | 325 +++++++++++++-- 7 files changed, 992 insertions(+), 216 deletions(-) delete mode 100644 backlog/tasks/task-012 - dark-mode.md diff --git a/backlog/tasks/task-012 - dark-mode.md b/backlog/tasks/task-012 - dark-mode.md deleted file mode 100644 index 28b0dd8..0000000 --- a/backlog/tasks/task-012 - dark-mode.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -id: task-012 -title: Dark Mode Theme -status: To Do -assignee: [] -created_date: '2025-12-03' -labels: [feature, ui, theme] -priority: low -branch: dark-mode ---- - -## Description -Implement dark mode theme support for the canvas interface. - -## Branch Info -- **Branch**: `dark-mode` - -## Acceptance Criteria -- [ ] Create dark theme colors -- [ ] Add theme toggle -- [ ] Persist user preference -- [ ] System theme detection diff --git a/docker-compose.yml b/docker-compose.yml index 2b086b2..357d6c5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ # Canvas Website Docker Compose -# Staging deployment at staging.jeffemmett.com -# Production deployment at jeffemmett.com (once tested) +# Production: jeffemmett.com, www.jeffemmett.com +# Staging: staging.jeffemmett.com services: canvas-website: @@ -13,8 +13,12 @@ services: container_name: canvas-website restart: unless-stopped labels: - # Staging deployment - "traefik.enable=true" + # Production deployment (jeffemmett.com and www) + - "traefik.http.routers.canvas-prod.rule=Host(`jeffemmett.com`) || Host(`www.jeffemmett.com`)" + - "traefik.http.routers.canvas-prod.entrypoints=web" + - "traefik.http.services.canvas-prod.loadbalancer.server.port=80" + # Staging deployment (keep for testing) - "traefik.http.routers.canvas-staging.rule=Host(`staging.jeffemmett.com`)" - "traefik.http.routers.canvas-staging.entrypoints=web" - "traefik.http.services.canvas-staging.loadbalancer.server.port=80" diff --git a/src/components/StandardizedToolWrapper.tsx b/src/components/StandardizedToolWrapper.tsx index a6d1ab1..e485fb1 100644 --- a/src/components/StandardizedToolWrapper.tsx +++ b/src/components/StandardizedToolWrapper.tsx @@ -1,4 +1,29 @@ -import React, { useState, ReactNode, useEffect, useRef } from 'react' +import React, { useState, ReactNode, useEffect, useRef, useMemo } from 'react' + +// Hook to detect dark mode +function useIsDarkMode() { + const [isDark, setIsDark] = useState(() => { + if (typeof document !== 'undefined') { + return document.documentElement.classList.contains('dark') + } + return false + }) + + useEffect(() => { + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if (mutation.attributeName === 'class') { + setIsDark(document.documentElement.classList.contains('dark')) + } + }) + }) + + observer.observe(document.documentElement, { attributes: true }) + return () => observer.disconnect() + }, []) + + return isDark +} export interface StandardizedToolWrapperProps { /** The title to display in the header */ @@ -64,6 +89,28 @@ export const StandardizedToolWrapper: React.FC = ( const [isEditingTags, setIsEditingTags] = useState(false) const [editingTagInput, setEditingTagInput] = useState('') const tagInputRef = useRef(null) + const isDarkMode = useIsDarkMode() + + // Dark mode aware colors + const colors = useMemo(() => isDarkMode ? { + contentBg: '#1a1a1a', + tagsBg: '#252525', + tagsBorder: '#404040', + tagBg: '#4a5568', + tagText: '#e4e4e4', + addTagBg: '#4a5568', + inputBg: '#333333', + inputBorder: '#555555', + } : { + contentBg: 'white', + tagsBg: '#f8f9fa', + tagsBorder: '#e0e0e0', + tagBg: '#6b7280', + tagText: 'white', + addTagBg: '#9ca3af', + inputBg: 'white', + inputBorder: '#9ca3af', + }, [isDarkMode]) // Bring selected shape to front when it becomes selected useEffect(() => { @@ -107,13 +154,13 @@ export const StandardizedToolWrapper: React.FC = ( const wrapperStyle: React.CSSProperties = { width: typeof width === 'number' ? `${width}px` : width, height: isMinimized ? 40 : (typeof height === 'number' ? `${height}px` : height), // Minimized height is just the header - backgroundColor: "white", + backgroundColor: colors.contentBg, border: isSelected ? `2px solid ${primaryColor}` : `1px solid ${primaryColor}40`, borderRadius: "8px", overflow: "hidden", - boxShadow: isSelected - ? `0 0 0 2px ${primaryColor}40, 0 4px 8px rgba(0,0,0,0.15)` - : '0 2px 4px rgba(0,0,0,0.1)', + boxShadow: isSelected + ? `0 0 0 2px ${primaryColor}40, 0 4px 8px rgba(0,0,0,${isDarkMode ? '0.4' : '0.15'})` + : `0 2px 4px rgba(0,0,0,${isDarkMode ? '0.3' : '0.1'})`, display: 'flex', flexDirection: 'column', fontFamily: "Inter, sans-serif", @@ -210,20 +257,20 @@ export const StandardizedToolWrapper: React.FC = ( const tagsContainerStyle: React.CSSProperties = { padding: '8px 12px', - borderTop: '1px solid #e0e0e0', + borderTop: `1px solid ${colors.tagsBorder}`, display: 'flex', flexWrap: 'wrap', gap: '4px', alignItems: 'center', minHeight: '32px', - backgroundColor: '#f8f9fa', + backgroundColor: colors.tagsBg, flexShrink: 0, touchAction: 'manipulation', // Improve touch responsiveness } const tagStyle: React.CSSProperties = { - backgroundColor: '#6b7280', - color: 'white', + backgroundColor: colors.tagBg, + color: colors.tagText, padding: '4px 8px', // Increased padding for better touch target borderRadius: '12px', fontSize: '10px', @@ -237,18 +284,20 @@ export const StandardizedToolWrapper: React.FC = ( } const tagInputStyle: React.CSSProperties = { - border: '1px solid #9ca3af', + border: `1px solid ${colors.inputBorder}`, borderRadius: '12px', padding: '2px 6px', fontSize: '10px', outline: 'none', minWidth: '60px', flex: 1, + backgroundColor: colors.inputBg, + color: isDarkMode ? '#e4e4e4' : '#333', } const addTagButtonStyle: React.CSSProperties = { - backgroundColor: '#9ca3af', - color: 'white', + backgroundColor: colors.addTagBg, + color: colors.tagText, border: 'none', borderRadius: '12px', padding: '4px 10px', // Increased padding for better touch target diff --git a/src/css/style.css b/src/css/style.css index a49b4f5..125980c 100644 --- a/src/css/style.css +++ b/src/css/style.css @@ -20,9 +20,12 @@ html.dark { --code-bg: #2d2d2d; --code-color: #e4e4e4; --hover-bg: #2d2d2d; - --tool-bg: #3a3a3a; + --tool-bg: #2a2a2a; --tool-text: #e0e0e0; --tool-border: #555555; + --card-bg: #252525; + --input-bg: #333333; + --muted-text: #a1a1aa; } html, @@ -1518,4 +1521,433 @@ html.dark .people-dropdown { max-width: calc(100% - 32px); margin: 16px; } +} + +/* ======================================== + Dark Mode Comprehensive Styles + ======================================== */ + +/* Dark mode for blockquotes */ +html.dark blockquote { + background-color: #2d2d2d; + border-left-color: #555; + color: #e0e0e0; +} + +/* Dark mode for tables */ +html.dark table th, +html.dark table td { + border-color: #404040; +} + +html.dark table th { + background-color: #2d2d2d; +} + +html.dark table tr:nth-child(even) { + background-color: #252525; +} + +/* Dark mode for navigation links */ +html.dark .nav-link { + color: #60a5fa; +} + +html.dark .nav-link:hover { + background-color: #2d2d2d; + border-color: #404040; +} + +/* Dark mode for list markers */ +html.dark ol li::marker, +html.dark ul li::marker { + color: rgba(255, 255, 255, 0.4); +} + +/* Dark mode for loading indicator */ +html.dark .loading { + background-color: #2d2d2d; + border-color: #404040; + color: #e4e4e4; +} + +/* Dark mode for presentations */ +html.dark .presentation-card { + border-color: #404040; + background-color: #252525; +} + +html.dark .presentation-card:hover { + border-color: #60a5fa; +} + +html.dark .presentation-card h3 { + color: #e4e4e4; +} + +html.dark .presentation-card p { + color: #a1a1aa; +} + +html.dark .presentation-meta { + border-top-color: #404040; + background-color: #252525; +} + +html.dark .presentation-meta span { + color: #a1a1aa; +} + +html.dark .presentation-meta a { + color: #60a5fa; +} + +html.dark .presentations-info { + background-color: #252525; + border-left-color: #60a5fa; +} + +html.dark .presentations-info h3 { + color: #e4e4e4; +} + +html.dark .presentations-info p { + color: #a1a1aa; +} + +html.dark .presentation-info { + background-color: #252525; + border-left-color: #60a5fa; +} + +html.dark .presentation-info h1 { + color: #e4e4e4; +} + +html.dark .video-clips h2, +html.dark .video-section h3 { + color: #e4e4e4; +} + +html.dark .presentation-embed h2 { + color: #e4e4e4; +} + +/* Dark mode for command palette */ +html.dark [cmdk-dialog] { + background-color: #1a1a1a; + border-color: #404040; +} + +html.dark [cmdk-dialog] input { + background-color: #252525; + color: #e4e4e4; +} + +html.dark [cmdk-dialog] input:focus { + background-color: #2d2d2d; +} + +html.dark [cmdk-item]:hover { + background-color: #2d2d2d; +} + +html.dark [cmdk-item] .tlui-kbd { + border-color: #404040; +} + +/* Dark mode for lock indicator */ +html.dark .lock-indicator { + background: #2d2d2d; +} + +html.dark .lock-indicator:hover { + background: #3d3d3d; +} + +/* Dark mode for overflowing container */ +html.dark .overflowing { + background-color: #1a1a1a; +} + +/* Dark mode for tldraw html layer markdown */ +html.dark .tl-html-layer code { + background-color: #2d2d2d; + color: #e4e4e4; +} + +html.dark .tl-html-layer pre { + background-color: #1e1e2e; + color: #cdd6f4; +} + +html.dark .tl-html-layer blockquote { + border-left-color: #555; + color: #a1a1aa; +} + +html.dark .tl-html-layer th, +html.dark .tl-html-layer td { + border-color: #404040; +} + +html.dark .tl-html-layer tr:nth-child(2n) { + background-color: #252525; +} + +/* Dark mode for Mycelial Intelligence inline code */ +html.dark .mi-inline-code { + background: rgba(255, 255, 255, 0.1) !important; + color: #e4e4e4 !important; +} + +/* Dark mode for MDXEditor (Markdown tool) */ +html.dark .mdxeditor { + background-color: #1a1a1a !important; +} + +html.dark .mdxeditor [role="toolbar"] { + background: #252525 !important; + border-bottom-color: #404040 !important; +} + +html.dark .mdxeditor [role="toolbar"] button { + color: #e4e4e4 !important; +} + +html.dark .mdxeditor [role="toolbar"] button:hover { + background: #3d3d3d !important; +} + +html.dark .mdxeditor [role="toolbar"] button[data-state="on"] { + background: rgba(20, 184, 166, 0.2) !important; + color: #14b8a6 !important; +} + +html.dark .mdxeditor .mdxeditor-root-contenteditable { + background: #1a1a1a !important; +} + +html.dark .mdx-editor-content { + color: #e4e4e4 !important; +} + +html.dark .mdx-editor-content h1 { + color: #f4f4f5 !important; +} + +html.dark .mdx-editor-content h2 { + color: #e4e4e5 !important; +} + +html.dark .mdx-editor-content h3 { + color: #d4d4d5 !important; +} + +html.dark .mdx-editor-content blockquote { + background: #252525 !important; + border-left-color: #14b8a6 !important; +} + +html.dark .mdx-editor-content code { + background: #2d2d2d !important; + color: #e4e4e4 !important; +} + +html.dark .mdx-editor-content th { + background: #252525 !important; +} + +html.dark .mdx-editor-content th, +html.dark .mdx-editor-content td { + border-color: #404040 !important; +} + +html.dark .mdx-editor-content hr { + border-top-color: #404040 !important; +} + +html.dark .mdx-editor-content a { + color: #2dd4bf !important; +} + +html.dark .mdxeditor [role="toolbar"] select { + background: #252525 !important; + border-color: #404040 !important; + color: #e4e4e4 !important; +} + +/* Dark mode for StandardizedToolWrapper */ +html.dark .tool-wrapper-content { + background-color: #1a1a1a !important; +} + +/* Dark mode for UserSettingsModal inline-styled elements */ +/* Using attribute selectors to target inline-styled divs */ +html.dark .settings-modal [style*="backgroundColor: #f9fafb"], +html.dark .settings-modal [style*="background-color: #f9fafb"] { + background-color: #252525 !important; + border-color: #404040 !important; +} + +html.dark .settings-modal [style*="backgroundColor: #fef3c7"], +html.dark .settings-modal [style*="background-color: #fef3c7"] { + background-color: #3d3620 !important; + border-color: #665930 !important; +} + +html.dark .settings-modal [style*="color: #374151"] { + color: #e4e4e4 !important; +} + +html.dark .settings-modal [style*="color: #1f2937"] { + color: #f4f4f5 !important; +} + +html.dark .settings-modal [style*="color: #6b7280"] { + color: #a1a1aa !important; +} + +html.dark .settings-modal [style*="color: #92400e"] { + color: #fbbf24 !important; +} + +html.dark .settings-modal [style*="borderTop: 1px solid #e5e7eb"], +html.dark .settings-modal [style*="border-top: 1px solid #e5e7eb"] { + border-top-color: #404040 !important; +} + +html.dark .settings-modal [style*="backgroundColor: #f8fafc"], +html.dark .settings-modal [style*="background-color: #f8fafc"] { + background-color: #252525 !important; + border-color: #404040 !important; +} + +/* Dark mode for settings modal cards */ +html.dark .settings-section [style*="background-color"] { + background-color: #252525 !important; +} + +/* Dark mode for AI tool cards in settings */ +html.dark .settings-section h3 { + color: #e4e4e4 !important; +} + +/* Dark mode for chat messages in PromptShape */ +html.dark .prompt-container [style*="backgroundColor: white"], +html.dark .prompt-container [style*="background-color: white"] { + background-color: #1a1a1a !important; +} + +html.dark .prompt-container [style*="backgroundColor: #efefef"], +html.dark .prompt-container [style*="background-color: #efefef"] { + background-color: #252525 !important; +} + +html.dark .prompt-container [style*="backgroundColor: #f0f0f0"], +html.dark .prompt-container [style*="background-color: #f0f0f0"] { + background-color: #3d3d3d !important; + color: #e4e4e4 !important; +} + +/* Dark mode chat bubbles */ +html.dark [style*="backgroundColor: #f0f0f0"][style*="borderRadius: 18px"] { + background-color: #3d3d3d !important; + color: #e4e4e4 !important; +} + +/* Dark mode for ObsNote and other shapes */ +html.dark .obs-note-container, +html.dark .transcription-container, +html.dark .holon-container { + background-color: #1a1a1a !important; +} + +/* Dark mode for FathomMeetingsBrowser and ObsidianBrowser */ +html.dark .fathom-meetings-browser-container, +html.dark .obsidian-browser-container, +html.dark .holon-browser-container { + background-color: #1a1a1a !important; +} + +/* Dark mode for chat container */ +html.dark .chat-container { + background-color: #1a1a1a !important; +} + +html.dark .chat-container .messages-container { + background-color: #1a1a1a !important; +} + +html.dark .chat-container .message { + background-color: #252525 !important; + border-color: #404040 !important; + color: #e4e4e4 !important; +} + +html.dark .chat-container .message.own-message { + background-color: #1e3a5f !important; +} + +html.dark .chat-container .message-input { + background-color: #252525 !important; + border-color: #404040 !important; + color: #e4e4e4 !important; +} + +html.dark .chat-container .send-button { + background-color: #3b82f6 !important; +} + +/* Dark mode for ImageGen and VideoGen shapes */ +html.dark .image-gen-container, +html.dark .video-gen-container { + background-color: #1a1a1a !important; +} + +/* Dark mode for all input fields in tools */ +html.dark input[type="text"], +html.dark input[type="email"], +html.dark input[type="password"], +html.dark textarea, +html.dark select { + background-color: var(--input-bg) !important; + border-color: var(--tool-border) !important; + color: var(--text-color) !important; +} + +html.dark input::placeholder, +html.dark textarea::placeholder { + color: var(--muted-text) !important; +} + +/* Dark mode for error messages */ +html.dark [style*="backgroundColor: #fee"], +html.dark [style*="background-color: #fee"] { + background-color: #3d2020 !important; + border-color: #5c3030 !important; + color: #f87171 !important; +} + +/* Dark mode for success messages */ +html.dark [style*="backgroundColor: #d1fae5"], +html.dark [style*="background-color: #d1fae5"] { + background-color: #1a3d2e !important; + color: #34d399 !important; +} + +/* Dark mode for links in general */ +html.dark a:not([class]) { + color: #60a5fa; +} + +/* Ensure proper contrast for buttons in dark mode */ +html.dark button:not([class*="primary"]):not([style*="background"]) { + background-color: var(--tool-bg); + color: var(--tool-text); + border-color: var(--tool-border); +} + +html.dark button:not([class*="primary"]):not([style*="background"]):hover { + background-color: var(--hover-bg); } \ No newline at end of file diff --git a/src/shapes/MarkdownShapeUtil.tsx b/src/shapes/MarkdownShapeUtil.tsx index 8100b18..bcc9657 100644 --- a/src/shapes/MarkdownShapeUtil.tsx +++ b/src/shapes/MarkdownShapeUtil.tsx @@ -1,4 +1,4 @@ -import React, { useState, useCallback, useRef, useEffect } from 'react' +import React, { useState, useCallback, useRef, useEffect, useMemo } from 'react' import { MDXEditor, headingsPlugin, @@ -59,8 +59,17 @@ export class MarkdownShape extends BaseBoxShapeUtil { component(shape: IMarkdownShape) { const isSelected = this.editor.getSelectedShapeIds().includes(shape.id) const [isMinimized, setIsMinimized] = useState(false) + const [isToolbarMinimized, setIsToolbarMinimized] = useState(false) const editorRef = useRef(null) + // Dark mode detection + const isDarkMode = useMemo(() => { + if (typeof document !== 'undefined') { + return document.documentElement.classList.contains('dark') + } + return false + }, []) + // Use the pinning hook usePinnedToView(this.editor, shape.id, shape.props.pinnedToView) @@ -136,7 +145,7 @@ export class MarkdownShape extends BaseBoxShapeUtil { style={{ width: '100%', height: '100%', - backgroundColor: '#FFFFFF', + backgroundColor: isDarkMode ? '#1a1a1a' : '#FFFFFF', pointerEvents: 'all', overflow: 'hidden', display: 'flex', @@ -210,22 +219,43 @@ export class MarkdownShape extends BaseBoxShapeUtil { // Toolbar toolbarPlugin({ toolbarContents: () => ( - <> - - - - - - - - - - - - - <> - - +
+ + {!isToolbarMinimized && ( + <> + + + + + + + + + + + + + <> + + + )} +
) }), ]} @@ -247,7 +277,10 @@ export class MarkdownShape extends BaseBoxShapeUtil { background: #f9fafb; padding: 4px 8px; gap: 2px; - flex-wrap: wrap; + flex-wrap: nowrap; + overflow-x: auto; + overflow-y: hidden; + min-height: ${isToolbarMinimized ? '32px' : 'auto'}; } .mdxeditor [role="toolbar"] button { @@ -268,10 +301,36 @@ export class MarkdownShape extends BaseBoxShapeUtil { .mdxeditor .mdxeditor-root-contenteditable { flex: 1; overflow-y: auto; + overflow-x: hidden; padding: 12px 16px; min-height: 0; } + /* Custom scrollbar styling - vertical only, auto-hide */ + .mdxeditor .mdxeditor-root-contenteditable::-webkit-scrollbar { + width: 8px; + height: 0; /* No horizontal scrollbar */ + } + + .mdxeditor .mdxeditor-root-contenteditable::-webkit-scrollbar-track { + background: transparent; + } + + .mdxeditor .mdxeditor-root-contenteditable::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.2); + border-radius: 4px; + } + + .mdxeditor .mdxeditor-root-contenteditable::-webkit-scrollbar-thumb:hover { + background: rgba(0, 0, 0, 0.3); + } + + /* Firefox scrollbar */ + .mdxeditor .mdxeditor-root-contenteditable { + scrollbar-width: thin; + scrollbar-color: rgba(0, 0, 0, 0.2) transparent; + } + .mdx-editor-content { min-height: 100%; height: 100%; diff --git a/src/ui/MycelialIntelligenceBar.tsx b/src/ui/MycelialIntelligenceBar.tsx index 3751b7f..98585f6 100644 --- a/src/ui/MycelialIntelligenceBar.tsx +++ b/src/ui/MycelialIntelligenceBar.tsx @@ -46,8 +46,8 @@ function renderMessageContent(content: string): React.ReactNode { return ( ([]) const [lastTransform, setLastTransform] = useState(null) const [toolInputMode, setToolInputMode] = useState<{ toolType: string; shapeId: string } | null>(null) + const [isModalOpen, setIsModalOpen] = useState(false) + + // Detect when modals/dialogs are open to fade the bar + useEffect(() => { + const checkForModals = () => { + // Check for common modal/dialog overlays + const hasSettingsModal = document.querySelector('.settings-modal-overlay') !== null + const hasTldrawDialog = document.querySelector('[data-state="open"][role="dialog"]') !== null + const hasAuthModal = document.querySelector('.auth-modal-overlay') !== null + const hasPopup = document.querySelector('.profile-popup') !== null + + setIsModalOpen(hasSettingsModal || hasTldrawDialog || hasAuthModal || hasPopup) + } + + // Initial check + checkForModals() + + // Use MutationObserver to detect DOM changes + const observer = new MutationObserver(checkForModals) + observer.observe(document.body, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ['class', 'style', 'data-state'] + }) + + return () => observer.disconnect() + }, []) // Derived state: get selected tool info const selectedToolInfo = getSelectedToolInfo(selectionInfo) @@ -982,7 +1010,21 @@ export function MycelialIntelligenceBar() { }, [conversationHistory]) // Theme-aware colors - const colors = { + const colors = isDark ? { + background: 'rgba(30, 30, 30, 0.98)', + backgroundHover: 'rgba(40, 40, 40, 1)', + border: 'rgba(70, 70, 70, 0.8)', + borderHover: 'rgba(90, 90, 90, 1)', + text: '#e4e4e4', + textMuted: '#a1a1aa', + inputBg: 'rgba(50, 50, 50, 0.8)', + inputBorder: 'rgba(70, 70, 70, 1)', + inputText: '#e4e4e4', + shadow: '0 8px 32px rgba(0, 0, 0, 0.4), 0 4px 16px rgba(0, 0, 0, 0.3)', + shadowHover: '0 12px 40px rgba(0, 0, 0, 0.5), 0 6px 20px rgba(0, 0, 0, 0.4)', + userBubble: 'rgba(16, 185, 129, 0.2)', + assistantBubble: 'rgba(50, 50, 50, 0.9)', + } : { background: 'rgba(255, 255, 255, 0.98)', backgroundHover: 'rgba(255, 255, 255, 1)', border: 'rgba(229, 231, 235, 0.8)', @@ -1300,9 +1342,17 @@ export function MycelialIntelligenceBar() { // Height: taller when showing suggestion chips (single tool or 2+ selected) const showSuggestions = selectedToolInfo || (selectionInfo && selectionInfo.count > 1) const collapsedHeight = showSuggestions ? 76 : 48 - const expandedHeight = 400 + const maxExpandedHeight = 400 const barWidth = 520 // Consistent width - const height = isExpanded ? expandedHeight : collapsedHeight + + // Calculate dynamic height when expanded based on content + // Header: ~45px, Input area: ~56px, padding: ~24px = ~125px fixed + // Each message is roughly 50-80px, we'll let CSS handle the actual sizing + const hasContent = conversationHistory.length > 0 || streamingResponse + // Minimum expanded height when there's no content (just empty state) + const minExpandedHeight = 180 + // Use auto height with max constraint when expanded + const height = isExpanded ? 'auto' : collapsedHeight return (
setIsHovering(true)} onPointerLeave={() => setIsHovering(false)} @@ -1325,6 +1379,8 @@ export function MycelialIntelligenceBar() { style={{ width: '100%', height: '100%', + minHeight: isExpanded ? minExpandedHeight : collapsedHeight, + maxHeight: isExpanded ? maxExpandedHeight : collapsedHeight, background: isHovering ? colors.backgroundHover : colors.background, borderRadius: isExpanded ? '20px' : '24px', border: `1px solid ${isHovering ? colors.borderHover : colors.border}`, @@ -1600,6 +1656,7 @@ export function MycelialIntelligenceBar() { justifyContent: 'space-between', padding: '10px 14px', borderBottom: `1px solid ${colors.border}`, + flexShrink: 0, }}>
)} - {/* Follow-up suggestions for assistant messages */} - {msg.role === 'assistant' && msg.followUpSuggestions && msg.followUpSuggestions.length > 0 && ( -
-
- Next Steps -
-
- {msg.followUpSuggestions.map((suggestion, i) => ( - handleSuggestionClick(suggestion.prompt)} - /> - ))} -
-
- )} ))} @@ -1824,74 +1847,6 @@ export function MycelialIntelligenceBar() { )}
- {/* Show suggested tools while streaming if available */} - {suggestedTools.length > 0 && ( -
-
- - Suggested Tools - - {suggestedTools.length > 1 && ( - - )} -
-
- {suggestedTools.map((tool) => ( - - ))} -
-
- )} )} @@ -1909,8 +1864,8 @@ export function MycelialIntelligenceBar() {
)} - {/* Current follow-up suggestions - shown at bottom when not loading */} - {!isLoading && followUpSuggestions.length > 0 && ( + {/* Combined "Try next" section - tools + follow-up suggestions in one scrollable row */} + {!isLoading && (followUpSuggestions.length > 0 || suggestedTools.length > 0) && (
- - Try next +
+ + Try next +
+ {suggestedTools.length > 1 && ( + + )}
- {followUpSuggestions.slice(0, 4).map((suggestion, i) => ( + {/* Suggested tools first */} + {suggestedTools.map((tool) => ( + + ))} + {/* Then follow-up prompts */} + {followUpSuggestions.map((suggestion, i) => ( ('general') + // Dark mode aware colors + const colors = isDarkMode ? { + cardBg: '#252525', + cardBorder: '#404040', + text: '#e4e4e4', + textMuted: '#a1a1aa', + textHeading: '#f4f4f5', + warningBg: '#3d3620', + warningBorder: '#665930', + warningText: '#fbbf24', + successBg: '#1a3d2e', + successText: '#34d399', + errorBg: '#3d2020', + errorText: '#f87171', + localBg: '#1a3d2e', + localText: '#34d399', + gpuBg: '#1e2756', + gpuText: '#818cf8', + cloudBg: '#3d3620', + cloudText: '#fbbf24', + fallbackBg: '#2d2d2d', + fallbackText: '#a1a1aa', + legendBg: '#252525', + legendBorder: '#404040', + linkColor: '#60a5fa', + dividerColor: '#404040', + } : { + cardBg: '#f9fafb', + cardBorder: '#e5e7eb', + text: '#374151', + textMuted: '#6b7280', + textHeading: '#1f2937', + warningBg: '#fef3c7', + warningBorder: '#fcd34d', + warningText: '#92400e', + successBg: '#d1fae5', + successText: '#065f46', + errorBg: '#fee2e2', + errorText: '#991b1b', + localBg: '#d1fae5', + localText: '#065f46', + gpuBg: '#e0e7ff', + gpuText: '#3730a3', + cloudBg: '#fef3c7', + cloudText: '#92400e', + fallbackBg: '#f3f4f6', + fallbackText: '#6b7280', + legendBg: '#f8fafc', + legendBorder: '#e2e8f0', + linkColor: '#3b82f6', + dividerColor: '#e5e7eb', + } + + // Email linking state + const [emailStatus, setEmailStatus] = useState(null) + const [showEmailInput, setShowEmailInput] = useState(false) + const [emailInput, setEmailInput] = useState('') + const [emailLinkLoading, setEmailLinkLoading] = useState(false) + const [emailLinkMessage, setEmailLinkMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null) + // Check API key status const checkApiKeys = () => { const settings = localStorage.getItem("openai_api_key") @@ -119,6 +180,64 @@ export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: Use } }, [session.authed, session.username]) + // Check email status when modal opens + useEffect(() => { + const fetchEmailStatus = async () => { + if (session.authed && session.username) { + const status = await checkEmailStatus(session.username) + setEmailStatus(status) + } + } + fetchEmailStatus() + }, [session.authed, session.username]) + + // Handle email linking + const handleLinkEmail = async () => { + if (!emailInput.trim() || !session.username) return + + setEmailLinkLoading(true) + setEmailLinkMessage(null) + + try { + const result = await linkEmailToAccount(emailInput.trim(), session.username) + if (result.success) { + if (result.emailSent) { + setEmailLinkMessage({ + type: 'success', + text: 'Verification email sent! Check your inbox to confirm.' + }) + } else if (result.emailVerified) { + setEmailLinkMessage({ + type: 'success', + text: 'Email already verified and linked!' + }) + } else { + setEmailLinkMessage({ + type: 'success', + text: 'Email linked successfully!' + }) + } + setShowEmailInput(false) + setEmailInput('') + // Refresh status + const status = await checkEmailStatus(session.username) + setEmailStatus(status) + } else { + setEmailLinkMessage({ + type: 'error', + text: result.error || 'Failed to link email' + }) + } + } catch (error) { + setEmailLinkMessage({ + type: 'error', + text: 'An error occurred while linking email' + }) + } finally { + setEmailLinkLoading(false) + } + } + // Handle escape key and click outside useEffect(() => { const handleEscape = (e: KeyboardEvent) => { @@ -211,6 +330,142 @@ export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: Use {isDarkMode ? 'Dark' : 'Light'}
+ +
+ + {/* CryptID Account Section */} +

+ CryptID Account +

+ + {session.authed && session.username ? ( +
+
+ 🔐 +
+ + {session.username} + +

+ Your CryptID username - cryptographically secured +

+
+
+ + {/* Email Section */} +
+
+ ✉️ + Email Recovery + + {emailStatus?.emailVerified ? 'Verified' : emailStatus?.email ? 'Pending' : 'Not Set'} + +
+ + {emailStatus?.email && ( +

+ {emailStatus.email} + {!emailStatus.emailVerified && ' (verification pending)'} +

+ )} + +

+ Link an email to recover your account on new devices. You'll receive a verification link. +

+ + {emailLinkMessage && ( +
+ {emailLinkMessage.text} +
+ )} + + {showEmailInput ? ( +
+ setEmailInput(e.target.value)} + placeholder="Enter your email address..." + className="settings-input" + style={{ width: '100%', marginBottom: '8px' }} + onKeyDown={(e) => { + if (e.key === 'Enter' && emailInput.trim()) { + handleLinkEmail() + } else if (e.key === 'Escape') { + setShowEmailInput(false) + setEmailInput('') + } + }} + autoFocus + disabled={emailLinkLoading} + /> +
+ + +
+
+ ) : ( + + )} +
+
+ ) : ( +
+

+ Sign in to manage your CryptID account settings +

+
+ )}
)} @@ -218,10 +473,10 @@ export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: Use
{/* AI Tools Overview */}
-

+

AI Tools & Models

-

+

Each tool uses optimized AI models. Local models run on your private server for free, cloud models require API keys.

@@ -231,24 +486,24 @@ export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: Use key={tool.id} style={{ padding: '12px', - backgroundColor: '#f9fafb', + backgroundColor: colors.cardBg, borderRadius: '8px', - border: '1px solid #e5e7eb', + border: `1px solid ${colors.cardBorder}`, }} >
{tool.icon} - {tool.name} + {tool.name}
-

{tool.description}

+

{tool.description}

@@ -260,8 +515,8 @@ export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: Use fontSize: '10px', padding: '3px 8px', borderRadius: '12px', - backgroundColor: '#f3f4f6', - color: '#6b7280', + backgroundColor: colors.fallbackBg, + color: colors.fallbackText, fontWeight: '500', }} > @@ -295,18 +550,18 @@ export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: Use {/* Model type legend */} -
-
+
+
- + Local (Free) - + GPU (RunPod) - + Cloud (API Key)
@@ -317,7 +572,7 @@ export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: Use {activeTab === 'integrations' && (
{/* Knowledge Management Section */} -

+

Knowledge Management

@@ -325,17 +580,17 @@ export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: Use
📁
- Obsidian Vault (Local) -

+ Obsidian Vault (Local) +

Import notes directly from your local Obsidian vault

@@ -344,7 +599,7 @@ export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: Use
{session.obsidianVaultName && ( -

+

Current vault: {session.obsidianVaultName}

)} @@ -357,17 +612,17 @@ export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: Use
🌐
- Obsidian Quartz (Web) -

+ Obsidian Quartz (Web) +

Import notes from your published Quartz site via GitHub

@@ -375,7 +630,7 @@ export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: Use Available
-

+

Quartz is a static site generator for Obsidian. If you publish your notes with Quartz, you can browse and import them here.

@@ -395,7 +650,7 @@ export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: Use
{/* Meeting & Communication Section */} -

+

Meeting & Communication

@@ -403,16 +658,16 @@ export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: Use
🎥
- Fathom Meetings -

+ Fathom Meetings +

Import meeting transcripts and AI summaries

@@ -476,7 +731,7 @@ export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: Use style={{ display: 'block', fontSize: '11px', - color: '#3b82f6', + color: colors.linkColor, textDecoration: 'none', marginTop: '8px', }} @@ -513,8 +768,8 @@ export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: Use
{/* Future Integrations Placeholder */} -
-

+

+

More integrations coming soon: Google Calendar, Notion, and more