From e45bc9e2b64cb5883b70274b45ff671411befe8b Mon Sep 17 00:00:00 2001 From: Timothy Cardoza Date: Wed, 25 Mar 2026 19:37:30 -0500 Subject: [PATCH] tips working and optimized --- docs/HELP_GUIDE.md | 86 ++++++++++++---- src/App.tsx | 25 ++--- src/lib/ai-help.ts | 215 ++++++++++++++++++++++++++++++++++----- src/store/preferences.ts | 12 +-- src/store/state.ts | 8 +- 5 files changed, 278 insertions(+), 68 deletions(-) diff --git a/docs/HELP_GUIDE.md b/docs/HELP_GUIDE.md index a5e6e8c..00e58c2 100644 --- a/docs/HELP_GUIDE.md +++ b/docs/HELP_GUIDE.md @@ -2,7 +2,7 @@ This document is a thorough reference for how TasGrid works today. It is intended to support future help tooling, deeper troubleshooting, and advanced feature explanations. It is not meant to match the shorter in-app Help screen one-to-one. -## Purpose and Scope +## Overview - This guide focuses on end-user and team-lead features inside TasGrid. - It explains both normal workflows and important behavior that may confuse users if it is not documented. @@ -20,6 +20,9 @@ This document is a thorough reference for how TasGrid works today. It is intende - `@bucket` for a shared bucket context - `#note` for note-linked workspaces - These special tags are not just labels. TasGrid converts them into structured relationships that affect visibility, ownership, and workflow behavior. +- For tasks, the important persisted relationship fields are `labelTags`, `shareRefs`, and `noteRefs`. +- The user-facing tag list is effectively a derived view built back from those structured relationships, plus the user's hidden favorite tag when applicable. +- This means editing tags is not a purely decorative string change. TasGrid reinterprets the tag list and normalizes it back into structured refs. ## Navigation and Access @@ -40,21 +43,33 @@ This document is a thorough reference for how TasGrid works today. It is intende - `Snowball` - `Dig In` - `Progress` -- The bottom of the sidebar includes `Help`, `Settings`, and the context switcher. +- The context switcher sits near the top of the sidebar above the task views. +- The lower utility area includes: + - `Ask Questions` + - `Help` + - `Settings` - The sidebar supports locked, collapsed, and peek states. ### Mobile Navigation - The mobile bottom nav exposes: - `Focus` - - `Matrix` - grouped `Value` views for `Priority` and `Urgency` + - `Matrix` inside the `Value` group - grouped `Flow` views for `Snowball`, `Dig In`, and `Progress` + - `Ask` - `Settings` - Mobile does not have a dedicated Help tab in bottom navigation. - Help is reached through `Settings > Resources > Help`. - The mobile context switcher is a floating control near the top-left area of the screen. +### Ask Questions Access + +- `Ask Questions` is a built-in question panel for looking up how TasGrid works. +- On desktop it opens inline inside the sidebar so it can be used alongside the rest of the app. +- On mobile it is a dedicated bottom-nav destination labeled `Ask`. +- The current implementation is guide-grounded rather than general-purpose: answers are intended to come from the TasGrid reference material rather than from outside product knowledge. + ### Help Access - Desktop Help is available from the left sidebar. @@ -62,7 +77,7 @@ This document is a thorough reference for how TasGrid works today. It is intende - Some higher-confusion areas include direct Help links that open the relevant help section. - Help supports deep-link anchors in the form `#help/`. -## Tasks +## Task Creation and Editing ### Task Creation @@ -116,6 +131,7 @@ This document is a thorough reference for how TasGrid works today. It is intende - rich description/content - Task detail auto-saves changes. - Some saves are debounced rather than written instantly. +- Tag edits are interpreted into `labelTags`, `shareRefs`, and `noteRefs`, then written back as a normalized task state. - If a user inserts an `@person` mention into task content, TasGrid also adds that matching `@person` context tag so the task participates in sharing correctly. ### Task Card Actions @@ -131,6 +147,8 @@ This document is a thorough reference for how TasGrid works today. It is intende - Some collaboratively shared tasks show update indicators. - There is also a `Collapse until updated` behavior for reducing clutter on ongoing shared work. +## Task Lifecycle and Scheduling + ### Task Status and Progress - Task status is numeric from `0` to `10`. @@ -307,7 +325,7 @@ These behaviors are important because a user may feel a task has disappeared whe - This is especially relevant for search and filters because a task cannot be found if it has not been loaded into memory yet. - Users may interpret missing search results as deletion when the actual issue is partial history loading. -## Contexts, Sharing, Buckets, and Supervision +## Contexts and Sharing ### Context Types @@ -331,9 +349,11 @@ These behaviors are important because a user may feel a task has disappeared whe - `My Bucket` - pinned shared buckets - supervised personal contexts shared with the current user -- Desktop places this at the bottom of the sidebar. +- Desktop places this near the top of the sidebar above the task views. - Mobile shows it through the floating control. +## Buckets and Supervision + ### Shared Buckets - Shared buckets are team contexts created in Settings. @@ -365,8 +385,22 @@ These behaviors are important because a user may feel a task has disappeared whe - an `@bucket` tag - a `#note` tag - This is the primary sharing model. +- TasGrid parses those tags into structured `shareRefs` and `noteRefs` rather than relying on the display strings alone. - Documentation should not describe a separate manual share dialog as the main expected workflow. +### `#note` Sharing Semantics + +- Applying a `#note` tag does more than add a label: + - it creates a `noteRef` + - it also creates a note-kind `shareRef` +- A `#note` tag links the task into that note workspace. It does **not** mean "all teammates can now see this task." +- Visibility from `#note` linkage is governed by note accessibility: + - if the note is private, only the note owner can gain visibility from that note link alone + - if the note is public, other authenticated users can gain visibility from the note link because the note is accessible to them +- For explicit person-to-person or team sharing, use `@person` and `@bucket`. +- If the goal is "make sure another specific teammate can see this task," `#note` alone is not the clearest or safest mechanism unless that teammate already has access to the note. +- In implementation terms, note-linked visibility is checked through accessible `noteRefs`, while user and bucket sharing are checked through personal-context and bucket-context rules. + ### Collaborative vs Handoff - TasGrid uses two important context policies: @@ -391,6 +425,7 @@ These behaviors are important because a user may feel a task has disappeared whe - Some system-backed context tags are intentionally hidden in normal UI. - Hidden does not mean inactive. - A task may still belong to a person, bucket, or note even when the user does not obviously see the matching context tag in normal task chips. +- In particular, the current user's own personal-context tag can be suppressed from visible task chips even though the underlying ref is still active. - This is one of the biggest sources of user confusion and should be treated as normal behavior rather than as data loss. ### Context Effects on Task Creation and Visibility @@ -404,7 +439,7 @@ These behaviors are important because a user may feel a task has disappeared whe - trash - whether the task belongs to an unpinned bucket -## Notes +## Notes Basics ### Notes Mode @@ -466,6 +501,8 @@ These behaviors are important because a user may feel a task has disappeared whe - matching `noteRefs` - matching note-based `shareRefs` +## Note Sharing and Linked Work + ### Public, Private, and Share Links - Notes can be public or private. @@ -478,6 +515,8 @@ These behaviors are important because a user may feel a task has disappeared whe - If a note is private when the share link is copied, TasGrid prompts the owner to make it public first. - Public notes can be viewed through the embed route without normal auth when the note ID is present. - Public notes are editable in-app by non-owners, but owner-only controls remain restricted. +- A public note can also make note-linked tasks visible inside the authenticated app because those tasks pass the note-access visibility check. +- A public embed link should be treated as note-content access, not as a guaranteed full unauthenticated task workspace. Linked-task loading still depends on the in-app task-loading path. ### Child Tabs @@ -508,6 +547,7 @@ Important privacy caveat: - A task counts as linked if either is true: - it has a matching `noteRef` - it has the matching `#note` tag +- In practice, TasGrid treats the root note as the linking owner for that workspace, even when the user is currently inside a child tab. - Linked tasks are shown with incomplete items first, then ranked by combined score. Linked task actions include: @@ -537,7 +577,7 @@ Current behavior caveats: - TasGrid also renames the corresponding `#note` tag definition after the note rename. - This helps linked tasks continue to point to the correct note workspace after the name changes. -## Rich Editor +## Rich Editor Basics ### Shared Editor Model @@ -618,6 +658,8 @@ Current caveat: - in task detail, `Open Note` is wired to actual note navigation - in the notes editor, the preview appears but `Open Note` is not fully wired through the Notes view +## Rich Editor Uploads and Embeds + ### Uploads and Embedded Media - Upload flows are available through: @@ -806,9 +848,11 @@ Background update behavior: - Like many PWAs, TasGrid can continue showing older cached assets until a service worker update is applied. - `Update App` and `Reset PWA` exist to help recover from stale-client issues. -## Important Constraints and User-Facing Caveats +## Common Confusions and Caveats - Sharing is primarily tag-driven, not driven by a separate share dialog. +- `Ask Questions` is help-oriented and guide-grounded; it should not be treated as an authoritative source for behaviors that are not documented here. +- Sharing a task to `#note` does not automatically mean every teammate can see it. It means the task is linked to that note workspace, and visibility then depends on whether the note itself is accessible. - Context tags may be hidden while still actively controlling task behavior. - Creating work in the wrong active context can place it somewhere unexpected. - Focus and Priority are not substantially different in current sorting behavior. @@ -826,7 +870,9 @@ Background update behavior: - Bucket archive exists, but a confirmed user-facing bucket restore flow was not identified. - In the notes editor, note mention previews exist, but `Open Note` from that preview is not fully wired through the same way it is in task detail. -## Explicit Exclusions +## Reference Notes + +### Explicit Exclusions - SDK and API documentation - integration-developer workflows @@ -835,6 +881,16 @@ Background update behavior: - admin-only repair or maintenance flows - inactive legacy settings UI hidden behind disabled conditions +### Summary for Future Help-Agent Use + +- The biggest mental models to preserve are: + - contexts are first-class behavior, not decorative tags + - notes are workspaces, not just text documents + - due date and urgency are linked + - hidden tags can still matter + - several views are ranking lenses over the same work pool + - retrieval issues are often context, filtering, or loading issues rather than actual data loss + ## Troubleshooting Checklist When a user thinks something is missing or not working, check these in order: @@ -847,13 +903,3 @@ When a user thinks something is missing or not working, check these in order: 6. Check whether the task belongs to an unpinned bucket or hidden system context. 7. For stale UI behavior, try `Update App`. 8. For persistent cache issues, use `Reset PWA`. - -## Summary for Future Help-Agent Use - -- The biggest mental models to preserve are: - - contexts are first-class behavior, not decorative tags - - notes are workspaces, not just text documents - - due date and urgency are linked - - hidden tags can still matter - - several views are ranking lenses over the same work pool - - retrieval issues are often context, filtering, or loading issues rather than actual data loss diff --git a/src/App.tsx b/src/App.tsx index 73fb283..881421d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,7 +5,7 @@ import { EmbedAuthWrapper } from './components/EmbedAuthWrapper'; import { CriticalView } from './views/CriticalView'; import { AuthCallback } from './components/Auth'; import { pb } from './lib/pocketbase'; -import { initStore, setStore, store, appendAIHelpTipHistory } from './store'; +import { initStore, setStore, store, advanceAIHelpTipSectionIndex } from './store'; import { generateAIHelpTip, hasFireworksApiKey } from './lib/ai-help'; // const CriticalView = lazy(() => import('./views/CriticalView').then(m => ({ default: m.CriticalView }))); @@ -45,19 +45,23 @@ const App: Component = () => { createEffect(() => { const userId = authUserId(); - const ready = isAuthenticated() && !!userId && isDesktop() && !store.isInitializing; - if (!ready) return; - if (!hasFireworksApiKey()) return; - if (tipGeneratedForUserId() === userId) return; + const authenticated = isAuthenticated(); + const desktop = isDesktop(); + const initializing = store.isInitializing; + const hasApiKey = hasFireworksApiKey(); + const alreadyGeneratedForUser = tipGeneratedForUserId() === userId; + const prefsLoadedForUser = !!userId && store.prefId === userId; + + if (!authenticated || !userId || !desktop || initializing || !prefsLoadedForUser || !hasApiKey || alreadyGeneratedForUser) return; setTipGeneratedForUserId(userId); - const historySnapshot = untrack(() => [...store.aiHelpTipHistory]); + const sectionIndex = untrack(() => store.aiHelpTipSectionIndex); void (async () => { try { - const nextTip = await generateAIHelpTip(historySnapshot); + const nextTip = await generateAIHelpTip(sectionIndex); if (!nextTip) return; setAiHelpTip(nextTip); - await appendAIHelpTipHistory(nextTip); + await advanceAIHelpTipSectionIndex(); } catch (err) { console.warn("Tip generation skipped", err); } @@ -162,9 +166,7 @@ const App: Component = () => { const url = new URL(loc); const path = url.pathname; const hash = url.hash; - const res = path.startsWith('/embed/') || hash.startsWith('#/embed/'); - console.log('[DEBUG] isEmbed check:', { res, path, hash, href: loc }); - return res; + return path.startsWith('/embed/') || hash.startsWith('#/embed/'); }; const getEmbedView = () => { @@ -174,7 +176,6 @@ const App: Component = () => { let view = null; if (fullPath.includes('/notes')) view = 'embed_notes'; else if (fullPath.includes('/quick-add')) view = 'embed_quick_add'; - console.log('[DEBUG] getEmbedView detected:', view, 'Full URL context:', fullPath); return view; }; diff --git a/src/lib/ai-help.ts b/src/lib/ai-help.ts index e093f59..1cea872 100644 --- a/src/lib/ai-help.ts +++ b/src/lib/ai-help.ts @@ -19,9 +19,23 @@ When helpful, mention the relevant section name from the guide. TASGRID REFERENCE GUIDE ${helpGuide}`; +const buildSectionPrompt = (sectionTitle: string, sectionContent: string) => `You answer questions about TasGrid using only the guide section 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 section does not contain the answer, say that the guide section does not mention it. +Do not use outside product knowledge. +Do not invent buttons, workflows, settings, or behaviors. +Prefer concise, practical answers. + +TASGRID GUIDE SECTION +## ${sectionTitle} +${sectionContent}`; + const normalizeLine = (value: string) => value .trim() + .replace(/^tip:\s*/i, "") .replace(/^["'\s]+|["'\s]+$/g, "") .replace(/\s+/g, " ") .toLowerCase(); @@ -32,8 +46,97 @@ const keepSingleSentence = (value: string) => { return (sentenceMatch?.[0] || trimmed).trim(); }; +const isReasoningLikeType = (value: unknown) => + typeof value === "string" && /reasoning|thought/i.test(value); + export const hasFireworksApiKey = () => FIREWORKS_API_KEY.trim().length > 0; +const extractGuideSections = (markdown: string) => { + const lines = markdown.split(/\r?\n/); + const sections: Array<{ title: string; content: string }> = []; + const excludedTopLevelTitles = new Set([ + "Overview", + "Reference Notes", + "Troubleshooting Checklist" + ]); + const excludedSubsectionTitles = new Set([ + "Explicit Exclusions", + "Summary for Future Help-Agent Use", + "Help Access", + "Main App Modes", + "Account Header" + ]); + let currentTopLevelTitle: string | null = null; + let currentSection: { title: string; lines: string[] } | null = null; + + const pushCurrentSection = () => { + if (!currentSection) return; + const content = currentSection.lines.join("\n").trim(); + if (!content) return; + if (excludedSubsectionTitles.has(currentSection.title)) return; + sections.push({ + title: currentSection.title, + content + }); + }; + + for (const line of lines) { + const topLevelHeadingMatch = line.match(/^##\s+(.+?)\s*$/); + if (topLevelHeadingMatch) { + pushCurrentSection(); + currentTopLevelTitle = topLevelHeadingMatch[1].trim(); + currentSection = null; + continue; + } + + const subsectionHeadingMatch = line.match(/^###\s+(.+?)\s*$/); + if (subsectionHeadingMatch) { + pushCurrentSection(); + if (!currentTopLevelTitle || excludedTopLevelTitles.has(currentTopLevelTitle)) { + currentSection = null; + continue; + } + currentSection = { + title: `${currentTopLevelTitle}: ${subsectionHeadingMatch[1].trim()}`, + lines: [] + }; + continue; + } + + if (currentSection) { + currentSection.lines.push(line); + } + } + + pushCurrentSection(); + + return sections; +}; + +const HELP_GUIDE_SECTIONS = extractGuideSections(helpGuide); + +export const getAIHelpTipSectionDebug = (sectionIndex: number) => { + if (HELP_GUIDE_SECTIONS.length === 0) { + return { requestedIndex: sectionIndex, resolvedIndex: -1, title: null as string | null }; + } + + const resolvedIndex = ((sectionIndex % HELP_GUIDE_SECTIONS.length) + HELP_GUIDE_SECTIONS.length) % HELP_GUIDE_SECTIONS.length; + return { + requestedIndex: sectionIndex, + resolvedIndex, + title: HELP_GUIDE_SECTIONS[resolvedIndex]?.title || null + }; +}; + +const cleanRawCompletion = (value: string) => + value + .replace(/[\s\S]*?<\/think>/gi, " ") + .replace(/[\s\S]*?<\/reasoning>/gi, " ") + .replace(/<\|[^>]+?\|>/g, " ") + .replace(/<\/?s>/gi, " ") + .replace(/\s+/g, " ") + .trim(); + const extractTextFromContent = (content: unknown): string => { if (typeof content === "string") { return content.trim(); @@ -44,6 +147,9 @@ const extractTextFromContent = (content: unknown): string => { .map(part => { if (typeof part === "string") return part; if (part && typeof part === "object") { + if (isReasoningLikeType((part as any).type)) { + return ""; + } if ("text" in part && typeof (part as any).text === "string") { return (part as any).text; } @@ -58,10 +164,41 @@ const extractTextFromContent = (content: unknown): string => { .trim(); } + if (content && typeof content === "object") { + if (isReasoningLikeType((content as any).type)) { + return ""; + } + if ("text" in content && typeof (content as any).text === "string") { + return (content as any).text.trim(); + } + if ("content" in content) { + return extractTextFromContent((content as any).content); + } + } + return ""; }; -const requestFireworks = async (messages: ChatRequestMessage[], options?: { maxTokens?: number; temperature?: number }) => { +const extractTextFromOutputArray = (output: unknown): string => { + if (!Array.isArray(output)) return ""; + + return output + .map(item => { + if (!item || typeof item !== "object") return ""; + if ("content" in item) { + return extractTextFromContent((item as any).content); + } + if ("text" in item && typeof (item as any).text === "string") { + return (item as any).text; + } + return ""; + }) + .filter(Boolean) + .join("\n") + .trim(); +}; + +const requestFireworks = async (messages: ChatRequestMessage[], options?: { maxTokens?: number; temperature?: number; reasoningEffort?: "none" | "low" | "medium" | "high" }) => { const response = await fetch(FIREWORKS_API_URL, { method: "POST", headers: { @@ -70,9 +207,10 @@ const requestFireworks = async (messages: ChatRequestMessage[], options?: { maxT }, body: JSON.stringify({ model: FIREWORKS_HELP_MODEL, - reasoning_effort: "low", + reasoning_effort: options?.reasoningEffort ?? "low", temperature: options?.temperature ?? 0.2, max_tokens: options?.maxTokens ?? 700, + raw_output: true, messages }) }); @@ -85,16 +223,41 @@ const requestFireworks = async (messages: ChatRequestMessage[], options?: { maxT const data = await response.json(); const choice = data?.choices?.[0]; const content = extractTextFromContent(choice?.message?.content) - || extractTextFromContent(choice?.message?.reasoning_content) || extractTextFromContent(choice?.delta?.content) || extractTextFromContent(choice?.text) || extractTextFromContent(data?.output_text); + const rawCompletion = cleanRawCompletion( + extractTextFromContent(choice?.raw_output?.completion) + || extractTextFromContent(data?.raw_output?.completion) + ); + const outputArrayText = extractTextFromOutputArray(data?.output); + const answer = content || outputArrayText || rawCompletion; - if (!content) { - throw new Error("The help assistant did not return any text."); + if (!answer) { + const finishReason = choice?.finish_reason ? ` finish_reason=${String(choice.finish_reason)}` : ""; + throw new Error(`The help assistant did not return any text.${finishReason}`); } - return content; + return answer; +}; + +const looksLikePromptEcho = (value: string) => { + const normalized = normalizeLine(value); + return [ + "we need to produce", + "create one short tasgrid desktop tip", + "maximum one sentence", + "return plain text only", + "do not repeat or closely paraphrase", + "previous tips to avoid", + "use only this guide", + "the guide does not mention", + "guide section does not mention", + "section does not mention", + "specific desktop tip", + "tasgrid desktop tip", + "based only on the guide" + ].some(fragment => normalized.includes(fragment)); }; export const askAIHelp = async (messages: Array<{ role: "user" | "assistant"; content: string }>) => @@ -103,37 +266,43 @@ export const askAIHelp = async (messages: Array<{ role: "user" | "assistant"; co { role: "system", content: HELP_SYSTEM_PROMPT }, ...messages ], - { maxTokens: 700, temperature: 0.2 } + { maxTokens: 700, temperature: 0.2, reasoningEffort: "low" } ); -export const generateAIHelpTip = async (history: string[]) => { - const recentHistory = history.slice(-10); - const historyBlock = recentHistory.length > 0 - ? recentHistory.map((tip, index) => `${index + 1}. ${tip}`).join("\n") - : "No previous tips."; +export const generateAIHelpTip = async (sectionIndex: number) => { + const sectionDebug = getAIHelpTipSectionDebug(sectionIndex); + const section = sectionDebug.resolvedIndex >= 0 + ? HELP_GUIDE_SECTIONS[sectionDebug.resolvedIndex] + : null; - const tipPrompt = `Create one short TasGrid desktop tip based only on the guide. + const sectionLabel = section?.title || "TasGrid"; + const tipPrompt = `Write one short practical TasGrid tip for a person already using the app. Rules: - Maximum one sentence. - Return plain text only. - No title, no bullet, no quotes. - Keep it subtle and practical. -- Do not repeat or closely paraphrase any previous tip. +- State a real TasGrid behavior or workflow the user can use. +- Do not talk about the guide, the section, the prompt, or what is not mentioned. +- Do not explain your limitations. - Use only this guide for all information and never assume you know something that isn't mentioned in the guide. - -Previous tips to avoid: -${historyBlock}`; - - const seen = new Set(recentHistory.map(normalizeLine)); +- Start the sentence with "Tip: ". +- Base the tip on this topic: ${sectionLabel}. +`; for (let attempt = 0; attempt < 2; attempt += 1) { const response = await requestFireworks( [ - { role: "system", content: HELP_SYSTEM_PROMPT }, + { + role: "system", + content: section + ? buildSectionPrompt(section.title, section.content) + : HELP_SYSTEM_PROMPT + }, { role: "user", content: tipPrompt } ], - { maxTokens: 80, temperature: 0.5 } + { maxTokens: 192, temperature: 0.5, reasoningEffort: "low" } ); const firstLine = response.split("\n")[0]?.trim() || ""; @@ -145,9 +314,9 @@ ${historyBlock}`; const normalized = normalizeLine(cleaned); if (!cleaned || !normalized) continue; - if (seen.has(normalized)) continue; + if (looksLikePromptEcho(cleaned)) continue; - return cleaned; + return cleaned.startsWith("Tip:") ? cleaned : `Tip: ${cleaned.replace(/^tip:\s*/i, "")}`; } return null; diff --git a/src/store/preferences.ts b/src/store/preferences.ts index 5026075..4cfce8b 100644 --- a/src/store/preferences.ts +++ b/src/store/preferences.ts @@ -28,7 +28,7 @@ export const syncPreferences = async () => { collapsedUntilUpdatedTasks: store.collapsedUntilUpdatedTasks, subscribedBuckets: store.subscribedBuckets, supervisorUserIds: store.personalContextSupervisorIds, - aiHelpTipHistory: store.aiHelpTipHistory, + aiHelpTipSectionIndex: store.aiHelpTipSectionIndex, lastCompletedTaskCleanupDate: store.lastCompletedTaskCleanupDate }; @@ -176,14 +176,8 @@ export const clearNoteFilter = () => { setStore("noteFilter", createDefaultFilter()); }; -export const appendAIHelpTipHistory = async (tip: string) => { - const normalizedTip = tip.trim(); - if (!normalizedTip) return; - - setStore("aiHelpTipHistory", prev => { - const next = [...prev.filter(existing => existing !== normalizedTip), normalizedTip]; - return next.slice(-10); - }); +export const advanceAIHelpTipSectionIndex = async () => { + setStore("aiHelpTipSectionIndex", prev => prev + 1); await syncPreferences(); }; diff --git a/src/store/state.ts b/src/store/state.ts index d6c57e6..ecdaca9 100644 --- a/src/store/state.ts +++ b/src/store/state.ts @@ -183,7 +183,7 @@ export interface TaskStore { buckets: Bucket[]; subscribedBuckets: string[]; personalContextSupervisorIds: string[]; - aiHelpTipHistory: string[]; + aiHelpTipSectionIndex: number; notes: Note[]; isNotepadMode: boolean; quickloadTasks: string[]; @@ -203,7 +203,7 @@ export interface ScopedTaskgridPrefs { noteFilter?: Filter; subscribedBuckets?: string[]; supervisorUserIds?: string[]; - aiHelpTipHistory?: string[]; + aiHelpTipSectionIndex?: number; dismissedTaskUpdateIndicators?: Record; collapsedUntilUpdatedTasks?: Record; migratedStructuredTagsV2?: boolean; @@ -248,7 +248,7 @@ export const [store, setStore] = createStore({ buckets: [], subscribedBuckets: [], personalContextSupervisorIds: [], - aiHelpTipHistory: [], + aiHelpTipSectionIndex: 0, notes: [], isNotepadMode: false, quickloadTasks: [], @@ -298,7 +298,7 @@ export const applyScopedPrefsToStore = (prefs: ScopedTaskgridPrefs) => { prefId: currentUserId, subscribedBuckets: prefs.subscribedBuckets || [], personalContextSupervisorIds: personalContext?.supervisorUserIds || prefs.supervisorUserIds || [], - aiHelpTipHistory: prefs.aiHelpTipHistory || [], + aiHelpTipSectionIndex: typeof prefs.aiHelpTipSectionIndex === "number" ? prefs.aiHelpTipSectionIndex : 0, tagDefinitions: (prefs.tagDefinitions || []).filter((def: TagDefinition) => !def.name.startsWith("@")), filterTemplates: prefs.filterTemplates || [], quickloadTasks: prefs.quickloadTasks || [],