From 5c87b8aafcb2519c119626a549ef0d1a664159a4 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Fri, 24 Jul 2026 12:53:53 -0700 Subject: [PATCH] [eric] help: real Help panel off the pill (ask starts a chat, bug/feature open prefilled GitHub issues, docs link) --- .../app/pages/Dashboard/desktop/HelpPanel.tsx | 182 ++++++++++++++++++ .../app/pages/Dashboard/desktop/HelpPill.tsx | 101 +++++----- 2 files changed, 239 insertions(+), 44 deletions(-) create mode 100644 frontend/src/app/pages/Dashboard/desktop/HelpPanel.tsx diff --git a/frontend/src/app/pages/Dashboard/desktop/HelpPanel.tsx b/frontend/src/app/pages/Dashboard/desktop/HelpPanel.tsx new file mode 100644 index 00000000..d4b88608 --- /dev/null +++ b/frontend/src/app/pages/Dashboard/desktop/HelpPanel.tsx @@ -0,0 +1,182 @@ +import React, { useCallback, useState } from 'react'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import { AnimatePresence, motion } from 'framer-motion'; +import AutoAwesomeRoundedIcon from '@mui/icons-material/AutoAwesomeRounded'; +import BugReportOutlinedIcon from '@mui/icons-material/BugReportOutlined'; +import LightbulbOutlinedIcon from '@mui/icons-material/LightbulbOutlined'; +import MenuBookOutlinedIcon from '@mui/icons-material/MenuBookOutlined'; +import ArrowOutwardRoundedIcon from '@mui/icons-material/ArrowOutwardRounded'; +import ChevronRightRoundedIcon from '@mui/icons-material/ChevronRightRounded'; +import ArrowBackRoundedIcon from '@mui/icons-material/ArrowBackRounded'; +import ArrowUpwardRoundedIcon from '@mui/icons-material/ArrowUpwardRounded'; +import { useAppDispatch, useAppSelector } from '@/shared/hooks'; +import { createDraftSession, launchAndSendFirstMessage, type AgentConfig } from '@/shared/state/agentsSlice'; +import { getLastDashboardId } from '@/shared/lastDashboardId'; + +const REPO_ISSUES_URL = 'https://github.com/openswarm-ai/openswarm/issues/new'; +const DOCS_URL = 'https://docs.openswarm.com'; + +function openExternal(url: string): void { + const api = (window as unknown as { openswarm?: { openExternal?: (u: string) => void } }).openswarm; + if (api?.openExternal) api.openExternal(url); + else window.open(url, '_blank'); +} + +type Pane = 'root' | 'bug' | 'idea'; + +// The Help panel: Linear/Raycast-style popover off the Help pill. Ask leads (starts a real chat with +// the question), then report-a-bug / request-a-feature (open a PREFILLED GitHub issue: version + OS +// attached in the body, no secret and no backend needed, and the user can watch the thread for team +// replies), then docs. Closes on outside click or Esc via the parent. +const HelpPanel: React.FC<{ onClose: () => void }> = ({ onClose }) => { + const dispatch = useAppDispatch(); + const model = useAppSelector((s) => s.settings.data.default_model); + const appVersion = useAppSelector((s) => s.update.appVersion); + const [pane, setPane] = useState('root'); + const [ask, setAsk] = useState(''); + const [reportText, setReportText] = useState(''); + + const startChat = useCallback((prompt: string): void => { + const p = prompt.trim(); + if (!p) return; + const dashboardId = getLastDashboardId() ?? undefined; + const config: AgentConfig = { name: 'Help', model, mode: 'agent', dashboard_id: dashboardId }; + const draftId = dispatch(createDraftSession({ mode: 'agent', model, dashboardId: dashboardId ?? '', setActive: true })).payload.draftId; + void dispatch(launchAndSendFirstMessage({ draftId, config, prompt: p, mode: 'agent', model, expand: true })); + onClose(); + }, [dispatch, model, onClose]); + + const fileOnGitHub = useCallback((kind: 'bug' | 'idea'): void => { + const body = [ + reportText.trim(), + '', + '---', + `App version: ${appVersion ?? 'dev'}`, + `Platform: ${navigator.platform}`, + ].join('\n'); + const params = new URLSearchParams({ + title: reportText.trim().split('\n')[0].slice(0, 80) || (kind === 'bug' ? 'Bug report' : 'Feature request'), + labels: kind === 'bug' ? 'bug' : 'enhancement', + body, + }); + openExternal(`${REPO_ISSUES_URL}?${params.toString()}`); + onClose(); + }, [reportText, appVersion, onClose]); + + const rowSx = { + display: 'flex', alignItems: 'center', gap: 1.25, width: '100%', + px: 1.25, py: 1, borderRadius: '9px', border: 'none', background: 'transparent', + color: 'rgba(255,255,255,0.9)', cursor: 'pointer', fontFamily: 'inherit', textAlign: 'left' as const, + '&:hover': { background: 'rgba(255,255,255,0.07)' }, + }; + const iconSx = { fontSize: 17, color: 'rgba(255,255,255,0.65)', flexShrink: 0 }; + const fieldSx = { + width: '100%', boxSizing: 'border-box' as const, resize: 'none' as const, + background: 'rgba(255,255,255,0.06)', border: '1px solid rgba(255,255,255,0.12)', borderRadius: '10px', + color: 'rgba(255,255,255,0.92)', fontFamily: 'inherit', fontSize: '0.8125rem', lineHeight: 1.5, + padding: '9px 11px', outline: 'none', + }; + + return ( + + e.stopPropagation()} + > + {pane === 'root' ? ( + + + How can we help? + + + + ) => setAsk(e.target.value)} + onKeyDown={(e: React.KeyboardEvent) => { if (e.key === 'Enter') startChat(ask); e.stopPropagation(); }} + placeholder="Ask OpenSwarm anything..." + sx={{ flex: 1, border: 'none', outline: 'none', background: 'transparent', color: 'rgba(255,255,255,0.92)', fontFamily: 'inherit', fontSize: '0.8125rem', '&::placeholder': { color: 'rgba(255,255,255,0.4)' } }} + /> + {ask.trim() && ( + startChat(ask)} sx={{ display: 'flex', border: 'none', background: 'transparent', color: 'rgba(255,255,255,0.8)', cursor: 'pointer', p: 0 }}> + + + )} + + { setReportText(''); setPane('bug'); }} sx={rowSx}> + + + Report a bug + Version attached automatically + + + + { setReportText(''); setPane('idea'); }} sx={rowSx}> + + + Request a feature + Tell us what's missing + + + + { openExternal(DOCS_URL); onClose(); }} sx={rowSx}> + + Docs & shortcuts + + + + ) : ( + + setPane('root')} sx={{ display: 'inline-flex', alignItems: 'center', gap: 0.5, border: 'none', background: 'transparent', color: 'rgba(255,255,255,0.55)', fontSize: '0.75rem', cursor: 'pointer', fontFamily: 'inherit', p: 0, mb: 1 }}> + {pane === 'bug' ? 'Report a bug' : 'Request a feature'} + + ) => setReportText(e.target.value)} + onKeyDown={(e: React.KeyboardEvent) => e.stopPropagation()} + placeholder={pane === 'bug' ? 'What went wrong?' : "What's missing?"} + sx={fieldSx} + /> + + Opens a prefilled GitHub issue; watch it for replies from the team. + + + Cancel + fileOnGitHub(pane === 'bug' ? 'bug' : 'idea')} + disabled={!reportText.trim()} + sx={{ + border: 'none', borderRadius: '9px', px: 1.5, py: 0.6, fontFamily: 'inherit', + fontSize: '0.8125rem', fontWeight: 600, cursor: reportText.trim() ? 'pointer' : 'default', + background: reportText.trim() ? 'rgba(255,255,255,0.92)' : 'rgba(255,255,255,0.15)', + color: reportText.trim() ? '#1c1b19' : 'rgba(255,255,255,0.4)', + }} + > + Continue on GitHub + + + + )} + + + ); +}; + +export default HelpPanel; diff --git a/frontend/src/app/pages/Dashboard/desktop/HelpPill.tsx b/frontend/src/app/pages/Dashboard/desktop/HelpPill.tsx index d50be64b..6d6cbd07 100644 --- a/frontend/src/app/pages/Dashboard/desktop/HelpPill.tsx +++ b/frontend/src/app/pages/Dashboard/desktop/HelpPill.tsx @@ -1,63 +1,76 @@ -import React from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; import Tooltip from '@mui/material/Tooltip'; import CircularProgress from '@mui/material/CircularProgress'; import MicNoneOutlinedIcon from '@mui/icons-material/MicNoneOutlined'; import MicIcon from '@mui/icons-material/Mic'; -import { useAppDispatch } from '@/shared/hooks'; -import { addBrowserCard } from '@/shared/state/dashboardLayoutSlice'; import { useVoice } from '@/shared/voice/VoiceDictationContext'; +import HelpPanel from './HelpPanel'; -const HELP_URL = 'https://openswarm.com'; - -/** Top-right desktop pill: Help opens the docs; the mic dictates (local whisper) into the focused field. */ +/** Top-right desktop pill: Help opens the help panel (ask, report a bug, docs); the mic dictates (local whisper) into the focused field. */ function HelpPill(): React.ReactElement { - const dispatch = useAppDispatch(); const { state, pct, pressStart, pressEnd } = useVoice(); + const [helpOpen, setHelpOpen] = useState(false); + const rootRef = useRef(null); const recording = state === 'recording'; const transcribing = state === 'transcribing'; const preparing = state === 'preparing'; const busy = transcribing || preparing; + // Outside click / Esc closes the panel; listeners only live while it's open. + useEffect(() => { + if (!helpOpen) return undefined; + const onDown = (e: MouseEvent): void => { + if (rootRef.current && !rootRef.current.contains(e.target as Node)) setHelpOpen(false); + }; + const onKey = (e: KeyboardEvent): void => { if (e.key === 'Escape') setHelpOpen(false); }; + document.addEventListener('mousedown', onDown); + document.addEventListener('keydown', onKey); + return () => { document.removeEventListener('mousedown', onDown); document.removeEventListener('keydown', onKey); }; + }, [helpOpen]); + return ( - dispatch(addBrowserCard({ url: HELP_URL }))} - > - - {recording ? 'Listening' : transcribing ? 'Transcribing' : preparing ? `Preparing ${pct}%` : 'Help'} - - - { e.stopPropagation(); if (!busy) pressStart(); }} - onPointerUp={(e) => { e.stopPropagation(); pressEnd(); }} - onPointerLeave={() => pressEnd()} - onClick={(e) => e.stopPropagation()} - > - {busy - ? - : recording - ? - : } - - + + setHelpOpen((v) => !v)} + > + + {recording ? 'Listening' : transcribing ? 'Transcribing' : preparing ? `Preparing ${pct}%` : 'Help'} + + + { e.stopPropagation(); if (!busy) pressStart(); }} + onPointerUp={(e) => { e.stopPropagation(); pressEnd(); }} + onPointerLeave={() => pressEnd()} + onClick={(e) => e.stopPropagation()} + > + {busy + ? + : recording + ? + : } + + + + {helpOpen && setHelpOpen(false)} />} ); }