diff --git a/frontend/src/App.css b/frontend/src/App.css index f7eb7b4..f23cec8 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -6,7 +6,7 @@ --border: #3f3f46; --text-primary: #e4e4e7; --text-secondary: #a1a1aa; - --text-muted: #71717a; + --text-muted: #8b8b95; --accent: #3b82f6; --accent-hover: #2563eb; --accent-glow: rgba(59, 130, 246, 0.3); @@ -17,6 +17,27 @@ --input-bg: #18181b; } +/* System color scheme detection */ +@media (prefers-color-scheme: light) { + :root:not([data-theme]) { + --bg-primary: #f8fafc; + --bg-secondary: #ffffff; + --bg-tertiary: #f1f5f9; + --border: #e2e8f0; + --text-primary: #1e293b; + --text-secondary: #64748b; + --text-muted: #64748b; + --accent: #3b82f6; + --accent-hover: #2563eb; + --accent-glow: rgba(59, 130, 246, 0.15); + --success: #16a34a; + --warning: #ea580c; + --danger: #dc2626; + --card-bg: #ffffff; + --input-bg: #ffffff; + } +} + [data-theme="light"] { --bg-primary: #f8fafc; --bg-secondary: #ffffff; @@ -78,7 +99,7 @@ body { .nav-tabs { display: flex; gap: 0.25rem; } .nav-tabs button { - padding: 0.4rem 0.8rem; + padding: 0.5rem 1rem; background: transparent; border: 1px solid transparent; color: var(--text-secondary); @@ -86,6 +107,8 @@ body { cursor: pointer; font-size: 0.85rem; transition: all 0.15s; + min-height: 44px; + min-width: 44px; } .nav-tabs button:hover { background: var(--bg-tertiary); } .nav-tabs button.active { @@ -96,13 +119,15 @@ body { .mode-toggle { display: flex; gap: 0.25rem; } .mode-toggle button { - padding: 0.3rem 0.6rem; + padding: 0.4rem 0.7rem; background: var(--bg-tertiary); border: 1px solid var(--border); color: var(--text-secondary); border-radius: 4px; cursor: pointer; font-size: 0.75rem; + min-height: 44px; + min-width: 44px; } .mode-toggle button.active { background: var(--accent); @@ -118,6 +143,8 @@ body { border-radius: 6px; cursor: pointer; font-size: 0.85rem; + min-height: 44px; + min-width: 44px; } .icon-btn:hover { background: var(--bg-tertiary); } @@ -232,6 +259,7 @@ body { padding: 0.5rem 1rem; background: var(--bg-tertiary); border: 1px solid var(--border); border-radius: 8px; color: var(--text-primary); cursor: pointer; font-size: 0.85rem; + min-height: 44px; } .suggestion:hover { border-color: var(--accent); } @@ -291,6 +319,7 @@ body { border: none; border-radius: 8px; color: white; cursor: pointer; font-weight: 600; transition: background 0.15s; + min-height: 44px; } .send-btn:hover:not(:disabled) { background: var(--accent-hover); } .send-btn:disabled { opacity: 0.5; cursor: not-allowed; } @@ -523,6 +552,127 @@ body { } .save-btn:hover { background: var(--accent-hover); } +/* ========== Head Colors ========== */ +.avatar[data-head="logic"] .avatar-placeholder { background: #6366f1; color: white; } +.avatar[data-head="research"] .avatar-placeholder { background: #8b5cf6; color: white; } +.avatar[data-head="systems"] .avatar-placeholder { background: #06b6d4; color: white; } +.avatar[data-head="strategy"] .avatar-placeholder { background: #f59e0b; color: #18181b; } +.avatar[data-head="product"] .avatar-placeholder { background: #ec4899; color: white; } +.avatar[data-head="security"] .avatar-placeholder { background: #ef4444; color: white; } +.avatar[data-head="safety"] .avatar-placeholder { background: #22c55e; color: #18181b; } +.avatar[data-head="reliability"] .avatar-placeholder { background: #14b8a6; color: white; } +.avatar[data-head="cost"] .avatar-placeholder { background: #f97316; color: white; } +.avatar[data-head="data"] .avatar-placeholder { background: #a855f7; color: white; } +.avatar[data-head="devex"] .avatar-placeholder { background: #0ea5e9; color: white; } +.avatar[data-head="witness"] .avatar-placeholder { background: #64748b; color: white; } + +.avatar.active .avatar-placeholder, .avatar.speaking .avatar-placeholder { + filter: brightness(1.2); + box-shadow: 0 0 8px var(--accent-glow); +} + +/* ========== Collapsible Avatar Grid ========== */ +.avatar-grid-wrapper { flex-shrink: 0; border-bottom: 1px solid var(--border); } +.avatar-grid-toggle { + display: none; width: 100%; padding: 0.4rem 1rem; + background: var(--bg-secondary); border: none; border-bottom: 1px solid var(--border); + color: var(--text-secondary); cursor: pointer; font-size: 0.8rem; + text-align: left; min-height: 44px; +} +.avatar-grid-toggle:hover { background: var(--bg-tertiary); } +.avatar-grid-wrapper .avatar-grid { border-bottom: none; } + +/* ========== Structured Response Cards ========== */ +.response-structured { display: flex; flex-direction: column; gap: 0.5rem; } +.response-synthesis { + font-size: 0.9rem; line-height: 1.6; margin-bottom: 0.25rem; +} +.response-synthesis p { margin-bottom: 0.5rem; } +.response-synthesis p:last-child { margin-bottom: 0; } +.response-synthesis code { + background: var(--bg-tertiary); padding: 0.15rem 0.4rem; + border-radius: 3px; font-size: 0.85em; +} +.response-synthesis pre { + background: var(--bg-tertiary); padding: 0.75rem; + border-radius: 6px; overflow-x: auto; margin: 0.5rem 0; +} +.response-synthesis pre code { background: none; padding: 0; } +.response-synthesis strong { color: var(--text-primary); } +.response-synthesis em { color: var(--text-secondary); } +.response-synthesis ul, .response-synthesis ol { padding-left: 1.5rem; margin: 0.25rem 0; } +.response-synthesis li { margin-bottom: 0.2rem; } +.response-synthesis a { color: var(--accent); text-decoration: none; } +.response-synthesis a:hover { text-decoration: underline; } +.response-synthesis blockquote { + border-left: 3px solid var(--accent); padding-left: 0.75rem; + margin: 0.5rem 0; color: var(--text-secondary); +} +.response-synthesis h1, .response-synthesis h2, .response-synthesis h3 { + margin-top: 0.75rem; margin-bottom: 0.25rem; +} +.response-synthesis h1 { font-size: 1.1rem; } +.response-synthesis h2 { font-size: 1rem; } +.response-synthesis h3 { font-size: 0.95rem; } + +.head-cards { display: flex; flex-direction: column; gap: 0.35rem; margin-top: 0.5rem; } +.head-card { + display: flex; align-items: flex-start; gap: 0.5rem; + padding: 0.4rem 0.6rem; border-radius: 6px; + background: var(--bg-tertiary); font-size: 0.8rem; +} +.head-card-dot { + width: 8px; height: 8px; border-radius: 50%; margin-top: 0.35rem; flex-shrink: 0; +} +.head-card-label { font-weight: 600; color: var(--text-primary); text-transform: capitalize; } +.head-card-text { color: var(--text-secondary); } + +/* Head card dot colors */ +.head-card[data-head="logic"] .head-card-dot { background: #6366f1; } +.head-card[data-head="research"] .head-card-dot { background: #8b5cf6; } +.head-card[data-head="systems"] .head-card-dot { background: #06b6d4; } +.head-card[data-head="strategy"] .head-card-dot { background: #f59e0b; } +.head-card[data-head="product"] .head-card-dot { background: #ec4899; } +.head-card[data-head="security"] .head-card-dot { background: #ef4444; } +.head-card[data-head="safety"] .head-card-dot { background: #22c55e; } +.head-card[data-head="reliability"] .head-card-dot { background: #14b8a6; } +.head-card[data-head="cost"] .head-card-dot { background: #f97316; } +.head-card[data-head="data"] .head-card-dot { background: #a855f7; } +.head-card[data-head="devex"] .head-card-dot { background: #0ea5e9; } +.head-card[data-head="witness"] .head-card-dot { background: #64748b; } + +/* ========== Status Indicators ========== */ +.status-value.healthy { color: var(--success); } +.status-value.degraded { color: var(--warning); } +.status-value.offline { color: var(--danger); } +.status-dot { + display: inline-block; width: 10px; height: 10px; border-radius: 50%; + margin-right: 0.4rem; vertical-align: middle; +} +.status-dot.healthy { background: var(--success); box-shadow: 0 0 6px rgba(34, 197, 94, 0.4); } +.status-dot.degraded { background: var(--warning); } +.status-dot.offline { background: var(--danger); } + +/* ========== Toast Notifications ========== */ +.toast-container { + position: fixed; bottom: 1.5rem; right: 1.5rem; + display: flex; flex-direction: column; gap: 0.5rem; + z-index: 1000; pointer-events: none; +} +.toast { + padding: 0.6rem 1rem; border-radius: 8px; + font-size: 0.85rem; font-weight: 500; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + animation: toast-in 0.3s ease-out, toast-out 0.3s ease-in 2.7s forwards; + pointer-events: auto; max-width: 320px; +} +.toast.success { background: var(--success); color: white; } +.toast.error { background: var(--danger); color: white; } +.toast.info { background: var(--accent); color: white; } +.toast.warning { background: var(--warning); color: white; } +@keyframes toast-in { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } +@keyframes toast-out { from { opacity: 1; } to { opacity: 0; } } + /* ========== Utilities ========== */ .muted { color: var(--text-muted); font-size: 0.85rem; } .error-banner { @@ -536,6 +686,12 @@ body { color: var(--text-muted); font-size: 0.9rem; } +/* ========== Focus visible (keyboard nav) ========== */ +:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} + /* ========== Responsive ========== */ @media (max-width: 768px) { .header { flex-direction: column; gap: 0.5rem; padding: 0.5rem 1rem; } @@ -543,16 +699,19 @@ body { .header-right { width: 100%; justify-content: flex-end; } .consensus-panel { display: none; } .avatar-grid { grid-template-columns: repeat(4, 1fr); } + .avatar-grid-toggle { display: block; } + .avatar-grid-wrapper.collapsed .avatar-grid { display: none; } .messages { padding: 0.75rem; } .message { max-width: 95%; } .admin-page, .ethics-page, .settings-page { padding: 1rem; } .status-grid { grid-template-columns: repeat(2, 1fr); } .add-form { flex-direction: column; } .setting-row { flex-direction: column; align-items: flex-start; gap: 0.5rem; } + .nav-tabs button { min-height: 44px; padding: 0.5rem 0.75rem; } } @media (max-width: 480px) { .avatar-grid { grid-template-columns: repeat(3, 1fr); } - .nav-tabs button { font-size: 0.75rem; padding: 0.3rem 0.5rem; } + .nav-tabs button { font-size: 0.75rem; padding: 0.4rem 0.6rem; min-height: 44px; } .mode-toggle { display: none; } } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 24fd1b7..38e6e2b 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,6 +2,7 @@ import { useState, useCallback, useEffect, useRef } from 'react' import { AvatarGrid } from './components/AvatarGrid' import { ConsensusPanel } from './components/ConsensusPanel' import { ChatMessage } from './components/ChatMessage' +import { ToastProvider, useToast } from './components/Toast' import { AdminPage } from './pages/AdminPage' import { EthicsPage } from './pages/EthicsPage' import { SettingsPage } from './pages/SettingsPage' @@ -169,13 +170,14 @@ function App() { } return ( -
$1')
+ out = out.replace(/\*\*([^*]+)\*\*/g, '$1')
+ out = out.replace(/\*([^*]+)\*/g, '$1')
+ out = out.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1')
+ return out
+}
+
+function parseMarkdown(md: string): string {
+ const lines = md.split('\n')
+ const html: string[] = []
+ let inCode = false
+ let codeBlock: string[] = []
+ let inList = false
+ let listType: 'ul' | 'ol' = 'ul'
+
+ for (const line of lines) {
+ if (line.startsWith('```')) {
+ if (inCode) {
+ html.push(`${escapeHtml(codeBlock.join('\n'))}`)
+ codeBlock = []
+ inCode = false
+ } else {
+ if (inList) { html.push(`${listType}>`); inList = false }
+ inCode = true
+ }
+ continue
+ }
+ if (inCode) { codeBlock.push(line); continue }
+
+ const trimmed = line.trim()
+ if (!trimmed) {
+ if (inList) { html.push(`${listType}>`); inList = false }
+ continue
+ }
+
+ if (trimmed.startsWith('### ')) {
+ if (inList) { html.push(`${listType}>`); inList = false }
+ html.push(`${renderInline(trimmed.slice(2))}`) + } else if (/^[-*] /.test(trimmed)) { + if (!inList || listType !== 'ul') { + if (inList) html.push(`${listType}>`) + html.push('
${renderInline(trimmed)}
`) + } + } + if (inCode) html.push(`${escapeHtml(codeBlock.join('\n'))}`)
+ if (inList) html.push(`${listType}>`)
+ return html.join('')
+}
+
+export function Markdown({ content }: { content: string }) {
+ return (
+
+ )
+}
diff --git a/frontend/src/components/Toast.tsx b/frontend/src/components/Toast.tsx
new file mode 100644
index 0000000..930e235
--- /dev/null
+++ b/frontend/src/components/Toast.tsx
@@ -0,0 +1,40 @@
+import { useState, useEffect, useCallback, createContext, useContext } from 'react'
+
+interface ToastItem {
+ id: number
+ message: string
+ type: 'success' | 'error' | 'info' | 'warning'
+}
+
+interface ToastContextType {
+ toast: (message: string, type?: ToastItem['type']) => void
+}
+
+const ToastContext = createContext