diff --git a/public/content/report-a-bug.md b/public/content/report-a-bug.md
index 1411f5eb..4a6a46b2 100644
--- a/public/content/report-a-bug.md
+++ b/public/content/report-a-bug.md
@@ -1,22 +1,24 @@
---
title: Report a Bug
-deck: Found something broken or have a suggestion? Let us know.
+deck: Found something broken or have a suggestion? Use the in-app reporter.
---
## How to report an issue
-If you've encountered an error, unexpected behavior, or have a suggestion for improvement, we want to know about it! Your feedback is essential to help us improve Ocotillo. Please email us with bug reports or feature requests at [ocotillo-nmbg@nmt.edu](mailto:ocotillo-nmbg@nmt.edu).
+Use the **Get Help** button in the top right corner of any page. It opens a panel where you can:
-## What to include
+- **Report a Bug** — describe what went wrong, and it gets filed directly as a JIRA ticket
+- **Request a Feature** — describe a problem and what you'd like Ocotillo to do differently
-A good bug report includes:
+Your name, email, current page, and browser are captured automatically so you don't have to include them.
+
+## What makes a good bug report
- **What you expected to happen**
- **What actually happened**
-- **Steps to reproduce** -- the more specific the better
-- **Browser and operating system** (e.g. Chrome on macOS)
-- **Screenshots are extremely helpful** if the issue is visual
+- **Steps to reproduce** — the more specific the better
+- **Screenshots** are extremely helpful if the issue is visual
## Contact
-For urgent issues or questions, reach out directly to the development team at [ocotillo-nmbg@nmt.edu](mailto:ocotillo-nmbg@nmt.edu).
\ No newline at end of file
+For urgent issues, email the development team directly at [ocotillo-nmbg@nmt.edu](mailto:ocotillo-nmbg@nmt.edu).
diff --git a/src/components/AppShell.tsx b/src/components/AppShell.tsx
index a0ede246..7d8474c4 100644
--- a/src/components/AppShell.tsx
+++ b/src/components/AppShell.tsx
@@ -43,11 +43,11 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import {
+ Bug,
Check,
ChevronDown,
ChevronRight,
FlaskConical,
- LifeBuoy,
Lock,
LogOut,
Menu,
@@ -63,6 +63,8 @@ import { ReportBugButton } from '@/components/Button'
import { AmpRole, PRIMARY_NAV, RESOURCE_NAV, type NavItem } from '@/config/navigation'
import { useAccessCapabilities } from '@/hooks'
import { useSearch } from '@/providers/search-provider'
+import { Textarea } from '@/components/ui/textarea'
+import { Label } from '@/components/ui/label'
// Support panel state shared between the sidebar footer button and the panel itself
export const SupportPanelContext = createContext<{
@@ -351,8 +353,7 @@ function AppSidebar() {
)}
- {/* Help & Support button — hidden until panel content is defined */}
- {/* */}
+
)
@@ -405,34 +406,84 @@ function SupportPanelTrigger({ collapsed }: { collapsed: boolean }) {
-
- {!collapsed && Help & Support }
+
+ {!collapsed && Get Help }
)
}
const PANEL_MIN_WIDTH = 320
-const PANEL_DEFAULT_WIDTH = 320
+const PANEL_DEFAULT_WIDTH = 360
+
+type PanelView = 'home' | 'bug' | 'feature'
+type SubmitState = 'idle' | 'loading' | 'success' | 'error'
+
+interface BugFormData {
+ whatHappened: string
+ severity: string
+}
+
+interface FeatureFormData {
+ problem: string
+ whoWouldUse: string
+ whatItShouldDo: string
+}
+
+function getBrowser(): string {
+ const ua = navigator.userAgent
+ if (ua.includes('Chrome') && !ua.includes('Edg')) return 'Chrome'
+ if (ua.includes('Firefox')) return 'Firefox'
+ if (ua.includes('Safari') && !ua.includes('Chrome')) return 'Safari'
+ if (ua.includes('Edg')) return 'Edge'
+ return 'Unknown'
+}
function SupportPanel() {
const { isOpen, close } = useContext(SupportPanelContext)
+ const { data: user } = useGetIdentity<{ name: string; email: string }>()
+ const location = useLocation()
+
+ const [view, setView] = useState('home')
+ const [submitState, setSubmitState] = useState('idle')
+ const [ticketKey, setTicketKey] = useState('')
+ const [ticketUrl, setTicketUrl] = useState('')
+ const [errorMsg, setErrorMsg] = useState('')
+
+ const [bugForm, setBugForm] = useState({ whatHappened: '', severity: 'Low' })
+ const [featureForm, setFeatureForm] = useState({
+ problem: '',
+ whoWouldUse: '',
+ whatItShouldDo: '',
+ })
+
const [width, setWidth] = useState(PANEL_DEFAULT_WIDTH)
const outerRef = useRef(null)
const innerRef = useRef(null)
const dragState = useRef<{ startX: number; startWidth: number } | null>(null)
+ // Reset to home view when panel closes
+ useEffect(() => {
+ if (!isOpen) {
+ setTimeout(() => {
+ setView('home')
+ setSubmitState('idle')
+ setBugForm({ whatHappened: '', severity: 'Low' })
+ setFeatureForm({ problem: '', whoWouldUse: '', whatItShouldDo: '' })
+ }, 300)
+ }
+ }, [isOpen])
+
const onMouseDown = useCallback((e: React.MouseEvent) => {
e.preventDefault()
dragState.current = { startX: e.clientX, startWidth: width }
- // Disable the CSS transition for the duration of the drag
if (outerRef.current) outerRef.current.style.transition = 'none'
document.body.style.cursor = 'col-resize'
document.body.style.userSelect = 'none'
@@ -444,13 +495,11 @@ function SupportPanel() {
const maxWidth = Math.floor(window.innerWidth / 2)
const delta = dragState.current.startX - e.clientX
const next = Math.min(maxWidth, Math.max(PANEL_MIN_WIDTH, dragState.current.startWidth + delta))
- // Write directly to DOM — no React re-render per frame
if (outerRef.current) outerRef.current.style.width = `${next}px`
if (innerRef.current) innerRef.current.style.width = `${next}px`
}
const onMouseUp = () => {
if (!dragState.current) return
- // Commit final width to React state and restore transition
const finalWidth = outerRef.current
? parseInt(outerRef.current.style.width, 10) || PANEL_DEFAULT_WIDTH
: PANEL_DEFAULT_WIDTH
@@ -468,6 +517,48 @@ function SupportPanel() {
}
}, [])
+ const handleSubmit = async (type: 'bug' | 'feature') => {
+ setSubmitState('loading')
+ setErrorMsg('')
+ const payload = {
+ type,
+ page_url: window.location.href,
+ reporter_name: user?.name,
+ reporter_email: user?.email,
+ browser: getBrowser(),
+ ...(type === 'bug'
+ ? { what_happened: bugForm.whatHappened, severity: bugForm.severity }
+ : {
+ problem: featureForm.problem,
+ who_would_use: featureForm.whoWouldUse,
+ what_it_should_do: featureForm.whatItShouldDo,
+ }),
+ }
+ try {
+ const res = await fetch('/api/feedback', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(payload),
+ })
+ if (!res.ok) throw new Error(`Server error ${res.status}`)
+ const data = await res.json()
+ setTicketKey(data.jira_key)
+ setTicketUrl(data.jira_url)
+ setSubmitState('success')
+ setTimeout(() => {
+ setView('home')
+ setSubmitState('idle')
+ setBugForm({ whatHappened: '', severity: 'Low' })
+ setFeatureForm({ problem: '', whoWouldUse: '', whatItShouldDo: '' })
+ }, 3000)
+ } catch (err) {
+ setErrorMsg(err instanceof Error ? err.message : 'Something went wrong. Please try again.')
+ setSubmitState('error')
+ }
+ }
+
+ const pageUrl = location.pathname
+
return (
- {/* Drag handle — updates DOM directly, no React state during drag */}
+ {/* Drag handle */}
{/* Panel header */}
-
Ocotillo Support
+
+ {view !== 'home' && (
+ { setView('home'); setSubmitState('idle') }}
+ className="rounded-md p-1 text-muted-foreground hover:bg-accent hover:text-foreground transition-colors"
+ aria-label="Back"
+ >
+
+
+ )}
+
+ {view === 'home' && 'Get Help'}
+ {view === 'bug' && 'Report a Bug'}
+ {view === 'feature' && 'Request a Feature'}
+
+
- {/* Panel body — placeholder until content is defined */}
-
-
-
Support coming soon
-
- This panel will contain help articles, contact options, and other support resources.
-
+ {/* Panel body */}
+
+
+ {/* Home view */}
+ {view === 'home' && (
+
+
+ Found something broken or have a suggestion? Let us know.
+
+
+
setView('bug')}
+ className="flex w-full items-start gap-3 rounded-[9px] bg-background p-4 text-left hover:bg-accent transition-colors cursor-pointer"
+ >
+
+
+
Report a Bug
+
+ Something is broken or not working as expected.
+
+
+
+
+
+
setView('feature')}
+ className="flex w-full items-start gap-3 rounded-[9px] bg-background p-4 text-left hover:bg-accent transition-colors cursor-pointer"
+ >
+
+
+
Request a Feature
+
+ Describe something you need that Ocotillo doesn't do yet.
+
+
+
+
+
+ For urgent issues, email{' '}
+
+ ocotillo-nmbg@nmt.edu
+
+
+
+ )}
+
+ {/* Bug form */}
+ {view === 'bug' && (
+
+ {/* Auto-captured context */}
+
+
Captured automatically
+
+ Page: {pageUrl}
+
+ {user?.name && (
+
+ Reported by: {user.name}
+
+ )}
+
+ Browser: {getBrowser()}
+
+
+ Date & time: {' '}
+ {new Date().toLocaleString(undefined, {
+ dateStyle: 'medium',
+ timeStyle: 'short',
+ })}
+
+
+
+
+
+ What happened? *
+
+
+
+
+
Severity
+ {[
+ { value: 'Low', label: 'Low', description: 'Minor annoyance, workaround exists' },
+ { value: 'Medium', label: 'Medium', description: 'Impacts my workflow' },
+ { value: 'High', label: 'High', description: 'Blocking, data loss, or completely broken' },
+ ].map(({ value, label, description }) => (
+
+ setBugForm((f) => ({ ...f, severity: value }))}
+ disabled={submitState === 'loading' || submitState === 'success'}
+ className="mt-0.5 accent-primary"
+ />
+
+
{label}
+
{description}
+
+
+ ))}
+
+
+ {submitState === 'success' && (
+
+ )}
+
+ {submitState === 'error' && (
+
+ {errorMsg}
+
+ )}
+
+
+ { setView('home'); setSubmitState('idle') }}
+ disabled={submitState === 'loading'}
+ className="flex-1"
+ >
+ Back
+
+ handleSubmit('bug')}
+ disabled={!bugForm.whatHappened.trim() || submitState === 'loading' || submitState === 'success'}
+ className="flex-1"
+ >
+ {submitState === 'loading' ? 'Submitting…' : 'Submit Bug'}
+
+
+
+ )}
+
+ {/* Feature request form */}
+ {view === 'feature' && (
+
+
+
+ What problem does this solve? *
+
+
+
+
+
+ Who would use this?{' '}
+ (optional)
+
+ setFeatureForm((f) => ({ ...f, whoWouldUse: e.target.value }))}
+ disabled={submitState === 'loading' || submitState === 'success'}
+ className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50"
+ />
+
+
+
+
+ What should it do? *
+
+
+
+ {submitState === 'success' && (
+
+ )}
+
+ {submitState === 'error' && (
+
+ {errorMsg}
+
+ )}
+
+
+ { setView('home'); setSubmitState('idle') }}
+ disabled={submitState === 'loading'}
+ className="flex-1"
+ >
+ Back
+
+ handleSubmit('feature')}
+ disabled={
+ !featureForm.problem.trim() ||
+ !featureForm.whatItShouldDo.trim() ||
+ submitState === 'loading' ||
+ submitState === 'success'
+ }
+ className="flex-1"
+ >
+ {submitState === 'loading' ? 'Submitting…' : 'Submit Request'}
+
+
+
+ )}
@@ -617,7 +966,7 @@ function ShellHeader() {
-
+
{/* Avatar on mobile, full name on sm+ */}
{initials}
diff --git a/src/components/Button/ReportBugButton.tsx b/src/components/Button/ReportBugButton.tsx
index ab9e3d5a..3dbc2e41 100644
--- a/src/components/Button/ReportBugButton.tsx
+++ b/src/components/Button/ReportBugButton.tsx
@@ -12,7 +12,7 @@ export const ReportBugButton = () => {
size="sm"
onClick={open}
aria-label="Get help / report a bug"
- className="h-8 px-2 sm:px-2.5 gap-1.5 text-muted-foreground hover:text-foreground"
+ className="h-8 px-2 sm:px-2.5 gap-1.5 text-muted-foreground hover:text-foreground cursor-pointer"
>
Get Help
diff --git a/src/config/bug-report.ts b/src/config/bug-report.ts
deleted file mode 100644
index 444f3edf..00000000
--- a/src/config/bug-report.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-export const BUG_REPORT_FORM_ID =
- '1FAIpQLSe5ezRfmZ5f0NKM-YURKcMnU5JaJW7Lb4JEYMgLZc1Jpkk35w'
-
-export const BUG_REPORT_FIELDS = {
- pageUrl: 'entry.1545554500',
- reportedBy: 'entry.379779905',
- browser: 'entry.1927946772',
- timestamp: 'entry.1884488036',
- whatHappened: 'entry.1987870369',
-}
diff --git a/src/config/index.ts b/src/config/index.ts
index b24a228f..aee79eed 100644
--- a/src/config/index.ts
+++ b/src/config/index.ts
@@ -1,5 +1,4 @@
export * from './auth'
-export * from './bug-report'
export * from './storage-key'
export * from './pdf'
export * from './time'
diff --git a/src/utils/BuildBugReportUrl.ts b/src/utils/BuildBugReportUrl.ts
deleted file mode 100644
index 7c43cf23..00000000
--- a/src/utils/BuildBugReportUrl.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { BUG_REPORT_FIELDS, BUG_REPORT_FORM_ID } from '@/config'
-
-function formatReportedBy(name?: string, email?: string): string {
- const parts = [name, email ? `<${email}>` : ''].filter(Boolean)
- return parts.length > 0 ? parts.join(' ') : 'unknown'
-}
-
-export function buildBugReportUrl(context: {
- userName?: string
- userEmail?: string
-}): string {
- const params = new URLSearchParams({
- [BUG_REPORT_FIELDS.pageUrl]: window.location.href,
- [BUG_REPORT_FIELDS.reportedBy]: formatReportedBy(
- context.userName,
- context.userEmail
- ),
- [BUG_REPORT_FIELDS.browser]: navigator.userAgent,
- [BUG_REPORT_FIELDS.timestamp]: new Date().toISOString(),
- })
- return `https://docs.google.com/forms/d/e/${BUG_REPORT_FORM_ID}/viewform?usp=pp_url&${params}`
-}
diff --git a/src/utils/index.ts b/src/utils/index.ts
index 17855784..e15ff5df 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -27,6 +27,5 @@ export * from './UpdateMapView'
export * from './UtmToLonLat'
export * from './WellBatchExport'
export * from './wellSiteName'
-export * from './BuildBugReportUrl'
export * from './docsSearch'
export * from './searchModal'