Improved Help and help chatbot
This commit is contained in:
+3
-1
@@ -1,2 +1,4 @@
|
||||
VITE_TASGRID_ENABLE_PROD_MIGRATION=false
|
||||
VITE_TASGRID_MIGRATION_ADMIN_EMAILS=tcardoza@cardoza.construction
|
||||
VITE_TASGRID_MIGRATION_ADMIN_EMAILS=tcardoza@cardoza.construction
|
||||
VITE_FIREWORKS_API_KEY=fw_2QZReEeWXyJsuYn3JWffTh
|
||||
VITE_FIREWORKS_HELP_MODEL=accounts/fireworks/models/gpt-oss-20b
|
||||
+807
-182
File diff suppressed because it is too large
Load Diff
+74
-13
@@ -1,11 +1,11 @@
|
||||
import { type Component, createSignal, onMount, onCleanup, createMemo, lazy, Suspense, Show } from 'solid-js';
|
||||
import { type Component, createSignal, onMount, onCleanup, createMemo, createEffect, lazy, Suspense, Show } from 'solid-js';
|
||||
import { Layout } from './components/Layout';
|
||||
import { EmbedLayout } from './components/EmbedLayout';
|
||||
import { EmbedAuthWrapper } from './components/EmbedAuthWrapper';
|
||||
import { CriticalView } from './views/CriticalView';
|
||||
import { AuthCallback } from './components/Auth';
|
||||
import { pb } from './lib/pocketbase';
|
||||
import { initStore } from './store';
|
||||
import { initStore, setStore } from './store';
|
||||
|
||||
// const CriticalView = lazy(() => import('./views/CriticalView').then(m => ({ default: m.CriticalView })));
|
||||
const UrgencyView = lazy(() => import('./views/UrgencyView').then(m => ({ default: m.UrgencyView })));
|
||||
@@ -16,19 +16,66 @@ const SnowballView = lazy(() => import('./views/SnowballView').then(m => ({ defa
|
||||
const DigInView = lazy(() => import('./views/DigInView').then(m => ({ default: m.DigInView })));
|
||||
const ProgressView = lazy(() => import('./views/ProgressView').then(m => ({ default: m.ProgressView })));
|
||||
const HelpView = lazy(() => import('./views/HelpView').then(m => ({ default: m.HelpView })));
|
||||
const AIHelpView = lazy(() => import('./views/AIHelpView').then(m => ({ default: m.AIHelpView })));
|
||||
const NotepadView = lazy(() => import('./views/NotepadView').then(m => ({ default: m.NotepadView })));
|
||||
const QuickEntryForm = lazy(() => import('./components/QuickEntryForm').then(m => ({ default: m.QuickEntryForm })));
|
||||
|
||||
const App: Component = () => {
|
||||
// Basic routing state
|
||||
const [currentView, setCurrentView] = createSignal("critical");
|
||||
const [lastNonAIView, setLastNonAIView] = createSignal("critical");
|
||||
const [isAuthenticated, setIsAuthenticated] = createSignal(pb.authStore.isValid);
|
||||
const [location, setLocation] = createSignal(window.location.href);
|
||||
const [isDesktop, setIsDesktop] = createSignal(window.matchMedia("(min-width: 768px)").matches);
|
||||
|
||||
createEffect(() => {
|
||||
const view = currentView();
|
||||
if (view !== "help_ai") {
|
||||
setLastNonAIView(view);
|
||||
}
|
||||
});
|
||||
|
||||
const effectiveView = createMemo(() =>
|
||||
currentView() === "help_ai" && isDesktop() ? lastNonAIView() : currentView()
|
||||
);
|
||||
|
||||
onMount(() => {
|
||||
const handleLocChange = () => setLocation(window.location.href);
|
||||
const mediaQuery = window.matchMedia("(min-width: 768px)");
|
||||
const openHelpGuide = (sectionId?: string) => {
|
||||
const nextHash = sectionId ? `#help/${sectionId}` : "#help";
|
||||
if (window.location.hash !== nextHash) {
|
||||
window.location.hash = nextHash;
|
||||
}
|
||||
setStore('isNotepadMode', false);
|
||||
setCurrentView("help");
|
||||
window.dispatchEvent(new CustomEvent("taskgrid:help-target", { detail: { sectionId } }));
|
||||
};
|
||||
|
||||
const openAIHelp = () => {
|
||||
setStore('isNotepadMode', false);
|
||||
setCurrentView("help_ai");
|
||||
};
|
||||
|
||||
const handleMediaChange = (event: MediaQueryListEvent) => {
|
||||
setIsDesktop(event.matches);
|
||||
};
|
||||
|
||||
const handleLocChange = () => {
|
||||
setLocation(window.location.href);
|
||||
|
||||
const hash = window.location.hash;
|
||||
if (hash === "#help" || hash.startsWith("#help/")) {
|
||||
setCurrentView("help");
|
||||
}
|
||||
};
|
||||
|
||||
(window as any).setCurrentView = setCurrentView;
|
||||
(window as any).openTasGridHelp = openHelpGuide;
|
||||
(window as any).openTasGridAIHelp = openAIHelp;
|
||||
|
||||
window.addEventListener('popstate', handleLocChange);
|
||||
window.addEventListener('hashchange', handleLocChange);
|
||||
mediaQuery.addEventListener('change', handleMediaChange);
|
||||
|
||||
let isFirstRun = true;
|
||||
const removeListener = pb.authStore.onChange((token) => {
|
||||
@@ -43,8 +90,12 @@ const App: Component = () => {
|
||||
|
||||
onCleanup(() => {
|
||||
removeListener();
|
||||
delete (window as any).setCurrentView;
|
||||
delete (window as any).openTasGridHelp;
|
||||
delete (window as any).openTasGridAIHelp;
|
||||
window.removeEventListener('popstate', handleLocChange);
|
||||
window.removeEventListener('hashchange', handleLocChange);
|
||||
mediaQuery.removeEventListener('change', handleMediaChange);
|
||||
});
|
||||
|
||||
// Background preload of other views when idle
|
||||
@@ -59,6 +110,7 @@ const App: Component = () => {
|
||||
import('./views/DigInView');
|
||||
import('./views/ProgressView');
|
||||
import('./views/HelpView');
|
||||
import('./views/AIHelpView');
|
||||
|
||||
// Remove splash screen after high priority work is done
|
||||
const splash = document.getElementById('splash');
|
||||
@@ -67,6 +119,8 @@ const App: Component = () => {
|
||||
setTimeout(() => splash.remove(), 500);
|
||||
}
|
||||
});
|
||||
|
||||
handleLocChange();
|
||||
});
|
||||
|
||||
// Determine if we are in an embed route (check both path and hash for compatibility)
|
||||
@@ -96,17 +150,24 @@ const App: Component = () => {
|
||||
when={isEmbed()}
|
||||
fallback={
|
||||
<Show when={isAuthenticated()} fallback={<AuthCallback onSuccess={() => { }} />}>
|
||||
<Layout currentView={currentView()} setView={setCurrentView}>
|
||||
<Layout
|
||||
currentView={effectiveView()}
|
||||
setView={setCurrentView}
|
||||
isAIHelpOpen={currentView() === "help_ai" && isDesktop()}
|
||||
onToggleAIHelp={() => setCurrentView(currentView() === "help_ai" ? lastNonAIView() : "help_ai")}
|
||||
hideQuickEntry={currentView() === "help_ai" && !isDesktop()}
|
||||
>
|
||||
<Suspense fallback={<div class="flex items-center justify-center h-full"><div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div></div>}>
|
||||
<Show when={currentView() === "critical"}><CriticalView /></Show>
|
||||
<Show when={currentView() === "urgency"}><UrgencyView /></Show>
|
||||
<Show when={currentView() === "priority"}><PriorityView /></Show>
|
||||
<Show when={currentView() === "matrix"}><MatrixView /></Show>
|
||||
<Show when={currentView() === "snowball"}><SnowballView /></Show>
|
||||
<Show when={currentView() === "dig_in"}><DigInView /></Show>
|
||||
<Show when={currentView() === "progress"}><ProgressView /></Show>
|
||||
<Show when={currentView() === "settings"}><SettingsView setView={setCurrentView} /></Show>
|
||||
<Show when={currentView() === "help"}><HelpView onBack={() => setCurrentView("settings")} /></Show>
|
||||
<Show when={effectiveView() === "critical"}><CriticalView /></Show>
|
||||
<Show when={effectiveView() === "urgency"}><UrgencyView /></Show>
|
||||
<Show when={effectiveView() === "priority"}><PriorityView /></Show>
|
||||
<Show when={effectiveView() === "matrix"}><MatrixView /></Show>
|
||||
<Show when={effectiveView() === "snowball"}><SnowballView /></Show>
|
||||
<Show when={effectiveView() === "dig_in"}><DigInView /></Show>
|
||||
<Show when={effectiveView() === "progress"}><ProgressView /></Show>
|
||||
<Show when={effectiveView() === "settings"}><SettingsView setView={setCurrentView} /></Show>
|
||||
<Show when={effectiveView() === "help_ai"}><AIHelpView /></Show>
|
||||
<Show when={effectiveView() === "help"}><HelpView onBack={() => setCurrentView("settings")} /></Show>
|
||||
</Suspense>
|
||||
</Layout>
|
||||
</Show>
|
||||
|
||||
@@ -0,0 +1,305 @@
|
||||
import { type Component, For, Show, createEffect, createMemo, createSignal } from "solid-js";
|
||||
import { Bot, LoaderCircle, RefreshCw, Send } from "lucide-solid";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { FIREWORKS_API_KEY, FIREWORKS_API_URL, FIREWORKS_HELP_MODEL } from "@/lib/app-config";
|
||||
import helpGuide from "../../docs/HELP_GUIDE.md?raw";
|
||||
|
||||
type ChatMessage = {
|
||||
role: "user" | "assistant";
|
||||
content: string;
|
||||
};
|
||||
|
||||
const SYSTEM_PROMPT = `You are the TasGrid AI Help assistant.
|
||||
|
||||
You answer questions about TasGrid using the reference guide below.
|
||||
|
||||
You must follow this instruction exactly: "use only this guide for all information and never assume you know something that isn't mentioned in the guide"
|
||||
|
||||
If the guide does not contain the answer, say that the guide does not mention it.
|
||||
Do not use outside product knowledge.
|
||||
Do not invent buttons, workflows, settings, or behaviors.
|
||||
Prefer concise, practical answers.
|
||||
When helpful, mention the relevant section name from the guide.
|
||||
|
||||
TASGRID REFERENCE GUIDE
|
||||
${helpGuide}`;
|
||||
|
||||
const SUGGESTED_PROMPTS = [
|
||||
"How does sharing work in TasGrid?",
|
||||
"What is the difference between Handoff and Collaborative?",
|
||||
"How do linked tasks and notes work together?",
|
||||
"Why might a task disappear from my view?",
|
||||
"How do recurrence and templates behave?"
|
||||
];
|
||||
|
||||
const INITIAL_MESSAGE = "Ask about TasGrid features, workflows, or edge cases. I only answer from the reference guide, so if the guide does not mention something, I will tell you.";
|
||||
|
||||
export const AIHelpPanel: Component<{
|
||||
variant?: "page" | "sidebar";
|
||||
class?: string;
|
||||
}> = (props) => {
|
||||
const variant = () => props.variant ?? "page";
|
||||
const [messages, setMessages] = createSignal<ChatMessage[]>([
|
||||
{ role: "assistant", content: INITIAL_MESSAGE }
|
||||
]);
|
||||
const [input, setInput] = createSignal("");
|
||||
const [isLoading, setIsLoading] = createSignal(false);
|
||||
const [error, setError] = createSignal<string | null>(null);
|
||||
let messagesViewport: HTMLDivElement | undefined;
|
||||
|
||||
const hasApiKey = createMemo(() => FIREWORKS_API_KEY.trim().length > 0);
|
||||
|
||||
createEffect(() => {
|
||||
messages();
|
||||
isLoading();
|
||||
queueMicrotask(() => {
|
||||
messagesViewport?.scrollTo({
|
||||
top: messagesViewport.scrollHeight,
|
||||
behavior: "smooth"
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const submitQuestion = async (questionOverride?: string) => {
|
||||
const question = (questionOverride ?? input()).trim();
|
||||
if (!question || isLoading()) return;
|
||||
|
||||
const priorMessages = messages();
|
||||
const userMessage: ChatMessage = { role: "user", content: question };
|
||||
const nextMessages = [...priorMessages, userMessage];
|
||||
|
||||
setMessages(nextMessages);
|
||||
setInput("");
|
||||
setError(null);
|
||||
setIsLoading(true);
|
||||
|
||||
if (!hasApiKey()) {
|
||||
setMessages([
|
||||
...nextMessages,
|
||||
{
|
||||
role: "assistant",
|
||||
content: "AI Help is not configured yet. Add `VITE_FIREWORKS_API_KEY` to the app environment to enable Fireworks responses."
|
||||
}
|
||||
]);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(FIREWORKS_API_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${FIREWORKS_API_KEY}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: FIREWORKS_HELP_MODEL,
|
||||
reasoning_effort: "low",
|
||||
temperature: 0.2,
|
||||
max_tokens: 700,
|
||||
messages: [
|
||||
{ role: "system", content: SYSTEM_PROMPT },
|
||||
...nextMessages.map(message => ({
|
||||
role: message.role,
|
||||
content: message.content
|
||||
}))
|
||||
]
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const bodyText = await response.text();
|
||||
throw new Error(bodyText || `Request failed with status ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const assistantText = data?.choices?.[0]?.message?.content?.trim();
|
||||
|
||||
if (!assistantText) {
|
||||
throw new Error("The help assistant did not return any text.");
|
||||
}
|
||||
|
||||
setMessages(prev => [...prev, { role: "assistant", content: assistantText }]);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : "Unknown error";
|
||||
setError(message);
|
||||
setMessages(prev => [
|
||||
...prev,
|
||||
{
|
||||
role: "assistant",
|
||||
content: "I couldn't get a response from the help model just now. Try again in a moment."
|
||||
}
|
||||
]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const resetChat = () => {
|
||||
setMessages([{ role: "assistant", content: INITIAL_MESSAGE }]);
|
||||
setInput("");
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const isSidebar = () => variant() === "sidebar";
|
||||
|
||||
return (
|
||||
<div
|
||||
class={cn(
|
||||
"flex min-h-0 flex-col overflow-hidden",
|
||||
isSidebar()
|
||||
? "rounded-2xl border border-border bg-card"
|
||||
: "h-full rounded-2xl border border-border bg-card",
|
||||
props.class
|
||||
)}
|
||||
>
|
||||
<div class={cn(
|
||||
"flex items-center justify-between gap-3 border-b border-border",
|
||||
isSidebar() ? "px-3 py-3" : "px-4 py-4 sm:px-5"
|
||||
)}>
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<Bot size={16} class="text-primary shrink-0" />
|
||||
<h1 class={cn("font-semibold tracking-tight", isSidebar() ? "text-sm" : "text-lg")}>AI Help</h1>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="gap-2 rounded-lg shrink-0"
|
||||
onClick={resetChat}
|
||||
>
|
||||
<RefreshCw size={13} />
|
||||
New
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class={cn(
|
||||
"flex min-h-0 flex-1 flex-col",
|
||||
isSidebar() ? "p-3 gap-3" : "p-4 gap-3 sm:p-5"
|
||||
)}>
|
||||
<Show
|
||||
when={hasApiKey()}
|
||||
fallback={
|
||||
<div class={cn(
|
||||
"rounded-2xl border border-amber-500/30 bg-amber-500/10",
|
||||
isSidebar() ? "p-3 text-[0.75rem]" : "p-4 text-sm"
|
||||
)}>
|
||||
Add `VITE_FIREWORKS_API_KEY` to enable AI Help. The view is wired to Fireworks model `{FIREWORKS_HELP_MODEL}`.
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<For each={SUGGESTED_PROMPTS}>
|
||||
{(prompt) => (
|
||||
<button
|
||||
class={cn(
|
||||
"rounded-full border border-border bg-background text-left font-medium text-foreground transition-colors hover:bg-muted",
|
||||
isSidebar() ? "px-2.5 py-1.5 text-[0.6875rem]" : "px-3 py-2 text-xs sm:text-sm"
|
||||
)}
|
||||
onClick={() => void submitQuestion(prompt)}
|
||||
disabled={isLoading()}
|
||||
>
|
||||
{prompt}
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div
|
||||
ref={messagesViewport}
|
||||
class={cn(
|
||||
"min-h-0 flex-1 overflow-y-auto space-y-3 rounded-xl border border-border bg-background",
|
||||
isSidebar() ? "p-2.5" : "p-3"
|
||||
)}
|
||||
>
|
||||
<For each={messages()}>
|
||||
{(message) => (
|
||||
<div class={cn("flex", message.role === "user" ? "justify-end" : "justify-start")}>
|
||||
<div
|
||||
class={cn(
|
||||
"rounded-2xl px-4 py-3 leading-relaxed whitespace-pre-wrap shadow-sm",
|
||||
isSidebar() ? "max-w-[92%] text-[0.75rem]" : "max-w-[90%] sm:max-w-[80%] text-sm",
|
||||
message.role === "user"
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-card border border-border text-foreground"
|
||||
)}
|
||||
>
|
||||
<div class="mb-1 flex items-center gap-2 text-[0.625rem] font-black uppercase tracking-[0.18em] opacity-70">
|
||||
<Show when={message.role === "assistant"} fallback={<span>You</span>}>
|
||||
<Bot size={12} />
|
||||
<span>Guide Assistant</span>
|
||||
</Show>
|
||||
</div>
|
||||
<div>{message.content}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
|
||||
<Show when={isLoading()}>
|
||||
<div class="flex justify-start">
|
||||
<div class={cn(
|
||||
"rounded-2xl px-4 py-3 leading-relaxed shadow-sm bg-card border border-border text-foreground",
|
||||
isSidebar() ? "max-w-[92%] text-[0.75rem]" : "max-w-[80%] text-sm"
|
||||
)}>
|
||||
<div class="mb-1 flex items-center gap-2 text-[0.625rem] font-black uppercase tracking-[0.18em] opacity-70">
|
||||
<Bot size={12} />
|
||||
<span>Guide Assistant</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-muted-foreground">
|
||||
<LoaderCircle size={14} class="animate-spin" />
|
||||
Looking through the guide...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<Show when={error()}>
|
||||
{(message) => (
|
||||
<div class={cn(
|
||||
"rounded-xl border border-destructive/30 bg-destructive/10 text-destructive",
|
||||
isSidebar() ? "px-3 py-2 text-[0.75rem]" : "px-4 py-3 text-sm"
|
||||
)}>
|
||||
Request error: {message()}
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
|
||||
<form
|
||||
class={cn(
|
||||
"space-y-2"
|
||||
)}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
void submitQuestion();
|
||||
}}
|
||||
>
|
||||
<textarea
|
||||
value={input()}
|
||||
onInput={(e) => setInput(e.currentTarget.value)}
|
||||
placeholder="Ask how a TasGrid feature works, what a setting does, or why something may behave a certain way..."
|
||||
class={cn(
|
||||
"flex w-full rounded-xl border border-input bg-background transition-colors placeholder:text-muted-foreground/60 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring resize-none leading-relaxed",
|
||||
isSidebar() ? "min-h-[88px] px-3 py-2.5 text-[0.75rem]" : "min-h-[96px] px-3 py-2.5 text-sm"
|
||||
)}
|
||||
/>
|
||||
<div class={cn(
|
||||
"flex gap-2",
|
||||
isSidebar() ? "flex-col" : "justify-end"
|
||||
)}>
|
||||
<Button
|
||||
type="submit"
|
||||
class={cn("gap-2 rounded-xl", isSidebar() && "w-full")}
|
||||
disabled={isLoading() || !input().trim()}
|
||||
>
|
||||
<Send size={14} />
|
||||
Ask AI Help
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+38
-10
@@ -15,6 +15,9 @@ interface LayoutProps {
|
||||
children: JSX.Element;
|
||||
currentView: string;
|
||||
setView: (v: string) => void;
|
||||
isAIHelpOpen?: boolean;
|
||||
onToggleAIHelp?: () => void;
|
||||
hideQuickEntry?: boolean;
|
||||
}
|
||||
|
||||
export const Layout: Component<LayoutProps> = (props) => {
|
||||
@@ -70,13 +73,27 @@ export const Layout: Component<LayoutProps> = (props) => {
|
||||
});
|
||||
|
||||
return (
|
||||
(() => {
|
||||
const sidebarWidthClass = () => {
|
||||
if (store.isNotepadMode) return "w-80";
|
||||
if (props.isAIHelpOpen) return "w-[24rem]";
|
||||
return "w-48";
|
||||
};
|
||||
|
||||
const sidebarPaddingClass = () => {
|
||||
if (store.isNotepadMode) return "md:pl-80";
|
||||
if (props.isAIHelpOpen) return "md:pl-[24rem]";
|
||||
return "md:pl-48";
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="flex h-screen w-full min-w-0 bg-background overflow-hidden relative font-sans antialiased text-foreground">
|
||||
{/* Desktop UI Container */}
|
||||
<div
|
||||
style={{ "will-change": "width, transform" }}
|
||||
class={cn(
|
||||
"hidden md:flex flex-col border-r border-border bg-card h-screen fixed left-0 top-0 transition-all duration-200 ease-in-out z-[50] overflow-hidden",
|
||||
isSidebarLocked() ? (store.isNotepadMode ? "w-80 translate-x-0" : "w-48 translate-x-0") : (store.isNotepadMode ? "w-80 -translate-x-full" : "w-48 -translate-x-full"),
|
||||
isSidebarLocked() ? `${sidebarWidthClass()} translate-x-0` : `${sidebarWidthClass()} -translate-x-full`,
|
||||
!isSidebarLocked() && (isSidebarPeeking() || (store.isNotepadMode && !selectedNoteId())) && "translate-x-0 shadow-2xl ring-1 ring-border"
|
||||
)}
|
||||
onMouseEnter={() => !isSidebarLocked() && setIsSidebarPeeking(true)}
|
||||
@@ -97,6 +114,8 @@ export const Layout: Component<LayoutProps> = (props) => {
|
||||
<Sidebar
|
||||
currentView={props.currentView}
|
||||
setView={props.setView}
|
||||
isAIHelpOpen={!!props.isAIHelpOpen}
|
||||
onToggleAIHelp={() => props.onToggleAIHelp?.()}
|
||||
isLocked={isSidebarLocked()}
|
||||
setIsLocked={setIsSidebarLocked}
|
||||
isPeeking={isSidebarPeeking()}
|
||||
@@ -149,7 +168,7 @@ export const Layout: Component<LayoutProps> = (props) => {
|
||||
class={cn(
|
||||
"flex-1 flex flex-col min-w-0 transition-all duration-200 ease-in-out h-screen",
|
||||
// Only apply md padding if locked
|
||||
isSidebarLocked() ? (store.isNotepadMode ? "md:pl-80" : "md:pl-48") : "md:pl-0"
|
||||
isSidebarLocked() ? sidebarPaddingClass() : "md:pl-0"
|
||||
)}>
|
||||
{/* Expand Trigger Button (visible when not locked) */}
|
||||
{!isSidebarLocked() && (
|
||||
@@ -212,15 +231,20 @@ export const Layout: Component<LayoutProps> = (props) => {
|
||||
|
||||
{/* Main Content Area */}
|
||||
<main class={cn(
|
||||
"flex-1 min-w-0 overflow-y-auto overflow-x-hidden relative scroll-smooth",
|
||||
"flex-1 min-w-0 overflow-x-hidden relative scroll-smooth",
|
||||
props.currentView === "help_ai" ? "overflow-hidden" : "overflow-y-auto",
|
||||
!store.isNotepadMode ? "sm:pb-6" : ""
|
||||
)}>
|
||||
<div class={cn(
|
||||
"w-full min-h-full mx-auto",
|
||||
!store.isNotepadMode ? "max-w-screen-2xl px-4 sm:px-8 lg:px-12 pt-16 sm:pt-20 mobile-footer-clearance space-y-6 sm:space-y-8" : "max-w-screen-2xl md:px-8 lg:px-12 md:pt-20 md:pb-8 pt-14"
|
||||
"w-full mx-auto",
|
||||
props.currentView === "help_ai"
|
||||
? "max-w-screen-2xl px-4 sm:px-8 lg:px-12 pt-16 sm:pt-20 pb-[calc(4.5rem+env(safe-area-inset-bottom,0px))] sm:pb-8 h-full"
|
||||
: !store.isNotepadMode
|
||||
? "min-h-full max-w-screen-2xl px-4 sm:px-8 lg:px-12 pt-16 sm:pt-20 mobile-footer-clearance space-y-6 sm:space-y-8"
|
||||
: "max-w-screen-2xl md:px-8 lg:px-12 md:pt-20 md:pb-8 pt-14"
|
||||
)}>
|
||||
<div class={cn(store.isNotepadMode ? "hidden" : "block")}>
|
||||
<Show when={props.currentView.toLowerCase() !== "settings"}>
|
||||
<div class={cn(store.isNotepadMode ? "hidden" : props.currentView === "help_ai" ? "block h-full" : "block")}>
|
||||
<Show when={!["settings", "help", "help_ai"].includes(props.currentView.toLowerCase())}>
|
||||
<TaskSearchPopover />
|
||||
</Show>
|
||||
{props.children}
|
||||
@@ -238,9 +262,11 @@ export const Layout: Component<LayoutProps> = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Suspense>
|
||||
<QuickEntry />
|
||||
</Suspense>
|
||||
<Show when={!props.hideQuickEntry}>
|
||||
<Suspense>
|
||||
<QuickEntry />
|
||||
</Suspense>
|
||||
</Show>
|
||||
|
||||
<Show when={activeTask()}>
|
||||
<Suspense>
|
||||
@@ -252,5 +278,7 @@ export const Layout: Component<LayoutProps> = (props) => {
|
||||
</Suspense>
|
||||
</Show>
|
||||
</div >
|
||||
);
|
||||
})()
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { type Component, For, Show } from "solid-js";
|
||||
import { LayoutDashboard, ListTodo, Settings, Clock, ArrowUpCircle, PanelLeftClose, Snowflake, Pickaxe, ChevronDown, Gauge, TrendingUp, Users, Box, BarChart3, HelpCircle } from "lucide-solid";
|
||||
import { LayoutDashboard, ListTodo, Settings, Clock, ArrowUpCircle, PanelLeftClose, Snowflake, Pickaxe, ChevronDown, Gauge, TrendingUp, Users, Box, BarChart3, HelpCircle, Bot } from "lucide-solid";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "./ui/button";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { store, currentTaskContext, setCurrentTaskContext, loadTasksForOwner } from "@/store";
|
||||
import { pb } from "@/lib/pocketbase";
|
||||
import { createSignal, createEffect } from "solid-js";
|
||||
import { AIHelpPanel } from "./AIHelpPanel";
|
||||
|
||||
interface NavItem {
|
||||
icon: any;
|
||||
@@ -34,13 +35,14 @@ interface MobileNavGroup {
|
||||
|
||||
const mobileNavGroups: MobileNavGroup[] = [
|
||||
{ icon: ListTodo, label: "Focus", view: "critical" },
|
||||
{ icon: LayoutDashboard, label: "Matrix", view: "matrix" },
|
||||
{
|
||||
icon: TrendingUp, label: "Value", items: [
|
||||
{ icon: ArrowUpCircle, label: "Priority", view: "priority" },
|
||||
{ icon: Clock, label: "Urgency", view: "urgency" },
|
||||
{ icon: LayoutDashboard, label: "Matrix", view: "matrix" },
|
||||
]
|
||||
},
|
||||
{ icon: Bot, label: "AI Help", view: "help_ai" },
|
||||
{
|
||||
icon: Gauge, label: "Flow", items: [
|
||||
{ icon: Snowflake, label: "Snowball", view: "snowball" },
|
||||
@@ -302,6 +304,8 @@ export const ContextSwitcher: Component<{
|
||||
export const Sidebar: Component<{
|
||||
currentView: string;
|
||||
setView: (v: string) => void;
|
||||
isAIHelpOpen: boolean;
|
||||
onToggleAIHelp: () => void;
|
||||
isLocked: boolean;
|
||||
setIsLocked: (v: boolean) => void;
|
||||
isPeeking: boolean;
|
||||
@@ -354,7 +358,7 @@ export const Sidebar: Component<{
|
||||
|
||||
|
||||
|
||||
<nav class="flex-1 px-3 space-y-1 mt-2">
|
||||
<nav class="flex-1 px-3 space-y-1 mt-2 min-h-0">
|
||||
<For each={desktopNavItems}>
|
||||
{(item) => (
|
||||
<button
|
||||
@@ -373,23 +377,45 @@ export const Sidebar: Component<{
|
||||
</For>
|
||||
</nav>
|
||||
|
||||
{/* Context Switcher - Only visible if there are oversight contexts */}
|
||||
<div class={cn(
|
||||
"px-3 mb-2 transition-opacity duration-200",
|
||||
(!props.isLocked && !props.isPeeking) ? "opacity-0 pointer-events-none" : "opacity-100 delay-100"
|
||||
)}>
|
||||
<ContextSwitcher
|
||||
isLocked={props.isLocked}
|
||||
isPeeking={props.isPeeking}
|
||||
setIsPeeking={props.setIsPeeking}
|
||||
onOpenChange={(open) => {
|
||||
setSwitcherOpen(open);
|
||||
props.onDropdownOpenChange?.(open);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="px-3 mt-auto pb-3 space-y-3">
|
||||
<div
|
||||
class={cn(
|
||||
"transition-all duration-300 ease-out overflow-hidden",
|
||||
props.isAIHelpOpen ? "max-h-[34rem] opacity-100" : "max-h-0 opacity-0 pointer-events-none"
|
||||
)}
|
||||
>
|
||||
<AIHelpPanel variant="sidebar" class="h-[32rem]" />
|
||||
</div>
|
||||
|
||||
<div class="px-3 border-t border-border mt-auto pt-3 space-y-1 pb-3">
|
||||
{/* Context Switcher - Only visible if there are oversight contexts */}
|
||||
<div class={cn(
|
||||
"transition-all duration-200",
|
||||
(!props.isLocked && !props.isPeeking) ? "opacity-0 pointer-events-none" : "opacity-100 delay-100"
|
||||
)}>
|
||||
<ContextSwitcher
|
||||
isLocked={props.isLocked}
|
||||
isPeeking={props.isPeeking}
|
||||
setIsPeeking={props.setIsPeeking}
|
||||
onOpenChange={(open) => {
|
||||
setSwitcherOpen(open);
|
||||
props.onDropdownOpenChange?.(open);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-border pt-3 space-y-1">
|
||||
<button
|
||||
onClick={props.onToggleAIHelp}
|
||||
class={cn(
|
||||
"flex items-center space-x-3 w-full px-3 py-2.5 rounded-lg transition-all duration-200 group text-sm",
|
||||
props.isAIHelpOpen
|
||||
? "bg-primary text-primary-foreground shadow-sm"
|
||||
: "text-muted-foreground hover:bg-muted hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
<Bot size={16} class={cn("transition-transform group-hover:scale-110")} />
|
||||
<span class="font-medium">AI Help</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => props.setView("help")}
|
||||
class={cn(
|
||||
@@ -400,7 +426,7 @@ export const Sidebar: Component<{
|
||||
)}
|
||||
>
|
||||
<HelpCircle size={16} class={cn("transition-transform group-hover:scale-110")} />
|
||||
<span class="font-medium">Help Guide</span>
|
||||
<span class="font-medium">Help</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => props.setView("settings")}
|
||||
@@ -414,6 +440,7 @@ export const Sidebar: Component<{
|
||||
<Settings size={16} class={cn("transition-transform group-hover:scale-110")} />
|
||||
<span class="font-medium">Settings</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
|
||||
@@ -6,6 +6,9 @@ const rawMigrationAdmins = import.meta.env.VITE_TASGRID_MIGRATION_ADMIN_EMAILS |
|
||||
const rawMigrationEnabled = (import.meta.env.VITE_TASGRID_ENABLE_PROD_MIGRATION || "").toLowerCase();
|
||||
|
||||
export const POCKETBASE_URL = import.meta.env.VITE_POCKETBASE_URL || "https://pocketbase.ccllc.pro";
|
||||
export const FIREWORKS_API_URL = import.meta.env.VITE_FIREWORKS_API_URL || "https://api.fireworks.ai/inference/v1/chat/completions";
|
||||
export const FIREWORKS_API_KEY = import.meta.env.VITE_FIREWORKS_API_KEY || "";
|
||||
export const FIREWORKS_HELP_MODEL = import.meta.env.VITE_FIREWORKS_HELP_MODEL || "accounts/fireworks/models/gpt-oss-20b";
|
||||
export const AUTH_STORE_KEY = TASGRID_IS_DEV_DATA ? "tasgrid_dev_auth" : "tasgrid_auth";
|
||||
export const STORAGE_KEY_PREFIX = TASGRID_IS_DEV_DATA ? "tasgrid_dev" : "tasgrid";
|
||||
export const PWA_CACHE_SUFFIX = TASGRID_IS_DEV_DATA ? "dev" : "prod";
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { type Component } from "solid-js";
|
||||
import { AIHelpPanel } from "@/components/AIHelpPanel";
|
||||
|
||||
export const AIHelpView: Component = () => {
|
||||
return (
|
||||
<div class="h-full min-h-0 w-full max-w-5xl mx-auto md:py-6 md:px-0">
|
||||
<AIHelpPanel variant="page" class="h-full" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+188
-565
@@ -1,583 +1,206 @@
|
||||
import { type Component, For, Show } from "solid-js";
|
||||
import {
|
||||
ArrowLeft,
|
||||
BookOpen,
|
||||
Boxes,
|
||||
ExternalLink,
|
||||
HelpCircle,
|
||||
LayoutDashboard,
|
||||
ListTodo,
|
||||
NotebookText,
|
||||
PencilLine,
|
||||
RefreshCcw,
|
||||
Search,
|
||||
Settings2,
|
||||
Share2,
|
||||
ShieldCheck,
|
||||
Smartphone,
|
||||
Sparkles,
|
||||
WandSparkles,
|
||||
Workflow,
|
||||
Zap
|
||||
} from "lucide-solid";
|
||||
import { type Component, For, Show, onCleanup, onMount } from "solid-js";
|
||||
import { ArrowLeft, BookOpen, Boxes, ExternalLink, HelpCircle, LayoutDashboard, Link2, ListTodo, NotebookText, PencilLine, RefreshCcw, Search, Settings2, Share2, ShieldCheck, Sparkles, WandSparkles, Workflow } from "lucide-solid";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
interface HelpViewProps {
|
||||
onBack?: () => void;
|
||||
}
|
||||
|
||||
interface GuideItem {
|
||||
title: string;
|
||||
body: string;
|
||||
bullets?: string[];
|
||||
}
|
||||
|
||||
interface GuideSection {
|
||||
title: string;
|
||||
icon: any;
|
||||
intro: string;
|
||||
items: GuideItem[];
|
||||
}
|
||||
|
||||
interface AccessCard {
|
||||
title: string;
|
||||
content: string;
|
||||
detail: string;
|
||||
href?: string;
|
||||
}
|
||||
interface HelpViewProps { onBack?: () => void; }
|
||||
interface AccessCard { title: string; content: string; detail: string; href?: string; }
|
||||
interface QuickAction { title: string; steps: string[]; target: string; }
|
||||
interface RelatedLink { label: string; target: string; }
|
||||
interface Section { id: string; title: string; icon: any; intro: string; body: string; steps: string[]; reminder: string; warning: string; related: RelatedLink[]; }
|
||||
interface DeepSection { id: string; title: string; icon: any; intro: string; bullets: string[]; why: string; related: RelatedLink[]; }
|
||||
|
||||
const accessCards: AccessCard[] = [
|
||||
{
|
||||
title: "Website",
|
||||
content: "tasgrid.ccllc.pro",
|
||||
href: "https://tasgrid.ccllc.pro",
|
||||
detail: "Open TasGrid in your browser or install it as an app on mobile."
|
||||
},
|
||||
{
|
||||
title: "Login",
|
||||
content: "Use your Cardoza Construction email",
|
||||
detail: "Your TasGrid account uses the same email and password you use for Outlook."
|
||||
},
|
||||
{
|
||||
title: "Finding Help",
|
||||
content: "Desktop: Sidebar Mobile: Settings > User Help Guide",
|
||||
detail: "The Help Guide is (This thing your looking at) is always available in the navigation sidebar on desktop or in settings on mobile, so you can reference it while learning the app."
|
||||
}
|
||||
{ title: "Website", content: "tasgrid.ccllc.pro", href: "https://tasgrid.ccllc.pro", detail: "Open TasGrid in your browser or install it as an app on mobile." },
|
||||
{ title: "Login", content: "Use your Cardoza Construction email", detail: "Your account uses the same email and password you use for Outlook." },
|
||||
{ title: "What This Guide Is For", content: "Refresh first, then go deeper", detail: "This guide supports the in-person training you already received." }
|
||||
];
|
||||
|
||||
const basicSections: GuideSection[] = [
|
||||
{
|
||||
title: "Create Tasks",
|
||||
icon: ListTodo,
|
||||
intro: "Quick Entry is the fastest way to get work into the system.",
|
||||
items: [
|
||||
{
|
||||
title: "Start a new task",
|
||||
body: "Use the floating plus button or press Ctrl/Cmd+K to open Quick Entry.",
|
||||
bullets: [
|
||||
"Title, Priority, and Urgency are the only required fields.",
|
||||
"You can also set Size, Due Date, tags, and a rich description before creating the task."
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Urgency and due date stay connected",
|
||||
body: "Adjust either one and TasGrid updates the other so your schedule and scoring stay aligned."
|
||||
},
|
||||
{
|
||||
title: "Tasks inherit your current context",
|
||||
body: "New tasks automatically pick up the context you are working inside.",
|
||||
bullets: [
|
||||
"My Bucket keeps the task in your personal context.",
|
||||
"Pinned bucket views add that bucket's @tag.",
|
||||
"Supervised views add the selected user's @tag.",
|
||||
"Creating from Notes mode adds the active note's #tag."
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Use templates and shorthand when you are moving fast",
|
||||
body: "Saved task templates can prefill common work, and Quick Entry shorthand can set properties without leaving the title field.",
|
||||
bullets: [
|
||||
"Use /pN for priority, /uN for urgency, and /t tag for tags.",
|
||||
"If a tag has a value, use /t tag:value."
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Edit and Manage Tasks",
|
||||
icon: PencilLine,
|
||||
intro: "Opening a task gives you the full working sheet for that item.",
|
||||
items: [
|
||||
{
|
||||
title: "Edit the task directly",
|
||||
body: "The task detail sheet lets you update title, status, priority, urgency, size, due date, tags, favorite state, and the rich text body."
|
||||
},
|
||||
{
|
||||
title: "Use quick actions from the list",
|
||||
body: "Without opening the detail sheet, you can still change status, duplicate a task, and toggle favorite on task cards."
|
||||
},
|
||||
{
|
||||
title: "Mark work complete",
|
||||
body: "Use the status control to move work through progress states or mark it complete.",
|
||||
bullets: [
|
||||
"Click the status circle on the left side of each task to mark it as partially or fully complete.",
|
||||
"Recurring tasks use the same status control and reopen automatically when their next cycle arrives."
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Trash is reversible",
|
||||
body: "Deleting a task sends it to trash for 7 days before permanent deletion and the app gives you an undo action right away."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Notes",
|
||||
icon: NotebookText,
|
||||
intro: "Notes are first-class workspace documents, not just task descriptions.",
|
||||
items: [
|
||||
{
|
||||
title: "Switch into Notes mode",
|
||||
body: "Use the Tasks/Notes toggle in the main layout to move between task lists and the notepad."
|
||||
},
|
||||
{
|
||||
title: "Create and edit notes",
|
||||
body: "Notes support titles, rich formatting, attachments, favorites, and public or private visibility."
|
||||
},
|
||||
{
|
||||
title: "Link tasks to notes",
|
||||
body: "TasGrid connects notes and tasks through #note tags and explicit note references.",
|
||||
bullets: [
|
||||
"Tasks created while a note is active automatically link back to that note.",
|
||||
"You can also search for an existing task and link it from the note view."
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Share notes when needed",
|
||||
body: "Notes can generate a share link from the note actions menu. If a note is private when you share it, TasGrid prompts you to make it public."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Share Tasks",
|
||||
icon: Share2,
|
||||
intro: "Sharing is tag-driven. The current app does not use the old manual per-task share flow as the primary model.",
|
||||
items: [
|
||||
{
|
||||
title: "Share with people, buckets, and notes",
|
||||
body: "Add @people, @buckets, or #notes in the tag list to create the sharing relationship."
|
||||
},
|
||||
{
|
||||
title: "Choose the share mode",
|
||||
body: "When you add a user, bucket, or note context through the tag picker, you can choose Collaborative or Handoff.",
|
||||
bullets: [
|
||||
"Collaborative keeps the task visible to both sides.",
|
||||
"Handoff moves responsibility to the receiving context."
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Read the active sharing state",
|
||||
body: "Task detail shows which contexts are attached to the task and which policy each one is using."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Buckets and Supervision",
|
||||
icon: Boxes,
|
||||
intro: "Contexts control where work lives and who can see it.",
|
||||
items: [
|
||||
{
|
||||
title: "My Bucket is your personal home view",
|
||||
body: "This is your normal personal context and where your own work lives by default."
|
||||
},
|
||||
{
|
||||
title: "Pinned buckets act like shared team spaces",
|
||||
body: "When you pin a bucket in Settings, it appears in the context switcher so you can jump into that shared workspace."
|
||||
},
|
||||
{
|
||||
title: "Supervision lets leads switch into personal contexts",
|
||||
body: "If someone grants you supervision access, their personal context appears in the same switcher as buckets."
|
||||
},
|
||||
{
|
||||
title: "Some system tags stay hidden on purpose",
|
||||
body: "TasGrid may hide the current bucket tag, your own personal context tag, or locked note tags while still preserving those relationships behind the scenes."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Views and Finding Work",
|
||||
icon: Search,
|
||||
intro: "TasGrid gives you multiple ways to surface the right work without manually resorting everything.",
|
||||
items: [
|
||||
{
|
||||
title: "Use the main views",
|
||||
body: "Focus, Matrix, Snowball, Dig In, Urgency, Priority, and Progress all present the same tasks through different sorting lenses."
|
||||
},
|
||||
{
|
||||
title: "Search and filter from the list",
|
||||
body: "Basic task filters include free-text search, tag filters, priority range, urgency range, edited today, mine, and starred."
|
||||
},
|
||||
{
|
||||
title: "Filter notes too",
|
||||
body: "Notes support their own search and filter flow, including note-specific saved filters."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Everyday Extras",
|
||||
icon: Smartphone,
|
||||
intro: "A few small habits make TasGrid easier to trust day to day.",
|
||||
items: [
|
||||
{
|
||||
title: "Recover from trash",
|
||||
body: "Settings includes separate trash areas for tasks and notes, with restore and permanent delete actions."
|
||||
},
|
||||
{
|
||||
title: "Install the app on mobile",
|
||||
body: "On Android, use Chrome's install or add-to-home-screen action. On iPhone, use Safari's Add to Home Screen option."
|
||||
},
|
||||
{
|
||||
title: "Refresh if things feel stale",
|
||||
body: "TasGrid updates in real time, but if the app ever feels out of sync, a quick refresh is the fastest first step."
|
||||
}
|
||||
]
|
||||
}
|
||||
const quickActions: QuickAction[] = [
|
||||
{ title: "Create a task", target: "create-tasks", steps: ["Open Quick Entry with + or Ctrl/Cmd+K.", "Enter Title, Priority, and Urgency.", "Create the task and let TasGrid keep the active context."] },
|
||||
{ title: "Edit a task", target: "manage-tasks", steps: ["Click the task to open it.", "Edit status, dates, tags, and notes.", "Use the status circle to update progress or completion."] },
|
||||
{ title: "Share a task", target: "share-tasks", steps: ["Open the task.", "Add an @person, @bucket, or #note tag.", "Choose Collaborative or Handoff if prompted."] },
|
||||
{ title: "Move work into a bucket", target: "buckets-supervision", steps: ["Add the bucket's @tag to the task.", "Or create the task while inside that bucket.", "Pin the bucket in Settings if you want it in the switcher."] },
|
||||
{ title: "Switch to notes", target: "notes", steps: ["Use the Tasks/Notes toggle.", "Open or create a note.", "Work inside it like a project document, not just a comment box."] },
|
||||
{ title: "Link a task to a note", target: "notes", steps: ["Open the note's Linked Tasks panel.", "Link an existing task or create a new one.", "TasGrid connects it through the note's #tag."] },
|
||||
{ title: "Find filtered work", target: "finding-work", steps: ["Use search and filters first.", "Switch views if you need a different ranking lens.", "Save the filter if you will reuse it."] },
|
||||
{ title: "Recover from trash", target: "recovery-trash", steps: ["Open Settings.", "Expand Trash.", "Recover the item before its 7-day window expires."] }
|
||||
];
|
||||
|
||||
const advancedSections: GuideSection[] = [
|
||||
{
|
||||
title: "Advanced Views",
|
||||
icon: LayoutDashboard,
|
||||
intro: "Each task view emphasizes a different decision-making lens.",
|
||||
items: [
|
||||
{
|
||||
title: "Focus, Urgency, Priority, Progress, Snowball, and Dig In",
|
||||
body: "These views sort the same active task pool by different scoring rules, so you can choose the mode that matches how you want to work right now."
|
||||
},
|
||||
{
|
||||
title: "Strategy Matrix",
|
||||
body: "Matrix plots incomplete tasks on an urgency-versus-priority grid.",
|
||||
bullets: [
|
||||
"Dots are clickable.",
|
||||
"Dot color reflects task size.",
|
||||
"The tooltip shows the task title plus U, S, and P values.",
|
||||
"The time scale can be changed to 1, 7, 30, 60, or 90 days."
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Notes Power Use",
|
||||
icon: NotebookText,
|
||||
intro: "Notes can operate like multi-tab project workspaces.",
|
||||
items: [
|
||||
{
|
||||
title: "Use tabs for structured note sets",
|
||||
body: "A root note can have child tabs, and those tabs inherit the parent's privacy settings.",
|
||||
bullets: [
|
||||
"Tabs can be renamed inline.",
|
||||
"Desktop supports drag-and-drop tab reordering."
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Understand root versus child note behavior",
|
||||
body: "Root notes manage the shared linked-task set. Child tabs can view those relationships, but root notes are the ones that can directly unlink tasks."
|
||||
},
|
||||
{
|
||||
title: "Keep note tags in sync automatically",
|
||||
body: "Renaming a note also renames the matching #note tag definition so linked tasks keep pointing at the right note."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Rich Editor",
|
||||
icon: WandSparkles,
|
||||
intro: "Tasks, notes, and templates all use the same editor, so the same advanced authoring tools work across the app.",
|
||||
items: [
|
||||
{
|
||||
title: "Use slash commands",
|
||||
body: "Type / in the editor to insert structured content.",
|
||||
bullets: [
|
||||
"Text and headings",
|
||||
"Bullet lists, checklists, and multi checklists",
|
||||
"Code blocks and blockquotes",
|
||||
"Uploads for images, videos, and documents",
|
||||
"Job File embeds",
|
||||
"Dividers and tables"
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Use formatting and mentions",
|
||||
body: "The editor supports underline, links, highlight, text alignment, resizable tables, and both @user and #note mentions."
|
||||
},
|
||||
{
|
||||
title: "Work with files and media",
|
||||
body: "You can paste or drag files directly into the editor.",
|
||||
bullets: [
|
||||
"Images are converted and inserted as image blocks.",
|
||||
"Videos become video blocks.",
|
||||
"Other files become file attachment blocks.",
|
||||
"Clicking an image opens a fullscreen zoomable preview."
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Embed job files from Prism",
|
||||
body: "The Job File slash command opens a searchable selector so users can attach plans, estimates, addendums, or files from the matched job."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Task Automation",
|
||||
icon: Workflow,
|
||||
intro: "Recurring work stays visible without forcing you to recreate the same task every cycle.",
|
||||
items: [
|
||||
{
|
||||
title: "Set recurrence from the task's More menu",
|
||||
body: "Tasks can repeat daily, weekly, or monthly."
|
||||
},
|
||||
{
|
||||
title: "Use weekly and monthly options",
|
||||
body: "Weekly recurrence supports selected weekdays, and monthly recurrence supports a chosen day of the month."
|
||||
},
|
||||
{
|
||||
title: "Let TasGrid reopen the task automatically",
|
||||
body: "Once a recurring task is completed, TasGrid resets it when the next scheduled cycle arrives."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Templates and Saved Filters",
|
||||
icon: BookOpen,
|
||||
intro: "Reuse good structure instead of re-entering the same setup every time.",
|
||||
items: [
|
||||
{
|
||||
title: "Task templates",
|
||||
body: "Templates can store a reusable name, title, priority, urgency, tags, and rich description, then be applied directly from Quick Entry."
|
||||
},
|
||||
{
|
||||
title: "Saved task and note filters",
|
||||
body: "Both task filters and note filters can be saved as named templates and reapplied later."
|
||||
},
|
||||
{
|
||||
title: "Load older history when needed",
|
||||
body: "Use Load All when you want to search or review older items that are not currently loaded into the active list."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Sharing and Context Policies",
|
||||
icon: Share2,
|
||||
intro: "Contexts are more than labels. They control access, ownership, and how work moves through the system.",
|
||||
items: [
|
||||
{
|
||||
title: "Collaborative versus Handoff",
|
||||
body: "Every user, bucket, or note context can behave as either collaborative access or a handoff target."
|
||||
},
|
||||
{
|
||||
title: "Per-user and per-bucket policies",
|
||||
body: "Settings lets you adjust user context policy and bucket policy independently, so not every shared space has to behave the same way."
|
||||
},
|
||||
{
|
||||
title: "Know the hidden rules",
|
||||
body: "TasGrid derives structured relationships from @user, @bucket, and #note tags, and some of those tags may be hidden in the current view while still remaining active."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Settings Deep Dive",
|
||||
icon: Settings2,
|
||||
intro: "Settings is where you shape how TasGrid behaves for you and your team.",
|
||||
items: [
|
||||
{
|
||||
title: "Theme, tags, and organization",
|
||||
body: "Choose Light, Dark, or System theme, manage tag values and colors, and review system tags for users, buckets, and notes."
|
||||
},
|
||||
{
|
||||
title: "Buckets and supervision",
|
||||
body: "Create buckets, rename them, pin or unpin them, archive old ones, and manage who can supervise your personal context."
|
||||
},
|
||||
{
|
||||
title: "Import and trash",
|
||||
body: "Bulk import tasks from pasted lists, then manage recoverable tasks and notes from their separate trash sections."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "App Maintenance",
|
||||
icon: Sparkles,
|
||||
intro: "TasGrid is a progressive web app, so it can install and update like a lightweight native app.",
|
||||
items: [
|
||||
{
|
||||
title: "Update flow",
|
||||
body: "TasGrid checks for updates on registration, regularly while running, and when the app becomes visible again.",
|
||||
bullets: [
|
||||
"You can manually trigger Update App from Settings.",
|
||||
"A ready update may also apply automatically when the app is hidden or idle."
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Reset the PWA if needed",
|
||||
body: "Reset PWA clears cached app state, unregisters service workers, and reloads the app."
|
||||
},
|
||||
{
|
||||
title: "Install behavior",
|
||||
body: "Mobile installation is supported, and the installed app uses the same Help, Notes, Tasks, and Settings experience as the browser version."
|
||||
}
|
||||
]
|
||||
}
|
||||
const coreSections: Section[] = [
|
||||
{ id: "create-tasks", title: "Create Tasks", icon: ListTodo, intro: "Use this when you remember the concept but forgot the exact task flow.", body: "Quick Entry is the main capture flow. It keeps scheduling and context consistent for you.", steps: ["Open Quick Entry with the floating plus button or Ctrl/Cmd+K.", "Enter Title, Priority, and Urgency. Add Size, Due Date, tags, and rich details if needed.", "Create the task. TasGrid automatically applies the current context, like a bucket, supervised user, or active note."], reminder: "Urgency and Due Date stay linked, and new tasks inherit the place you are currently working from.", warning: "If you create work inside a bucket, supervised context, or note, TasGrid usually adds that context automatically.", related: [{ label: "How contexts and sharing work", target: "contexts-sharing" }, { label: "How to find work later", target: "finding-work" }] },
|
||||
{ id: "manage-tasks", title: "Edit and Manage Tasks", icon: PencilLine, intro: "Use this when you know the task exists but forgot where a control lives.", body: "The task detail sheet is the main editing surface for title, status, dates, tags, favorites, and task notes.", steps: ["Click a task to open the detail sheet.", "Edit title, status, priority, urgency, size, due date, tags, favorite state, and the rich text body.", "Use the status circle to move through progress states or mark the task complete."], reminder: "Task cards also support quick actions like status updates, duplicate, and favorite.", warning: "Deleting a task sends it to trash first, so check trash before assuming something is gone forever.", related: [{ label: "Recover from trash", target: "recovery-trash" }, { label: "Templates and recurrence", target: "time-savers" }] },
|
||||
{ id: "notes", title: "Notes", icon: NotebookText, intro: "Use this when you need to refresh how notes behave as workspaces.", body: "Notes are a full mode inside the app. They can hold rich content, linked tasks, favorites, and public or private visibility.", steps: ["Switch to Notes using the Tasks/Notes toggle.", "Create or open a note from the notes sidebar.", "Use Linked Tasks to connect existing work or create a task directly from the note context."], reminder: "Creating a task from a note usually links it automatically through the note's #tag.", warning: "If you rename a note, TasGrid also updates the matching #note tag definition so linked tasks stay aligned.", related: [{ label: "How notes become project workspaces", target: "notes-project-workspaces" }, { label: "How rich editing works", target: "editor-rich-content" }] },
|
||||
{ id: "share-tasks", title: "Share Tasks", icon: Share2, intro: "Use this if you remember that sharing exists, but not the current model.", body: "Task sharing is tag-driven. You share by attaching a user, bucket, or note context to the task.", steps: ["Open the task and edit its tags.", "Add an @person, @bucket, or #note context through the tag picker.", "Choose Collaborative or Handoff when TasGrid asks for a sharing mode."], reminder: "The old mental model of a separate share dialog is no longer the main workflow.", warning: "Handoff and Collaborative do very different things. Handoff moves responsibility; Collaborative keeps the task visible in both places.", related: [{ label: "How contexts and sharing work", target: "contexts-sharing" }, { label: "Buckets and supervision", target: "buckets-supervision" }] },
|
||||
{ id: "buckets-supervision", title: "Buckets and Supervision", icon: Boxes, intro: "Use this if you forgot how to move between your work, team buckets, and supervised contexts.", body: "Contexts control where work lives and who can see it. The same switcher handles My Bucket, pinned shared buckets, and supervised personal contexts.", steps: ["Use the context switcher to move between My Bucket, pinned buckets, and supervised contexts.", "Pin buckets in Settings if you want them in the switcher.", "Create tasks from the context you are already inside so TasGrid applies the right tag automatically."], reminder: "Your own personal context tag, the active bucket tag, and some note tags may stay hidden while still remaining active.", warning: "If a task seems to belong to a bucket or person even though you cannot see the tag, hidden context behavior is often the reason.", related: [{ label: "Share tasks", target: "share-tasks" }, { label: "Deep dive on context behavior", target: "contexts-sharing" }] },
|
||||
{ id: "finding-work", title: "Views and Finding Work", icon: Search, intro: "Use this when the task exists but you cannot remember how to surface it quickly.", body: "TasGrid gives you multiple sort views plus search and filters so you can find the right work without rebuilding lists by hand.", steps: ["Use search for text and the filter controls for tags, ranges, edited today, mine, or starred.", "Switch between Focus, Matrix, Snowball, Dig In, Urgency, Priority, or Progress depending on the lens you need.", "Save filters you reuse often so you do not have to rebuild them every time."], reminder: "Changing views is often faster than changing the task itself.", warning: "If a task seems missing, first check your current context and filters before assuming it was deleted or moved.", related: [{ label: "How task views think", target: "task-views-think" }, { label: "Templates and saved filters", target: "time-savers" }] },
|
||||
{ id: "recovery-trash", title: "Recovery and Trash", icon: RefreshCcw, intro: "Use this when you are trying to undo something or confirm whether an item is really gone.", body: "Settings contains separate trash sections for tasks and notes. Items stay there for 7 days before permanent deletion.", steps: ["Open Settings and expand Trash.", "Look in the Tasks or Notes list depending on what you lost.", "Recover the item or permanently delete it if you are sure."], reminder: "Deletion is usually reversible during the trash window, and the app also offers an immediate undo action.", warning: "Expired trash is permanently removed when the app loads, so do not rely on trash as long-term storage.", related: [{ label: "Manage tasks", target: "manage-tasks" }, { label: "How settings change behavior", target: "settings-behavior" }] }
|
||||
];
|
||||
|
||||
const GuideSectionCard: Component<{ section: GuideSection }> = (props) => {
|
||||
const Icon = props.section.icon;
|
||||
const deepSections: DeepSection[] = [
|
||||
{ id: "task-views-think", title: "How Task Views Think", icon: LayoutDashboard, intro: "Use this when you want to understand why the same task appears differently across views.", bullets: ["Focus, Urgency, Priority, Progress, Snowball, and Dig In all sort the same active task pool by different rules.", "Matrix places incomplete tasks on an urgency-versus-priority grid instead of a list.", "Matrix dots are clickable, size-aware, and show U, S, and P values.", "The Matrix time scale can be adjusted to 1, 7, 30, 60, or 90 days."], why: "If users understand the view's mental model, they stop fighting the sort order and start choosing the right view for the moment.", related: [{ label: "Find work refresher", target: "finding-work" }, { label: "Templates and saved filters", target: "time-savers" }] },
|
||||
{ id: "contexts-sharing", title: "How Contexts and Sharing Work", icon: Share2, intro: "Use this when the app's access behavior feels smarter or stranger than a normal tag list.", bullets: ["TasGrid turns @user, @bucket, and #note tags into structured relationships, not just labels.", "Each context can behave as Collaborative or Handoff.", "Per-user policies, bucket policies, and supervision access all change how work moves and who can see it.", "Some context tags stay hidden in the current view while still remaining active."], why: "Most sharing confusion comes from treating contexts like plain tags when they actually control access and ownership behavior.", related: [{ label: "Share tasks refresher", target: "share-tasks" }, { label: "Buckets and supervision", target: "buckets-supervision" }] },
|
||||
{ id: "notes-project-workspaces", title: "How Notes Become Project Workspaces", icon: NotebookText, intro: "Use this when you want to go beyond simple note editing and understand note structure.", bullets: ["Root notes can have child tabs that inherit the parent note's privacy.", "Desktop supports drag-and-drop tab ordering.", "Linked tasks live at the note workspace level, and root notes manage those relationships directly.", "Share links open note content through the embed route."], why: "Once users treat notes like a workspace with tabs and linked tasks, notes become much more useful than static text pages.", related: [{ label: "Notes refresher", target: "notes" }, { label: "How the editor handles rich content", target: "editor-rich-content" }] },
|
||||
{ id: "editor-rich-content", title: "How the Editor Handles Rich Content", icon: WandSparkles, intro: "Use this when someone remembers that the editor is powerful, but forgot what it can do.", bullets: ["Tasks, notes, and templates all use the same editor.", "Typing / opens slash commands for blocks, lists, checklists, uploads, job files, dividers, and tables.", "The editor supports underline, links, highlight, alignment, resizable tables, @user mentions, and #note mentions.", "Images, videos, and files are handled differently and can be pasted or dragged directly into the editor."], why: "Users trust the note and task body more when they know it can hold real working content, not just plain text.", related: [{ label: "Notes refresher", target: "notes" }, { label: "How notes become project workspaces", target: "notes-project-workspaces" }] },
|
||||
{ id: "time-savers", title: "How Templates, Filters, and Recurrence Save Time", icon: Workflow, intro: "Use this when the user understands the basics and wants to stop repeating setup work.", bullets: ["Task templates can store reusable titles, priority, urgency, tags, and rich descriptions.", "Task filters and note filters can be saved as reusable templates.", "Load All is useful when older history is not currently loaded into the visible list.", "Recurrence supports daily, weekly, and monthly schedules, and completed recurring tasks reopen automatically."], why: "These features remove repeated setup and reduce the amount of remembering users have to do from scratch.", related: [{ label: "Create tasks refresher", target: "create-tasks" }, { label: "Find work refresher", target: "finding-work" }] },
|
||||
{ id: "settings-behavior", title: "How Settings Change Behavior", icon: Settings2, intro: "Use this when a user needs to understand how the app can be configured, not just used.", bullets: ["Settings controls theme, tag values and colors, bucket creation and pinning, supervision access, import tools, trash recovery, app updates, and PWA reset.", "Shared Buckets defines which team spaces exist and whether they behave as Collaborative or Handoff.", "Tag Sharing defines how user contexts behave and who can supervise your personal context.", "Import Tasks helps convert pasted lists into structured work with bulk defaults."], why: "Most team-level confusion comes from not realizing that sharing, buckets, and maintenance behavior are defined in Settings.", related: [{ label: "Buckets and supervision", target: "buckets-supervision" }, { label: "How contexts and sharing work", target: "contexts-sharing" }] }
|
||||
];
|
||||
|
||||
return (
|
||||
<article class="rounded-3xl border border-border/60 bg-card/70 backdrop-blur-sm overflow-hidden shadow-sm">
|
||||
<div class="p-5 sm:p-6 border-b border-border/40 bg-gradient-to-r from-primary/[0.08] via-primary/[0.03] to-transparent">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="hidden sm:flex h-11 w-11 shrink-0 items-center justify-center rounded-2xl bg-primary/10 text-primary">
|
||||
<Icon size={20} />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<h3 class="text-lg sm:text-xl font-black tracking-tight flex items-center gap-2">
|
||||
<Icon size={18} class="sm:hidden text-primary" />
|
||||
{props.section.title}
|
||||
</h3>
|
||||
<p class="text-sm text-muted-foreground leading-relaxed">{props.section.intro}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
const jumpLinks = [
|
||||
{ label: "Get Unstuck Fast", target: "quick-start" },
|
||||
{ label: "Create Tasks", target: "create-tasks" },
|
||||
{ label: "Share Tasks", target: "share-tasks" },
|
||||
{ label: "Notes", target: "notes" },
|
||||
{ label: "Find Work", target: "finding-work" },
|
||||
{ label: "Deep Dive", target: "task-views-think" }
|
||||
];
|
||||
|
||||
<div class="p-5 sm:p-6 space-y-4">
|
||||
<For each={props.section.items}>
|
||||
{(item) => (
|
||||
<div class="rounded-2xl border border-border/40 bg-background/60 p-4 space-y-2">
|
||||
<h4 class="text-sm font-bold tracking-tight">{item.title}</h4>
|
||||
<p class="text-sm text-muted-foreground leading-relaxed">{item.body}</p>
|
||||
<Show when={item.bullets?.length}>
|
||||
<ul class="space-y-1.5 pt-1">
|
||||
<For each={item.bullets}>
|
||||
{(bullet) => (
|
||||
<li class="text-sm text-muted-foreground leading-relaxed flex gap-2">
|
||||
<span class="mt-[0.45rem] h-1.5 w-1.5 shrink-0 rounded-full bg-primary/60" />
|
||||
<span>{bullet}</span>
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
</ul>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
};
|
||||
const RelatedTopics: Component<{ items: RelatedLink[]; onJump: (id: string) => void }> = (props) => (
|
||||
<div class="pt-3 border-t border-border/40 space-y-2">
|
||||
<p class="text-[0.625rem] font-black uppercase tracking-widest text-muted-foreground">Related topics</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<For each={props.items}>{(item) => <button class="rounded-full border border-border/60 bg-background px-3 py-1.5 text-[0.6875rem] font-medium text-muted-foreground hover:text-foreground hover:border-primary/40 hover:bg-primary/5 transition-colors" onClick={() => props.onJump(item.target)}>{item.label}</button>}</For>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const HelpView: Component<HelpViewProps> = (props) => {
|
||||
return (
|
||||
<div class="w-full max-w-5xl mx-auto py-4 sm:py-10 px-4 sm:px-0 animate-in fade-in slide-in-from-bottom-2 duration-500">
|
||||
<header class="flex items-center gap-4 mb-8 sm:mb-10">
|
||||
<Show when={props.onBack}>
|
||||
<Button variant="ghost" size="icon" onClick={() => props.onBack?.()} class="h-10 w-10 rounded-xl">
|
||||
<ArrowLeft size={20} />
|
||||
</Button>
|
||||
</Show>
|
||||
<div>
|
||||
<h1 class="text-2xl sm:text-4xl font-black tracking-tighter flex items-center gap-3">
|
||||
<HelpCircle size={32} class="text-primary hidden sm:block" />
|
||||
Help Guide
|
||||
</h1>
|
||||
<p class="text-[0.625rem] sm:text-xs text-muted-foreground font-mono uppercase tracking-widest opacity-70">
|
||||
Full guide to TasGrid 's daily workflows and advanced tools
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
const scrollToSection = (id?: string, behavior: ScrollBehavior = "smooth") => {
|
||||
if (!id) return;
|
||||
document.getElementById(`help-${id}`)?.scrollIntoView({ behavior, block: "start" });
|
||||
};
|
||||
const getSectionFromHash = () => window.location.hash.startsWith("#help/") ? decodeURIComponent(window.location.hash.slice("#help/".length)) : undefined;
|
||||
const jumpTo = (id: string) => { if (window.location.hash !== `#help/${id}`) window.location.hash = `#help/${id}`; scrollToSection(id); };
|
||||
|
||||
<div class="space-y-12 pb-20">
|
||||
<section class="space-y-5">
|
||||
<h2 class="text-xl font-bold border-b border-border pb-2 flex items-center gap-2">
|
||||
<ShieldCheck size={20} class="text-primary" />
|
||||
Welcome & Access
|
||||
</h2>
|
||||
<div class="grid gap-4 md:grid-cols-3">
|
||||
<For each={accessCards}>
|
||||
{(card) => (
|
||||
<div class="rounded-2xl border border-border/50 bg-card/50 p-4 space-y-2 shadow-sm">
|
||||
<p class="text-[0.625rem] font-black uppercase tracking-widest text-muted-foreground">
|
||||
{card.title}
|
||||
</p>
|
||||
<Show
|
||||
when={card.href}
|
||||
fallback={<p class="text-sm font-bold tracking-tight">{card.content}</p>}
|
||||
>
|
||||
<a
|
||||
href={card.href}
|
||||
target="_blank"
|
||||
class="text-sm font-bold tracking-tight text-primary flex items-center gap-1 hover:underline"
|
||||
>
|
||||
{card.content}
|
||||
<ExternalLink size={12} />
|
||||
</a>
|
||||
</Show>
|
||||
<p class="text-sm text-muted-foreground leading-relaxed">{card.detail}</p>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</section>
|
||||
onMount(() => {
|
||||
const syncToHash = () => window.setTimeout(() => scrollToSection(getSectionFromHash(), "auto"), 50);
|
||||
const handleHelpTarget = (event: Event) => {
|
||||
const detail = (event as CustomEvent).detail as { sectionId?: string } | undefined;
|
||||
window.setTimeout(() => scrollToSection(detail?.sectionId || getSectionFromHash()), 50);
|
||||
};
|
||||
syncToHash();
|
||||
window.addEventListener("hashchange", syncToHash);
|
||||
window.addEventListener("taskgrid:help-target", handleHelpTarget);
|
||||
onCleanup(() => {
|
||||
window.removeEventListener("hashchange", syncToHash);
|
||||
window.removeEventListener("taskgrid:help-target", handleHelpTarget);
|
||||
});
|
||||
});
|
||||
|
||||
<section class="space-y-6">
|
||||
<div class="space-y-2">
|
||||
<h2 class="text-xl font-bold border-b border-border pb-2 flex items-center gap-2">
|
||||
<ListTodo size={20} class="text-primary" />
|
||||
Basic Functions
|
||||
</h2>
|
||||
<p class="text-sm text-muted-foreground leading-relaxed">
|
||||
Start here if you want the day-to-day version of TasGrid . These sections cover the normal workflows most users touch every day.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-5">
|
||||
<For each={basicSections}>
|
||||
{(section) => <GuideSectionCard section={section} />}
|
||||
</For>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="space-y-6">
|
||||
<div class="space-y-2">
|
||||
<h2 class="text-xl font-bold border-b border-border pb-2 flex items-center gap-2">
|
||||
<Zap size={20} class="text-yellow-500" />
|
||||
Advanced Features
|
||||
</h2>
|
||||
<p class="text-sm text-muted-foreground leading-relaxed">
|
||||
This section covers the deeper controls, power-user tools, and settings-driven behavior that shape how TasGrid works across teams.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-5">
|
||||
<For each={advancedSections}>
|
||||
{(section) => <GuideSectionCard section={section} />}
|
||||
</For>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer class="pt-10 border-t border-border flex flex-col items-center text-center space-y-4">
|
||||
<p class="text-sm text-muted-foreground max-w-2xl leading-relaxed">
|
||||
If the app feels out of sync, a quick{" "}
|
||||
<button
|
||||
onClick={() => location.reload()}
|
||||
class="text-primary font-bold hover:underline cursor-pointer bg-transparent border-none p-0 inline-flex items-center gap-1"
|
||||
>
|
||||
refresh
|
||||
<RefreshCcw size={12} />
|
||||
</button>{" "}
|
||||
is still the best first step. Most changes sync automatically in real time.
|
||||
</p>
|
||||
<p class="text-sm font-medium">
|
||||
If you still need help after that, contact Timothy Cardoza or IT.
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
return (
|
||||
<div class="w-full max-w-5xl mx-auto py-4 sm:py-10 px-4 sm:px-0 animate-in fade-in slide-in-from-bottom-2 duration-500">
|
||||
<header class="flex items-center gap-4 mb-8 sm:mb-10">
|
||||
<Show when={props.onBack}><Button variant="ghost" size="icon" onClick={() => props.onBack?.()} class="h-10 w-10 rounded-xl"><ArrowLeft size={20} /></Button></Show>
|
||||
<div>
|
||||
<h1 class="text-2xl sm:text-4xl font-black tracking-tighter flex items-center gap-3"><HelpCircle size={32} class="text-primary hidden sm:block" />Help</h1>
|
||||
<p class="text-[0.625rem] sm:text-xs text-muted-foreground font-mono uppercase tracking-widest opacity-70">Refresh what you forgot, then learn the deeper system</p>
|
||||
</div>
|
||||
);
|
||||
</header>
|
||||
|
||||
<div class="space-y-12 pb-20">
|
||||
<section class="rounded-3xl border border-primary/20 bg-primary/[0.05] p-5 sm:p-6 space-y-4">
|
||||
<div class="space-y-2">
|
||||
<p class="text-[0.625rem] font-black uppercase tracking-widest text-primary/80">Jump to</p>
|
||||
<h2 class="text-xl font-black tracking-tight">Need the fast version?</h2>
|
||||
<p class="text-sm text-muted-foreground leading-relaxed">Use these shortcuts when you remember being shown something, but do not remember the exact steps anymore.</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<For each={jumpLinks}>{(item) => <button class="rounded-full border border-primary/20 bg-background/80 px-3 py-1.5 text-[0.6875rem] font-semibold text-foreground hover:bg-primary/10 hover:border-primary/40 transition-colors" onClick={() => jumpTo(item.target)}>{item.label}</button>}</For>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="help-quick-start" class="space-y-5">
|
||||
<div class="space-y-2">
|
||||
<h2 class="text-xl font-bold border-b border-border pb-2 flex items-center gap-2"><Sparkles size={20} class="text-primary" />Get Unstuck Fast</h2>
|
||||
<p class="text-sm text-muted-foreground leading-relaxed">These cards answer the questions users most often have after in-person training.</p>
|
||||
</div>
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<For each={quickActions}>{(action) => (
|
||||
<div class="rounded-2xl border border-border/50 bg-card/70 shadow-sm p-4 space-y-3">
|
||||
<div class="space-y-1"><p class="text-[0.625rem] font-black uppercase tracking-widest text-primary/80">I need to...</p><h3 class="text-sm font-bold tracking-tight">{action.title}</h3></div>
|
||||
<ol class="space-y-2">
|
||||
<For each={action.steps}>{(step, index) => <li class="flex gap-3 text-sm text-muted-foreground leading-relaxed"><span class="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-primary/10 text-primary text-[0.625rem] font-bold">{index() + 1}</span><span>{step}</span></li>}</For>
|
||||
</ol>
|
||||
<Button variant="ghost" size="sm" class="h-8 px-2 text-[0.6875rem] font-bold text-primary hover:bg-primary/10" onClick={() => jumpTo(action.target)}>Open full guide</Button>
|
||||
</div>
|
||||
)}</For>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="space-y-6 rounded-[2rem] border border-sky-200/40 bg-sky-500/[0.04] p-5 sm:p-6">
|
||||
<div class="space-y-2">
|
||||
<h2 class="text-xl font-bold border-b border-border/60 pb-2 flex items-center gap-2"><BookOpen size={20} class="text-sky-600" />Core Workflows</h2>
|
||||
<p class="text-sm text-muted-foreground leading-relaxed">This layer is for refreshing the workflows you already learned in person.</p>
|
||||
</div>
|
||||
<div class="space-y-5">
|
||||
<For each={coreSections}>{(section) => {
|
||||
const Icon = section.icon;
|
||||
return <article id={`help-${section.id}`} class="rounded-3xl border border-border/50 bg-card/80 shadow-sm overflow-hidden">
|
||||
<div class="p-5 sm:p-6 border-b border-border/40 bg-gradient-to-r from-sky-500/[0.08] via-sky-500/[0.03] to-transparent">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="hidden sm:flex h-11 w-11 shrink-0 items-center justify-center rounded-2xl bg-sky-500/10 text-sky-700"><Icon size={20} /></div>
|
||||
<div class="space-y-2 min-w-0"><h3 class="text-lg sm:text-xl font-black tracking-tight flex items-center gap-2"><Icon size={18} class="sm:hidden text-sky-700" />{section.title}</h3><p class="text-sm text-muted-foreground leading-relaxed">{section.intro}</p></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-5 sm:p-6 space-y-5">
|
||||
<div class="space-y-2"><p class="text-[0.625rem] font-black uppercase tracking-widest text-muted-foreground">What it is</p><p class="text-sm leading-relaxed text-foreground">{section.body}</p></div>
|
||||
<div class="space-y-3"><p class="text-[0.625rem] font-black uppercase tracking-widest text-muted-foreground">How to do it</p><ol class="space-y-2"><For each={section.steps}>{(step, index) => <li class="flex gap-3 text-sm text-muted-foreground leading-relaxed"><span class="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-sky-500/10 text-sky-700 text-[0.625rem] font-bold">{index() + 1}</span><span>{step}</span></li>}</For></ol></div>
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<div class="rounded-2xl border border-amber-300/40 bg-amber-500/[0.06] p-4 space-y-2"><p class="text-[0.625rem] font-black uppercase tracking-widest text-amber-700">What people usually forget</p><p class="text-sm text-muted-foreground leading-relaxed">{section.reminder}</p></div>
|
||||
<div class="rounded-2xl border border-rose-300/40 bg-rose-500/[0.06] p-4 space-y-2"><p class="text-[0.625rem] font-black uppercase tracking-widest text-rose-700">Watch for this</p><p class="text-sm text-muted-foreground leading-relaxed">{section.warning}</p></div>
|
||||
</div>
|
||||
<RelatedTopics items={section.related} onJump={jumpTo} />
|
||||
</div>
|
||||
</article>;
|
||||
}}</For>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="space-y-6 rounded-[2rem] border border-violet-200/40 bg-violet-500/[0.04] p-5 sm:p-6">
|
||||
<div class="space-y-2">
|
||||
<h2 class="text-xl font-bold border-b border-border/60 pb-2 flex items-center gap-2"><Link2 size={20} class="text-violet-700" />Deep Dive</h2>
|
||||
<p class="text-sm text-muted-foreground leading-relaxed">This layer is optional enrichment for users who want the mental model behind the app.</p>
|
||||
</div>
|
||||
<div class="space-y-5">
|
||||
<For each={deepSections}>{(section) => {
|
||||
const Icon = section.icon;
|
||||
return <article id={`help-${section.id}`} class="rounded-3xl border border-border/50 bg-card/80 shadow-sm overflow-hidden">
|
||||
<div class="p-5 sm:p-6 border-b border-border/40 bg-gradient-to-r from-violet-500/[0.08] via-violet-500/[0.03] to-transparent">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="hidden sm:flex h-11 w-11 shrink-0 items-center justify-center rounded-2xl bg-violet-500/10 text-violet-700"><Icon size={20} /></div>
|
||||
<div class="space-y-2 min-w-0"><h3 class="text-lg sm:text-xl font-black tracking-tight flex items-center gap-2"><Icon size={18} class="sm:hidden text-violet-700" />{section.title}</h3><p class="text-sm text-muted-foreground leading-relaxed">{section.intro}</p></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-5 sm:p-6 space-y-5">
|
||||
<div class="space-y-3"><p class="text-[0.625rem] font-black uppercase tracking-widest text-muted-foreground">Learn more</p><ul class="space-y-2"><For each={section.bullets}>{(item) => <li class="flex gap-3 text-sm text-muted-foreground leading-relaxed"><span class="mt-[0.45rem] h-1.5 w-1.5 shrink-0 rounded-full bg-violet-600/70" /><span>{item}</span></li>}</For></ul></div>
|
||||
<div class="rounded-2xl border border-violet-300/40 bg-violet-500/[0.06] p-4 space-y-2"><p class="text-[0.625rem] font-black uppercase tracking-widest text-violet-700">Why it matters</p><p class="text-sm text-muted-foreground leading-relaxed">{section.why}</p></div>
|
||||
<RelatedTopics items={section.related} onJump={jumpTo} />
|
||||
</div>
|
||||
</article>;
|
||||
}}</For>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="space-y-5">
|
||||
<h2 class="text-xl font-bold border-b border-border pb-2 flex items-center gap-2"><ShieldCheck size={20} class="text-primary" />Welcome & Access</h2>
|
||||
<p class="text-sm text-muted-foreground leading-relaxed">This section is mostly here for helping someone else get into TasGrid or for occasional reference later. It is intentionally placed near the end because most users opening Help already know how to get in.</p>
|
||||
<div class="grid gap-4 md:grid-cols-3">
|
||||
<For each={accessCards}>{(card) => (
|
||||
<div class="rounded-2xl border border-border/50 bg-card/50 p-4 space-y-2 shadow-sm">
|
||||
<p class="text-[0.625rem] font-black uppercase tracking-widest text-muted-foreground">{card.title}</p>
|
||||
<Show when={card.href} fallback={<p class="text-sm font-bold tracking-tight">{card.content}</p>}>
|
||||
<a href={card.href} target="_blank" class="text-sm font-bold tracking-tight text-primary flex items-center gap-1 hover:underline">{card.content}<ExternalLink size={12} /></a>
|
||||
</Show>
|
||||
<p class="text-sm text-muted-foreground leading-relaxed">{card.detail}</p>
|
||||
</div>
|
||||
)}</For>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer class="pt-10 border-t border-border flex flex-col items-center text-center space-y-4">
|
||||
<p class="text-sm text-muted-foreground max-w-2xl leading-relaxed">If the app feels out of sync, a quick <button onClick={() => location.reload()} class="text-primary font-bold hover:underline cursor-pointer bg-transparent border-none p-0 inline-flex items-center gap-1">refresh<RefreshCcw size={12} /></button> is still the best first step. Most changes sync automatically in real time.</p>
|
||||
<p class="text-sm font-medium">If you still need help after that, contact Timothy Cardoza or IT.</p>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -292,6 +292,10 @@ export const NotepadView: Component<{
|
||||
window.dispatchEvent(new KeyboardEvent('keydown', { metaKey: true, key: 'k' }));
|
||||
};
|
||||
|
||||
const openHelp = (sectionId?: string) => {
|
||||
(window as any).openTasGridHelp?.(sectionId);
|
||||
};
|
||||
|
||||
const handleDragStart = () => {
|
||||
setIsDragging(true);
|
||||
};
|
||||
@@ -696,6 +700,18 @@ export const NotepadView: Component<{
|
||||
</div>
|
||||
<Show when={isLinkedTasksOpen()}>
|
||||
<div class="flex items-center gap-1">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
class="h-7 px-2 text-[0.625rem] font-bold uppercase tracking-wider text-primary hover:bg-primary/10 rounded-lg"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openHelp("notes");
|
||||
}}
|
||||
title="Open note linking help"
|
||||
>
|
||||
Help
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
|
||||
@@ -62,6 +62,21 @@ export const SettingsView: Component<{ setView?: (v: string) => void }> = (props
|
||||
.filter(context => !context.deletedAt && context.kind === "user")
|
||||
.sort((a, b) => a.displayName.localeCompare(b.displayName));
|
||||
|
||||
const openHelp = (sectionId?: string) => {
|
||||
if ((window as any).openTasGridHelp) {
|
||||
(window as any).openTasGridHelp(sectionId);
|
||||
return;
|
||||
}
|
||||
if (sectionId) {
|
||||
window.location.hash = `#help/${sectionId}`;
|
||||
}
|
||||
if (props.setView) {
|
||||
props.setView("help");
|
||||
} else {
|
||||
(window as any).setCurrentView?.("help");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="w-full max-w-2xl mx-auto py-4 sm:py-10 px-4 sm:px-0 space-y-8 sm:space-y-12 animate-in fade-in slide-in-from-bottom-2 duration-500 min-w-0 overflow-hidden text-balance">
|
||||
<header class="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
@@ -101,7 +116,7 @@ export const SettingsView: Component<{ setView?: (v: string) => void }> = (props
|
||||
<HelpCircle size={20} />
|
||||
</div>
|
||||
<div class="text-left">
|
||||
<h3 class="text-sm font-bold">User Help Guide</h3>
|
||||
<h3 class="text-sm font-bold">Help</h3>
|
||||
<p class="text-[0.625rem] text-muted-foreground">Learn how to use TasGrid effectively.</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -125,6 +140,15 @@ export const SettingsView: Component<{ setView?: (v: string) => void }> = (props
|
||||
Tag Sharing
|
||||
</h3>
|
||||
<p class="text-sm text-muted-foreground">Share through tags: `@people` to collaborate, `@buckets` to move tasks to buckets, and `#notes` to link tasks to notes.</p>
|
||||
<button
|
||||
class="text-[0.6875rem] font-semibold text-primary hover:underline"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openHelp("contexts-sharing");
|
||||
}}
|
||||
>
|
||||
Need help with sharing and contexts?
|
||||
</button>
|
||||
</div>
|
||||
<div class="w-8 h-8 rounded-full flex items-center justify-center bg-muted/50 group-hover:bg-muted transition-colors">
|
||||
{isSharingOpen() ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
|
||||
@@ -512,6 +536,15 @@ export const SettingsView: Component<{ setView?: (v: string) => void }> = (props
|
||||
Shared Buckets
|
||||
</h3>
|
||||
<p class="text-sm text-muted-foreground">Buckets are shared spaces where tasks can be moved and collaborated on.</p>
|
||||
<button
|
||||
class="text-[0.6875rem] font-semibold text-primary hover:underline"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openHelp("buckets-supervision");
|
||||
}}
|
||||
>
|
||||
Need help with buckets and the context switcher?
|
||||
</button>
|
||||
</div>
|
||||
<div class="w-8 h-8 rounded-full flex items-center justify-center bg-muted/50 group-hover:bg-muted transition-colors">
|
||||
{isBucketsOpen() ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
|
||||
@@ -947,6 +980,15 @@ export const SettingsView: Component<{ setView?: (v: string) => void }> = (props
|
||||
Trash
|
||||
</h3>
|
||||
<p class="text-sm text-muted-foreground truncate">Expired trash is permanently deleted when the app loads.</p>
|
||||
<button
|
||||
class="text-[0.6875rem] font-semibold text-primary hover:underline"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openHelp("recovery-trash");
|
||||
}}
|
||||
>
|
||||
Need help recovering something?
|
||||
</button>
|
||||
</div>
|
||||
<div class="w-8 h-8 rounded-full flex items-center justify-center bg-muted/50 group-hover:bg-muted transition-colors">
|
||||
{isTrashOpen() ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
|
||||
|
||||
Reference in New Issue
Block a user