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:
parent
2aeb2b0c34
commit
846816b1aa
|
|
@ -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
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
@ -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%;
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue