Add production Traefik labels for jeffemmett.com

- 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 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2025-12-03 22:34:34 -08:00
parent 2aeb2b0c34
commit 846816b1aa
7 changed files with 992 additions and 216 deletions

View File

@ -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

View File

@ -1,6 +1,6 @@
# Canvas Website Docker Compose # Canvas Website Docker Compose
# Staging deployment at staging.jeffemmett.com # Production: jeffemmett.com, www.jeffemmett.com
# Production deployment at jeffemmett.com (once tested) # Staging: staging.jeffemmett.com
services: services:
canvas-website: canvas-website:
@ -13,8 +13,12 @@ services:
container_name: canvas-website container_name: canvas-website
restart: unless-stopped restart: unless-stopped
labels: labels:
# Staging deployment
- "traefik.enable=true" - "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.rule=Host(`staging.jeffemmett.com`)"
- "traefik.http.routers.canvas-staging.entrypoints=web" - "traefik.http.routers.canvas-staging.entrypoints=web"
- "traefik.http.services.canvas-staging.loadbalancer.server.port=80" - "traefik.http.services.canvas-staging.loadbalancer.server.port=80"

View File

@ -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 { export interface StandardizedToolWrapperProps {
/** The title to display in the header */ /** The title to display in the header */
@ -64,6 +89,28 @@ export const StandardizedToolWrapper: React.FC<StandardizedToolWrapperProps> = (
const [isEditingTags, setIsEditingTags] = useState(false) const [isEditingTags, setIsEditingTags] = useState(false)
const [editingTagInput, setEditingTagInput] = useState('') const [editingTagInput, setEditingTagInput] = useState('')
const tagInputRef = useRef<HTMLInputElement>(null) const tagInputRef = useRef<HTMLInputElement>(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 // Bring selected shape to front when it becomes selected
useEffect(() => { useEffect(() => {
@ -107,13 +154,13 @@ export const StandardizedToolWrapper: React.FC<StandardizedToolWrapperProps> = (
const wrapperStyle: React.CSSProperties = { const wrapperStyle: React.CSSProperties = {
width: typeof width === 'number' ? `${width}px` : width, width: typeof width === 'number' ? `${width}px` : width,
height: isMinimized ? 40 : (typeof height === 'number' ? `${height}px` : height), // Minimized height is just the header 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`, border: isSelected ? `2px solid ${primaryColor}` : `1px solid ${primaryColor}40`,
borderRadius: "8px", borderRadius: "8px",
overflow: "hidden", overflow: "hidden",
boxShadow: isSelected boxShadow: isSelected
? `0 0 0 2px ${primaryColor}40, 0 4px 8px rgba(0,0,0,0.15)` ? `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,0.1)', : `0 2px 4px rgba(0,0,0,${isDarkMode ? '0.3' : '0.1'})`,
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
fontFamily: "Inter, sans-serif", fontFamily: "Inter, sans-serif",
@ -210,20 +257,20 @@ export const StandardizedToolWrapper: React.FC<StandardizedToolWrapperProps> = (
const tagsContainerStyle: React.CSSProperties = { const tagsContainerStyle: React.CSSProperties = {
padding: '8px 12px', padding: '8px 12px',
borderTop: '1px solid #e0e0e0', borderTop: `1px solid ${colors.tagsBorder}`,
display: 'flex', display: 'flex',
flexWrap: 'wrap', flexWrap: 'wrap',
gap: '4px', gap: '4px',
alignItems: 'center', alignItems: 'center',
minHeight: '32px', minHeight: '32px',
backgroundColor: '#f8f9fa', backgroundColor: colors.tagsBg,
flexShrink: 0, flexShrink: 0,
touchAction: 'manipulation', // Improve touch responsiveness touchAction: 'manipulation', // Improve touch responsiveness
} }
const tagStyle: React.CSSProperties = { const tagStyle: React.CSSProperties = {
backgroundColor: '#6b7280', backgroundColor: colors.tagBg,
color: 'white', color: colors.tagText,
padding: '4px 8px', // Increased padding for better touch target padding: '4px 8px', // Increased padding for better touch target
borderRadius: '12px', borderRadius: '12px',
fontSize: '10px', fontSize: '10px',
@ -237,18 +284,20 @@ export const StandardizedToolWrapper: React.FC<StandardizedToolWrapperProps> = (
} }
const tagInputStyle: React.CSSProperties = { const tagInputStyle: React.CSSProperties = {
border: '1px solid #9ca3af', border: `1px solid ${colors.inputBorder}`,
borderRadius: '12px', borderRadius: '12px',
padding: '2px 6px', padding: '2px 6px',
fontSize: '10px', fontSize: '10px',
outline: 'none', outline: 'none',
minWidth: '60px', minWidth: '60px',
flex: 1, flex: 1,
backgroundColor: colors.inputBg,
color: isDarkMode ? '#e4e4e4' : '#333',
} }
const addTagButtonStyle: React.CSSProperties = { const addTagButtonStyle: React.CSSProperties = {
backgroundColor: '#9ca3af', backgroundColor: colors.addTagBg,
color: 'white', color: colors.tagText,
border: 'none', border: 'none',
borderRadius: '12px', borderRadius: '12px',
padding: '4px 10px', // Increased padding for better touch target padding: '4px 10px', // Increased padding for better touch target

View File

@ -20,9 +20,12 @@ html.dark {
--code-bg: #2d2d2d; --code-bg: #2d2d2d;
--code-color: #e4e4e4; --code-color: #e4e4e4;
--hover-bg: #2d2d2d; --hover-bg: #2d2d2d;
--tool-bg: #3a3a3a; --tool-bg: #2a2a2a;
--tool-text: #e0e0e0; --tool-text: #e0e0e0;
--tool-border: #555555; --tool-border: #555555;
--card-bg: #252525;
--input-bg: #333333;
--muted-text: #a1a1aa;
} }
html, html,
@ -1518,4 +1521,433 @@ html.dark .people-dropdown {
max-width: calc(100% - 32px); max-width: calc(100% - 32px);
margin: 16px; 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);
} }

View File

@ -1,4 +1,4 @@
import React, { useState, useCallback, useRef, useEffect } from 'react' import React, { useState, useCallback, useRef, useEffect, useMemo } from 'react'
import { import {
MDXEditor, MDXEditor,
headingsPlugin, headingsPlugin,
@ -59,8 +59,17 @@ export class MarkdownShape extends BaseBoxShapeUtil<IMarkdownShape> {
component(shape: IMarkdownShape) { component(shape: IMarkdownShape) {
const isSelected = this.editor.getSelectedShapeIds().includes(shape.id) const isSelected = this.editor.getSelectedShapeIds().includes(shape.id)
const [isMinimized, setIsMinimized] = useState(false) const [isMinimized, setIsMinimized] = useState(false)
const [isToolbarMinimized, setIsToolbarMinimized] = useState(false)
const editorRef = useRef<MDXEditorMethods>(null) const editorRef = useRef<MDXEditorMethods>(null)
// Dark mode detection
const isDarkMode = useMemo(() => {
if (typeof document !== 'undefined') {
return document.documentElement.classList.contains('dark')
}
return false
}, [])
// Use the pinning hook // Use the pinning hook
usePinnedToView(this.editor, shape.id, shape.props.pinnedToView) usePinnedToView(this.editor, shape.id, shape.props.pinnedToView)
@ -136,7 +145,7 @@ export class MarkdownShape extends BaseBoxShapeUtil<IMarkdownShape> {
style={{ style={{
width: '100%', width: '100%',
height: '100%', height: '100%',
backgroundColor: '#FFFFFF', backgroundColor: isDarkMode ? '#1a1a1a' : '#FFFFFF',
pointerEvents: 'all', pointerEvents: 'all',
overflow: 'hidden', overflow: 'hidden',
display: 'flex', display: 'flex',
@ -210,22 +219,43 @@ export class MarkdownShape extends BaseBoxShapeUtil<IMarkdownShape> {
// Toolbar // Toolbar
toolbarPlugin({ toolbarPlugin({
toolbarContents: () => ( toolbarContents: () => (
<> <div style={{ display: 'flex', alignItems: 'center', width: '100%', gap: '4px' }}>
<UndoRedo /> <button
<Separator /> onClick={() => setIsToolbarMinimized(!isToolbarMinimized)}
<BoldItalicUnderlineToggles /> style={{
<Separator /> background: 'none',
<BlockTypeSelect /> border: 'none',
<Separator /> cursor: 'pointer',
<ListsToggle /> padding: '4px 6px',
<Separator /> fontSize: '12px',
<CreateLink /> borderRadius: '4px',
<InsertTable /> display: 'flex',
<Separator /> alignItems: 'center',
<DiffSourceToggleWrapper> justifyContent: 'center',
<></> }}
</DiffSourceToggleWrapper> title={isToolbarMinimized ? 'Expand toolbar' : 'Collapse toolbar'}
</> >
{isToolbarMinimized ? '▶' : '▼'}
</button>
{!isToolbarMinimized && (
<>
<UndoRedo />
<Separator />
<BoldItalicUnderlineToggles />
<Separator />
<BlockTypeSelect />
<Separator />
<ListsToggle />
<Separator />
<CreateLink />
<InsertTable />
<Separator />
<DiffSourceToggleWrapper>
<></>
</DiffSourceToggleWrapper>
</>
)}
</div>
) )
}), }),
]} ]}
@ -247,7 +277,10 @@ export class MarkdownShape extends BaseBoxShapeUtil<IMarkdownShape> {
background: #f9fafb; background: #f9fafb;
padding: 4px 8px; padding: 4px 8px;
gap: 2px; gap: 2px;
flex-wrap: wrap; flex-wrap: nowrap;
overflow-x: auto;
overflow-y: hidden;
min-height: ${isToolbarMinimized ? '32px' : 'auto'};
} }
.mdxeditor [role="toolbar"] button { .mdxeditor [role="toolbar"] button {
@ -268,10 +301,36 @@ export class MarkdownShape extends BaseBoxShapeUtil<IMarkdownShape> {
.mdxeditor .mdxeditor-root-contenteditable { .mdxeditor .mdxeditor-root-contenteditable {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
overflow-x: hidden;
padding: 12px 16px; padding: 12px 16px;
min-height: 0; 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 { .mdx-editor-content {
min-height: 100%; min-height: 100%;
height: 100%; height: 100%;

View File

@ -46,8 +46,8 @@ function renderMessageContent(content: string): React.ReactNode {
return ( return (
<code <code
key={j} key={j}
className="mi-inline-code"
style={{ style={{
background: 'rgba(0, 0, 0, 0.06)',
padding: '1px 4px', padding: '1px 4px',
borderRadius: '3px', borderRadius: '3px',
fontSize: '0.9em', fontSize: '0.9em',
@ -828,6 +828,34 @@ export function MycelialIntelligenceBar() {
const [followUpSuggestions, setFollowUpSuggestions] = useState<FollowUpSuggestion[]>([]) const [followUpSuggestions, setFollowUpSuggestions] = useState<FollowUpSuggestion[]>([])
const [lastTransform, setLastTransform] = useState<TransformCommand | null>(null) const [lastTransform, setLastTransform] = useState<TransformCommand | null>(null)
const [toolInputMode, setToolInputMode] = useState<{ toolType: string; shapeId: string } | null>(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 // Derived state: get selected tool info
const selectedToolInfo = getSelectedToolInfo(selectionInfo) const selectedToolInfo = getSelectedToolInfo(selectionInfo)
@ -982,7 +1010,21 @@ export function MycelialIntelligenceBar() {
}, [conversationHistory]) }, [conversationHistory])
// Theme-aware colors // 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)', background: 'rgba(255, 255, 255, 0.98)',
backgroundHover: 'rgba(255, 255, 255, 1)', backgroundHover: 'rgba(255, 255, 255, 1)',
border: 'rgba(229, 231, 235, 0.8)', 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) // Height: taller when showing suggestion chips (single tool or 2+ selected)
const showSuggestions = selectedToolInfo || (selectionInfo && selectionInfo.count > 1) const showSuggestions = selectedToolInfo || (selectionInfo && selectionInfo.count > 1)
const collapsedHeight = showSuggestions ? 76 : 48 const collapsedHeight = showSuggestions ? 76 : 48
const expandedHeight = 400 const maxExpandedHeight = 400
const barWidth = 520 // Consistent width 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 ( return (
<div <div
@ -1314,9 +1364,13 @@ export function MycelialIntelligenceBar() {
left: '50%', left: '50%',
transform: 'translateX(-50%)', transform: 'translateX(-50%)',
width: barWidth, width: barWidth,
height, height: isExpanded ? 'auto' : collapsedHeight,
zIndex: 99999, minHeight: isExpanded ? minExpandedHeight : collapsedHeight,
pointerEvents: 'auto', maxHeight: isExpanded ? maxExpandedHeight : collapsedHeight,
zIndex: isModalOpen ? 1 : 99999, // Lower z-index when modals are open
pointerEvents: isModalOpen ? 'none' : 'auto', // Disable interactions when modal is open
opacity: isModalOpen ? 0.3 : 1, // Fade when modal is open
transition: 'opacity 0.2s ease, z-index 0s',
}} }}
onPointerEnter={() => setIsHovering(true)} onPointerEnter={() => setIsHovering(true)}
onPointerLeave={() => setIsHovering(false)} onPointerLeave={() => setIsHovering(false)}
@ -1325,6 +1379,8 @@ export function MycelialIntelligenceBar() {
style={{ style={{
width: '100%', width: '100%',
height: '100%', height: '100%',
minHeight: isExpanded ? minExpandedHeight : collapsedHeight,
maxHeight: isExpanded ? maxExpandedHeight : collapsedHeight,
background: isHovering ? colors.backgroundHover : colors.background, background: isHovering ? colors.backgroundHover : colors.background,
borderRadius: isExpanded ? '20px' : '24px', borderRadius: isExpanded ? '20px' : '24px',
border: `1px solid ${isHovering ? colors.borderHover : colors.border}`, border: `1px solid ${isHovering ? colors.borderHover : colors.border}`,
@ -1600,6 +1656,7 @@ export function MycelialIntelligenceBar() {
justifyContent: 'space-between', justifyContent: 'space-between',
padding: '10px 14px', padding: '10px 14px',
borderBottom: `1px solid ${colors.border}`, borderBottom: `1px solid ${colors.border}`,
flexShrink: 0,
}}> }}>
<div style={{ <div style={{
display: 'flex', display: 'flex',
@ -1652,7 +1709,8 @@ export function MycelialIntelligenceBar() {
<div <div
ref={chatContainerRef} ref={chatContainerRef}
style={{ style={{
flex: 1, flex: '1 1 auto',
minHeight: 0, // Allow flex shrinking below content size
overflowY: 'auto', overflowY: 'auto',
padding: '12px', padding: '12px',
display: 'flex', display: 'flex',
@ -1756,41 +1814,6 @@ export function MycelialIntelligenceBar() {
</div> </div>
)} )}
{/* Follow-up suggestions for assistant messages */}
{msg.role === 'assistant' && msg.followUpSuggestions && msg.followUpSuggestions.length > 0 && (
<div
style={{
alignSelf: 'flex-start',
maxWidth: '100%',
padding: '8px',
marginTop: '6px',
}}
>
<div style={{
fontSize: '10px',
fontWeight: 500,
color: colors.textMuted,
marginBottom: '6px',
textTransform: 'uppercase',
letterSpacing: '0.05em',
}}>
Next Steps
</div>
<div style={{
display: 'flex',
flexWrap: 'wrap',
gap: '6px',
}}>
{msg.followUpSuggestions.map((suggestion, i) => (
<FollowUpChip
key={`${suggestion.label}-${i}`}
suggestion={suggestion}
onClick={() => handleSuggestionClick(suggestion.prompt)}
/>
))}
</div>
</div>
)}
</React.Fragment> </React.Fragment>
))} ))}
@ -1824,74 +1847,6 @@ export function MycelialIntelligenceBar() {
)} )}
</div> </div>
{/* Show suggested tools while streaming if available */}
{suggestedTools.length > 0 && (
<div
style={{
alignSelf: 'flex-start',
maxWidth: '100%',
padding: '10px',
marginTop: '6px',
background: 'linear-gradient(135deg, rgba(16, 185, 129, 0.05) 0%, rgba(99, 102, 241, 0.05) 100%)',
borderRadius: '12px',
border: '1px solid rgba(16, 185, 129, 0.15)',
}}
>
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: '8px',
}}>
<span style={{
fontSize: '11px',
fontWeight: 600,
color: ACCENT_COLOR,
textTransform: 'uppercase',
letterSpacing: '0.05em',
}}>
Suggested Tools
</span>
{suggestedTools.length > 1 && (
<button
onClick={(e) => {
e.stopPropagation()
handleSpawnAllTools()
}}
onPointerDown={(e) => e.stopPropagation()}
style={{
fontSize: '11px',
padding: '4px 10px',
borderRadius: '8px',
border: `1px solid ${ACCENT_COLOR}`,
background: 'transparent',
color: ACCENT_COLOR,
cursor: 'pointer',
fontWeight: 500,
transition: 'all 0.2s ease',
}}
title="Spawn all suggested tools on canvas"
>
Spawn All
</button>
)}
</div>
<div style={{
display: 'flex',
flexWrap: 'wrap',
gap: '8px',
}}>
{suggestedTools.map((tool) => (
<ToolCard
key={tool.id}
tool={tool}
onSpawn={handleSpawnTool}
isSpawned={spawnedToolIds.has(tool.id)}
/>
))}
</div>
</div>
)}
</> </>
)} )}
@ -1909,8 +1864,8 @@ export function MycelialIntelligenceBar() {
</div> </div>
)} )}
{/* Current follow-up suggestions - shown at bottom when not loading */} {/* Combined "Try next" section - tools + follow-up suggestions in one scrollable row */}
{!isLoading && followUpSuggestions.length > 0 && ( {!isLoading && (followUpSuggestions.length > 0 || suggestedTools.length > 0) && (
<div <div
style={{ style={{
alignSelf: 'flex-start', alignSelf: 'flex-start',
@ -1923,25 +1878,68 @@ export function MycelialIntelligenceBar() {
}} }}
> >
<div style={{ <div style={{
fontSize: '10px',
fontWeight: 600,
color: '#6366f1',
marginBottom: '8px',
textTransform: 'uppercase',
letterSpacing: '0.05em',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: '4px', justifyContent: 'space-between',
marginBottom: '8px',
}}> }}>
<span></span> <div style={{
Try next fontSize: '10px',
fontWeight: 600,
color: '#6366f1',
textTransform: 'uppercase',
letterSpacing: '0.05em',
display: 'flex',
alignItems: 'center',
gap: '4px',
}}>
<span></span>
Try next
</div>
{suggestedTools.length > 1 && (
<button
onClick={(e) => {
e.stopPropagation()
handleSpawnAllTools()
}}
onPointerDown={(e) => e.stopPropagation()}
style={{
fontSize: '10px',
padding: '3px 8px',
borderRadius: '6px',
border: `1px solid ${ACCENT_COLOR}`,
background: 'transparent',
color: ACCENT_COLOR,
cursor: 'pointer',
fontWeight: 500,
transition: 'all 0.2s ease',
}}
title="Spawn all suggested tools on canvas"
>
Spawn All
</button>
)}
</div> </div>
<div style={{ <div style={{
display: 'flex', display: 'flex',
flexWrap: 'wrap',
gap: '6px', gap: '6px',
overflowX: 'auto',
overflowY: 'hidden',
paddingBottom: '4px',
scrollbarWidth: 'thin',
scrollbarColor: 'rgba(99, 102, 241, 0.3) transparent',
}}> }}>
{followUpSuggestions.slice(0, 4).map((suggestion, i) => ( {/* Suggested tools first */}
{suggestedTools.map((tool) => (
<ToolCard
key={tool.id}
tool={tool}
onSpawn={handleSpawnTool}
isSpawned={spawnedToolIds.has(tool.id)}
/>
))}
{/* Then follow-up prompts */}
{followUpSuggestions.map((suggestion, i) => (
<FollowUpChip <FollowUpChip
key={`current-${suggestion.label}-${i}`} key={`current-${suggestion.label}-${i}`}
suggestion={suggestion} suggestion={suggestion}
@ -1960,6 +1958,7 @@ export function MycelialIntelligenceBar() {
gap: '8px', gap: '8px',
padding: '10px 12px', padding: '10px 12px',
borderTop: `1px solid ${colors.border}`, borderTop: `1px solid ${colors.border}`,
flexShrink: 0,
}}> }}>
<input <input
ref={inputRef} ref={inputRef}

View File

@ -3,6 +3,7 @@ import { useAuth } from "../context/AuthContext"
import { useDialogs } from "tldraw" import { useDialogs } from "tldraw"
import { SettingsDialog } from "./SettingsDialog" import { SettingsDialog } from "./SettingsDialog"
import { getFathomApiKey, saveFathomApiKey, removeFathomApiKey, isFathomApiKeyConfigured } from "../lib/fathomApiKey" import { getFathomApiKey, saveFathomApiKey, removeFathomApiKey, isFathomApiKeyConfigured } from "../lib/fathomApiKey"
import { linkEmailToAccount, checkEmailStatus, type LookupResult } from "../lib/auth/cryptidEmailService"
// AI tool model configurations // AI tool model configurations
const AI_TOOLS = [ const AI_TOOLS = [
@ -83,6 +84,66 @@ export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: Use
const [fathomApiKeyInput, setFathomApiKeyInput] = useState('') const [fathomApiKeyInput, setFathomApiKeyInput] = useState('')
const [activeTab, setActiveTab] = useState<'general' | 'ai' | 'integrations'>('general') const [activeTab, setActiveTab] = useState<'general' | 'ai' | 'integrations'>('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<LookupResult | null>(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 // Check API key status
const checkApiKeys = () => { const checkApiKeys = () => {
const settings = localStorage.getItem("openai_api_key") const settings = localStorage.getItem("openai_api_key")
@ -119,6 +180,64 @@ export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: Use
} }
}, [session.authed, session.username]) }, [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 // Handle escape key and click outside
useEffect(() => { useEffect(() => {
const handleEscape = (e: KeyboardEvent) => { const handleEscape = (e: KeyboardEvent) => {
@ -211,6 +330,142 @@ export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: Use
<span>{isDarkMode ? 'Dark' : 'Light'}</span> <span>{isDarkMode ? 'Dark' : 'Light'}</span>
</button> </button>
</div> </div>
<div className="settings-divider" />
{/* CryptID Account Section */}
<h3 style={{ fontSize: '14px', fontWeight: '600', marginBottom: '12px', color: colors.text }}>
CryptID Account
</h3>
{session.authed && session.username ? (
<div
style={{
padding: '12px',
backgroundColor: colors.cardBg,
borderRadius: '8px',
border: `1px solid ${colors.cardBorder}`,
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}>
<span style={{ fontSize: '20px' }}>🔐</span>
<div style={{ flex: 1 }}>
<span style={{ fontSize: '13px', fontWeight: '600', color: colors.textHeading }}>
{session.username}
</span>
<p style={{ fontSize: '11px', color: colors.textMuted, marginTop: '2px' }}>
Your CryptID username - cryptographically secured
</p>
</div>
</div>
{/* Email Section */}
<div style={{ marginTop: '12px', paddingTop: '12px', borderTop: `1px solid ${colors.dividerColor}` }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}>
<span style={{ fontSize: '16px' }}></span>
<span style={{ fontSize: '12px', fontWeight: '500', color: colors.text }}>Email Recovery</span>
<span
className={`status-badge ${emailStatus?.emailVerified ? 'success' : 'warning'}`}
style={{ fontSize: '10px', marginLeft: 'auto' }}
>
{emailStatus?.emailVerified ? 'Verified' : emailStatus?.email ? 'Pending' : 'Not Set'}
</span>
</div>
{emailStatus?.email && (
<p style={{ fontSize: '11px', color: emailStatus.emailVerified ? colors.successText : colors.warningText, marginBottom: '8px' }}>
{emailStatus.email}
{!emailStatus.emailVerified && ' (verification pending)'}
</p>
)}
<p style={{ fontSize: '11px', color: colors.textMuted, marginBottom: '8px', lineHeight: '1.4' }}>
Link an email to recover your account on new devices. You'll receive a verification link.
</p>
{emailLinkMessage && (
<div
style={{
padding: '8px 12px',
borderRadius: '6px',
marginBottom: '8px',
backgroundColor: emailLinkMessage.type === 'success' ? colors.successBg : colors.errorBg,
color: emailLinkMessage.type === 'success' ? colors.successText : colors.errorText,
fontSize: '11px',
}}
>
{emailLinkMessage.text}
</div>
)}
{showEmailInput ? (
<div>
<input
type="email"
value={emailInput}
onChange={(e) => 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}
/>
<div style={{ display: 'flex', gap: '8px' }}>
<button
className="settings-btn-sm primary"
style={{ flex: 1 }}
onClick={handleLinkEmail}
disabled={emailLinkLoading || !emailInput.trim()}
>
{emailLinkLoading ? 'Sending...' : 'Send Verification'}
</button>
<button
className="settings-btn-sm"
style={{ flex: 1 }}
onClick={() => {
setShowEmailInput(false)
setEmailInput('')
setEmailLinkMessage(null)
}}
disabled={emailLinkLoading}
>
Cancel
</button>
</div>
</div>
) : (
<button
className="settings-action-btn"
style={{ width: '100%' }}
onClick={() => setShowEmailInput(true)}
>
{emailStatus?.email ? 'Update Email' : 'Link Email'}
</button>
)}
</div>
</div>
) : (
<div
style={{
padding: '12px',
backgroundColor: colors.warningBg,
borderRadius: '8px',
border: `1px solid ${colors.warningBorder}`,
}}
>
<p style={{ fontSize: '12px', color: colors.warningText }}>
Sign in to manage your CryptID account settings
</p>
</div>
)}
</div> </div>
)} )}
@ -218,10 +473,10 @@ export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: Use
<div className="settings-section"> <div className="settings-section">
{/* AI Tools Overview */} {/* AI Tools Overview */}
<div style={{ marginBottom: '16px' }}> <div style={{ marginBottom: '16px' }}>
<h3 style={{ fontSize: '14px', fontWeight: '600', marginBottom: '12px', color: '#374151' }}> <h3 style={{ fontSize: '14px', fontWeight: '600', marginBottom: '12px', color: colors.text }}>
AI Tools & Models AI Tools & Models
</h3> </h3>
<p style={{ fontSize: '12px', color: '#6b7280', marginBottom: '16px', lineHeight: '1.4' }}> <p style={{ fontSize: '12px', color: colors.textMuted, marginBottom: '16px', lineHeight: '1.4' }}>
Each tool uses optimized AI models. Local models run on your private server for free, cloud models require API keys. Each tool uses optimized AI models. Local models run on your private server for free, cloud models require API keys.
</p> </p>
@ -231,24 +486,24 @@ export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: Use
key={tool.id} key={tool.id}
style={{ style={{
padding: '12px', padding: '12px',
backgroundColor: '#f9fafb', backgroundColor: colors.cardBg,
borderRadius: '8px', borderRadius: '8px',
border: '1px solid #e5e7eb', border: `1px solid ${colors.cardBorder}`,
}} }}
> >
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '6px' }}> <div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '6px' }}>
<span style={{ fontSize: '16px' }}>{tool.icon}</span> <span style={{ fontSize: '16px' }}>{tool.icon}</span>
<span style={{ fontSize: '13px', fontWeight: '600', color: '#1f2937' }}>{tool.name}</span> <span style={{ fontSize: '13px', fontWeight: '600', color: colors.textHeading }}>{tool.name}</span>
</div> </div>
<p style={{ fontSize: '11px', color: '#6b7280', marginBottom: '8px' }}>{tool.description}</p> <p style={{ fontSize: '11px', color: colors.textMuted, marginBottom: '8px' }}>{tool.description}</p>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px' }}> <div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px' }}>
<span <span
style={{ style={{
fontSize: '10px', fontSize: '10px',
padding: '3px 8px', padding: '3px 8px',
borderRadius: '12px', borderRadius: '12px',
backgroundColor: tool.models.primary.type === 'local' ? '#d1fae5' : tool.models.primary.type === 'gpu' ? '#e0e7ff' : '#fef3c7', backgroundColor: tool.models.primary.type === 'local' ? colors.localBg : tool.models.primary.type === 'gpu' ? colors.gpuBg : colors.cloudBg,
color: tool.models.primary.type === 'local' ? '#065f46' : tool.models.primary.type === 'gpu' ? '#3730a3' : '#92400e', color: tool.models.primary.type === 'local' ? colors.localText : tool.models.primary.type === 'gpu' ? colors.gpuText : colors.cloudText,
fontWeight: '500', fontWeight: '500',
}} }}
> >
@ -260,8 +515,8 @@ export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: Use
fontSize: '10px', fontSize: '10px',
padding: '3px 8px', padding: '3px 8px',
borderRadius: '12px', borderRadius: '12px',
backgroundColor: '#f3f4f6', backgroundColor: colors.fallbackBg,
color: '#6b7280', color: colors.fallbackText,
fontWeight: '500', fontWeight: '500',
}} }}
> >
@ -295,18 +550,18 @@ export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: Use
</button> </button>
{/* Model type legend */} {/* Model type legend */}
<div style={{ marginTop: '16px', padding: '12px', backgroundColor: '#f8fafc', borderRadius: '6px', border: '1px solid #e2e8f0' }}> <div style={{ marginTop: '16px', padding: '12px', backgroundColor: colors.legendBg, borderRadius: '6px', border: `1px solid ${colors.legendBorder}` }}>
<div style={{ fontSize: '11px', color: '#64748b', display: 'flex', flexWrap: 'wrap', gap: '12px' }}> <div style={{ fontSize: '11px', color: colors.textMuted, display: 'flex', flexWrap: 'wrap', gap: '12px' }}>
<span style={{ display: 'flex', alignItems: 'center', gap: '4px' }}> <span style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
<span style={{ width: '8px', height: '8px', borderRadius: '50%', backgroundColor: '#10b981' }}></span> <span style={{ width: '8px', height: '8px', borderRadius: '50%', backgroundColor: colors.localText }}></span>
Local (Free) Local (Free)
</span> </span>
<span style={{ display: 'flex', alignItems: 'center', gap: '4px' }}> <span style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
<span style={{ width: '8px', height: '8px', borderRadius: '50%', backgroundColor: '#6366f1' }}></span> <span style={{ width: '8px', height: '8px', borderRadius: '50%', backgroundColor: colors.gpuText }}></span>
GPU (RunPod) GPU (RunPod)
</span> </span>
<span style={{ display: 'flex', alignItems: 'center', gap: '4px' }}> <span style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
<span style={{ width: '8px', height: '8px', borderRadius: '50%', backgroundColor: '#f59e0b' }}></span> <span style={{ width: '8px', height: '8px', borderRadius: '50%', backgroundColor: colors.cloudText }}></span>
Cloud (API Key) Cloud (API Key)
</span> </span>
</div> </div>
@ -317,7 +572,7 @@ export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: Use
{activeTab === 'integrations' && ( {activeTab === 'integrations' && (
<div className="settings-section"> <div className="settings-section">
{/* Knowledge Management Section */} {/* Knowledge Management Section */}
<h3 style={{ fontSize: '14px', fontWeight: '600', marginBottom: '12px', color: '#374151' }}> <h3 style={{ fontSize: '14px', fontWeight: '600', marginBottom: '12px', color: colors.text }}>
Knowledge Management Knowledge Management
</h3> </h3>
@ -325,17 +580,17 @@ export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: Use
<div <div
style={{ style={{
padding: '12px', padding: '12px',
backgroundColor: '#f9fafb', backgroundColor: colors.cardBg,
borderRadius: '8px', borderRadius: '8px',
border: '1px solid #e5e7eb', border: `1px solid ${colors.cardBorder}`,
marginBottom: '12px', marginBottom: '12px',
}} }}
> >
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}> <div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}>
<span style={{ fontSize: '20px' }}>📁</span> <span style={{ fontSize: '20px' }}>📁</span>
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
<span style={{ fontSize: '13px', fontWeight: '600', color: '#1f2937' }}>Obsidian Vault (Local)</span> <span style={{ fontSize: '13px', fontWeight: '600', color: colors.textHeading }}>Obsidian Vault (Local)</span>
<p style={{ fontSize: '11px', color: '#6b7280', marginTop: '2px' }}> <p style={{ fontSize: '11px', color: colors.textMuted, marginTop: '2px' }}>
Import notes directly from your local Obsidian vault Import notes directly from your local Obsidian vault
</p> </p>
</div> </div>
@ -344,7 +599,7 @@ export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: Use
</span> </span>
</div> </div>
{session.obsidianVaultName && ( {session.obsidianVaultName && (
<p style={{ fontSize: '11px', color: '#059669', marginBottom: '8px' }}> <p style={{ fontSize: '11px', color: colors.successText, marginBottom: '8px' }}>
Current vault: {session.obsidianVaultName} Current vault: {session.obsidianVaultName}
</p> </p>
)} )}
@ -357,17 +612,17 @@ export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: Use
<div <div
style={{ style={{
padding: '12px', padding: '12px',
backgroundColor: '#f9fafb', backgroundColor: colors.cardBg,
borderRadius: '8px', borderRadius: '8px',
border: '1px solid #e5e7eb', border: `1px solid ${colors.cardBorder}`,
marginBottom: '12px', marginBottom: '12px',
}} }}
> >
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}> <div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}>
<span style={{ fontSize: '20px' }}>🌐</span> <span style={{ fontSize: '20px' }}>🌐</span>
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
<span style={{ fontSize: '13px', fontWeight: '600', color: '#1f2937' }}>Obsidian Quartz (Web)</span> <span style={{ fontSize: '13px', fontWeight: '600', color: colors.textHeading }}>Obsidian Quartz (Web)</span>
<p style={{ fontSize: '11px', color: '#6b7280', marginTop: '2px' }}> <p style={{ fontSize: '11px', color: colors.textMuted, marginTop: '2px' }}>
Import notes from your published Quartz site via GitHub Import notes from your published Quartz site via GitHub
</p> </p>
</div> </div>
@ -375,7 +630,7 @@ export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: Use
Available Available
</span> </span>
</div> </div>
<p style={{ fontSize: '11px', color: '#6b7280', marginBottom: '8px', lineHeight: '1.4' }}> <p style={{ fontSize: '11px', color: colors.textMuted, marginBottom: '8px', lineHeight: '1.4' }}>
Quartz is a static site generator for Obsidian. If you publish your notes with Quartz, you can browse and import them here. Quartz is a static site generator for Obsidian. If you publish your notes with Quartz, you can browse and import them here.
</p> </p>
<a <a
@ -384,7 +639,7 @@ export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: Use
rel="noopener noreferrer" rel="noopener noreferrer"
style={{ style={{
fontSize: '11px', fontSize: '11px',
color: '#3b82f6', color: colors.linkColor,
textDecoration: 'none', textDecoration: 'none',
}} }}
> >
@ -395,7 +650,7 @@ export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: Use
<div className="settings-divider" /> <div className="settings-divider" />
{/* Meeting & Communication Section */} {/* Meeting & Communication Section */}
<h3 style={{ fontSize: '14px', fontWeight: '600', marginBottom: '12px', marginTop: '8px', color: '#374151' }}> <h3 style={{ fontSize: '14px', fontWeight: '600', marginBottom: '12px', marginTop: '8px', color: colors.text }}>
Meeting & Communication Meeting & Communication
</h3> </h3>
@ -403,16 +658,16 @@ export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: Use
<div <div
style={{ style={{
padding: '12px', padding: '12px',
backgroundColor: '#f9fafb', backgroundColor: colors.cardBg,
borderRadius: '8px', borderRadius: '8px',
border: '1px solid #e5e7eb', border: `1px solid ${colors.cardBorder}`,
}} }}
> >
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}> <div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}>
<span style={{ fontSize: '20px' }}>🎥</span> <span style={{ fontSize: '20px' }}>🎥</span>
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
<span style={{ fontSize: '13px', fontWeight: '600', color: '#1f2937' }}>Fathom Meetings</span> <span style={{ fontSize: '13px', fontWeight: '600', color: colors.textHeading }}>Fathom Meetings</span>
<p style={{ fontSize: '11px', color: '#6b7280', marginTop: '2px' }}> <p style={{ fontSize: '11px', color: colors.textMuted, marginTop: '2px' }}>
Import meeting transcripts and AI summaries Import meeting transcripts and AI summaries
</p> </p>
</div> </div>
@ -476,7 +731,7 @@ export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: Use
style={{ style={{
display: 'block', display: 'block',
fontSize: '11px', fontSize: '11px',
color: '#3b82f6', color: colors.linkColor,
textDecoration: 'none', textDecoration: 'none',
marginTop: '8px', marginTop: '8px',
}} }}
@ -513,8 +768,8 @@ export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: Use
</div> </div>
{/* Future Integrations Placeholder */} {/* Future Integrations Placeholder */}
<div style={{ marginTop: '16px', padding: '12px', backgroundColor: '#f8fafc', borderRadius: '6px', border: '1px dashed #cbd5e1' }}> <div style={{ marginTop: '16px', padding: '12px', backgroundColor: colors.legendBg, borderRadius: '6px', border: `1px dashed ${colors.cardBorder}` }}>
<p style={{ fontSize: '12px', color: '#64748b', textAlign: 'center' }}> <p style={{ fontSize: '12px', color: colors.textMuted, textAlign: 'center' }}>
More integrations coming soon: Google Calendar, Notion, and more More integrations coming soon: Google Calendar, Notion, and more
</p> </p>
</div> </div>