v0.6.2 added help window

This commit is contained in:
2026-03-10 14:43:25 -04:00
parent 605d10ae02
commit 85cfad6318
18 changed files with 435 additions and 172 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "jama-frontend",
"version": "0.5.1",
"version": "0.6.2",
"private": true,
"scripts": {
"dev": "vite",
@@ -16,7 +16,8 @@
"@emoji-mart/data": "^1.1.2",
"@emoji-mart/react": "^1.1.1",
"papaparse": "^5.4.1",
"date-fns": "^3.3.1"
"date-fns": "^3.3.1",
"marked": "^12.0.0"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.2.1",

View File

@@ -0,0 +1,71 @@
import { useState, useEffect } from 'react';
import { marked } from 'marked';
import { api } from '../utils/api.js';
// Configure marked for safe rendering
marked.setOptions({ breaks: true, gfm: true });
export default function HelpModal({ onClose, dismissed: initialDismissed }) {
const [content, setContent] = useState('');
const [loading, setLoading] = useState(true);
const [dismissed, setDismissed] = useState(!!initialDismissed);
useEffect(() => {
api.getHelp()
.then(({ content }) => setContent(content))
.catch(() => setContent('# Getting Started\n\nHelp content could not be loaded.'))
.finally(() => setLoading(false));
}, []);
const handleDismissToggle = async (e) => {
const val = e.target.checked;
setDismissed(val);
try {
await api.dismissHelp(val);
if (val) onClose(); // immediately close when "do not show again" checked
} catch (_) {}
};
return (
<div className="modal-overlay" onClick={e => e.target === e.currentTarget && onClose()}>
<div className="modal help-modal">
{/* Header */}
<div className="flex items-center justify-between" style={{ marginBottom: 16 }}>
<h2 className="modal-title" style={{ margin: 0 }}>Getting Started</h2>
<button className="btn-icon" onClick={onClose}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</div>
{/* Scrollable markdown content */}
<div className="help-content">
{loading ? (
<div style={{ textAlign: 'center', padding: 40, color: 'var(--text-tertiary)' }}>Loading</div>
) : (
<div
className="help-markdown"
dangerouslySetInnerHTML={{ __html: marked.parse(content) }}
/>
)}
</div>
{/* Footer */}
<div className="help-footer">
<label className="flex items-center gap-2 text-sm" style={{ cursor: 'pointer', color: 'var(--text-secondary)' }}>
<input
type="checkbox"
checked={dismissed}
onChange={handleDismissToggle}
/>
Do not show again at login
</label>
<button className="btn btn-primary btn-sm" onClick={onClose}>Close</button>
</div>
</div>
</div>
);
}

View File

@@ -148,7 +148,7 @@ export default function SettingsModal({ onClose }) {
{settings.pw_reset_active === 'true' && (
<div className="warning-banner">
<span></span>
<span><strong>PW_RESET is active.</strong> The default admin password is being reset on every restart. Set PW_RESET=false in your environment variables to stop this.</span>
<span><strong>ADMPW_RESET is active.</strong> The default admin password is being reset on every restart. Set ADMPW_RESET=false in your environment variables to stop this.</span>
</div>
)}
</div>

View File

@@ -43,7 +43,7 @@ function useAppSettings() {
return settings;
}
export default function Sidebar({ groups, activeGroupId, onSelectGroup, notifications, unreadGroups = new Map(), onNewChat, onProfile, onUsers, onSettings: onOpenSettings, onGroupsUpdated, isMobile, onAbout }) {
export default function Sidebar({ groups, activeGroupId, onSelectGroup, notifications, unreadGroups = new Map(), onNewChat, onProfile, onUsers, onSettings: onOpenSettings, onGroupsUpdated, isMobile, onAbout, onHelp }) {
const { user, logout } = useAuth();
const { connected } = useSocket();
const toast = useToast();
@@ -219,6 +219,10 @@ export default function Sidebar({ groups, activeGroupId, onSelectGroup, notifica
</>
)}
<hr className="divider" style={{ margin: '4px 0' }} />
<button className="footer-menu-item" onClick={() => { setShowMenu(false); onHelp && onHelp(); }}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
Help
</button>
<button className="footer-menu-item" onClick={() => { setShowMenu(false); onAbout && onAbout(); }}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
About

View File

@@ -399,3 +399,50 @@ a { color: inherit; text-decoration: none; }
background: var(--primary-light);
}
/* ── Help Modal ─────────────────────────────────────────────── */
.help-modal {
width: min(720px, 94vw);
max-height: 85vh;
display: flex;
flex-direction: column;
}
.help-content {
flex: 1;
overflow-y: auto;
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 20px 24px;
background: var(--surface-variant);
margin-bottom: 16px;
}
.help-footer {
display: flex;
align-items: center;
justify-content: space-between;
padding-top: 4px;
border-top: 1px solid var(--border);
padding-top: 12px;
}
/* Markdown typography */
.help-markdown h1 { font-size: 1.5rem; font-weight: 700; margin: 0 0 16px; color: var(--text-primary); }
.help-markdown h2 { font-size: 1.15rem; font-weight: 700; margin: 24px 0 10px; color: var(--text-primary); border-bottom: 1px solid var(--border); padding-bottom: 4px; }
.help-markdown h3 { font-size: 1rem; font-weight: 600; margin: 16px 0 6px; color: var(--text-primary); }
.help-markdown p { margin: 0 0 12px; line-height: 1.65; color: var(--text-secondary); font-size: 14px; }
.help-markdown ul, .help-markdown ol { margin: 0 0 12px 20px; color: var(--text-secondary); font-size: 14px; }
.help-markdown li { margin-bottom: 4px; line-height: 1.6; }
.help-markdown strong { font-weight: 600; color: var(--text-primary); }
.help-markdown em { font-style: italic; }
.help-markdown code { font-family: monospace; font-size: 13px; background: var(--background); padding: 1px 5px; border-radius: 4px; color: var(--primary); }
.help-markdown pre { background: var(--background); border: 1px solid var(--border); border-radius: var(--radius); padding: 12px 16px; overflow-x: auto; margin: 0 0 12px; }
.help-markdown pre code { background: none; padding: 0; color: var(--text-primary); }
.help-markdown blockquote { border-left: 3px solid var(--primary); margin: 0 0 12px; padding: 6px 14px; background: var(--primary-light); border-radius: 0 var(--radius) var(--radius) 0; }
.help-markdown blockquote p { margin: 0; color: var(--text-secondary); }
.help-markdown hr { border: none; border-top: 1px solid var(--border); margin: 20px 0; }
.help-markdown a { color: var(--primary); text-decoration: underline; }
[data-theme="dark"] .help-markdown code { background: var(--surface); }
[data-theme="dark"] .help-markdown pre { background: var(--surface); }
[data-theme="dark"] .help-markdown blockquote { background: rgba(99,102,241,0.1); }

View File

@@ -11,6 +11,7 @@ import SettingsModal from '../components/SettingsModal.jsx';
import NewChatModal from '../components/NewChatModal.jsx';
import GlobalBar from '../components/GlobalBar.jsx';
import AboutModal from '../components/AboutModal.jsx';
import HelpModal from '../components/HelpModal.jsx';
import './Chat.css';
function urlBase64ToUint8Array(base64String) {
@@ -31,10 +32,21 @@ export default function Chat() {
const [activeGroupId, setActiveGroupId] = useState(null);
const [notifications, setNotifications] = useState([]);
const [unreadGroups, setUnreadGroups] = useState(new Map());
const [modal, setModal] = useState(null); // 'profile' | 'users' | 'settings' | 'newchat'
const [modal, setModal] = useState(null); // 'profile' | 'users' | 'settings' | 'newchat' | 'help'
const [helpDismissed, setHelpDismissed] = useState(true); // true until status loaded
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
const [showSidebar, setShowSidebar] = useState(true);
// Check if help should be shown on login
useEffect(() => {
api.getHelpStatus()
.then(({ dismissed }) => {
setHelpDismissed(dismissed);
if (!dismissed) setModal('help');
})
.catch(() => {});
}, []);
useEffect(() => {
const handle = () => {
const mobile = window.innerWidth < 768;
@@ -210,6 +222,7 @@ export default function Chat() {
onGroupsUpdated={loadGroups}
isMobile={isMobile}
onAbout={() => setModal('about')}
onHelp={() => setModal('help')}
/>
)}
@@ -228,6 +241,7 @@ export default function Chat() {
{modal === 'settings' && <SettingsModal onClose={() => setModal(null)} />}
{modal === 'newchat' && <NewChatModal onClose={() => setModal(null)} onCreated={(g) => { loadGroups(); setModal(null); setActiveGroupId(g.id); }} />}
{modal === 'about' && <AboutModal onClose={() => setModal(null)} />}
{modal === 'help' && <HelpModal onClose={() => setModal(null)} dismissed={helpDismissed} />}
</div>
);
}

View File

@@ -85,7 +85,7 @@ export default function Login() {
{settings.pw_reset_active === 'true' && (
<div className="warning-banner" style={{ marginBottom: 16 }}>
<span></span>
<span><strong>PW_RESET is enabled.</strong> The admin password is being reset on each restart. Disable PW_RESET in your environment to stop this behavior.</span>
<span><strong>ADMPW_RESET is enabled.</strong> The admin password is being reset on each restart. Disable ADMPW_RESET in your environment to stop this behavior.</span>
</div>
)}

View File

@@ -74,6 +74,9 @@ export const api = {
createGroup: (body) => req('POST', '/groups', body),
renameGroup: (id, name) => req('PATCH', `/groups/${id}/rename`, { name }),
setCustomGroupName: (id, name) => req('PATCH', `/groups/${id}/custom-name`, { name }),
getHelp: () => req('GET', '/help'),
getHelpStatus: () => req('GET', '/help/status'),
dismissHelp: (dismissed) => req('POST', '/help/dismiss', { dismissed }),
getMembers: (id) => req('GET', `/groups/${id}/members`),
addMember: (groupId, userId) => req('POST', `/groups/${groupId}/members`, { userId }),
removeMember: (groupId, userId) => req('DELETE', `/groups/${groupId}/members/${userId}`),