tips working and optimized
This commit is contained in:
+66
-20
@@ -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.
|
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.
|
- 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.
|
- 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
|
- `@bucket` for a shared bucket context
|
||||||
- `#note` for note-linked workspaces
|
- `#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.
|
- 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
|
## Navigation and Access
|
||||||
|
|
||||||
@@ -40,21 +43,33 @@ This document is a thorough reference for how TasGrid works today. It is intende
|
|||||||
- `Snowball`
|
- `Snowball`
|
||||||
- `Dig In`
|
- `Dig In`
|
||||||
- `Progress`
|
- `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.
|
- The sidebar supports locked, collapsed, and peek states.
|
||||||
|
|
||||||
### Mobile Navigation
|
### Mobile Navigation
|
||||||
|
|
||||||
- The mobile bottom nav exposes:
|
- The mobile bottom nav exposes:
|
||||||
- `Focus`
|
- `Focus`
|
||||||
- `Matrix`
|
|
||||||
- grouped `Value` views for `Priority` and `Urgency`
|
- grouped `Value` views for `Priority` and `Urgency`
|
||||||
|
- `Matrix` inside the `Value` group
|
||||||
- grouped `Flow` views for `Snowball`, `Dig In`, and `Progress`
|
- grouped `Flow` views for `Snowball`, `Dig In`, and `Progress`
|
||||||
|
- `Ask`
|
||||||
- `Settings`
|
- `Settings`
|
||||||
- Mobile does not have a dedicated Help tab in bottom navigation.
|
- Mobile does not have a dedicated Help tab in bottom navigation.
|
||||||
- Help is reached through `Settings > Resources > Help`.
|
- Help is reached through `Settings > Resources > Help`.
|
||||||
- The mobile context switcher is a floating control near the top-left area of the screen.
|
- 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
|
### Help Access
|
||||||
|
|
||||||
- Desktop Help is available from the left sidebar.
|
- 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.
|
- Some higher-confusion areas include direct Help links that open the relevant help section.
|
||||||
- Help supports deep-link anchors in the form `#help/<section-id>`.
|
- Help supports deep-link anchors in the form `#help/<section-id>`.
|
||||||
|
|
||||||
## Tasks
|
## Task Creation and Editing
|
||||||
|
|
||||||
### Task Creation
|
### Task Creation
|
||||||
|
|
||||||
@@ -116,6 +131,7 @@ This document is a thorough reference for how TasGrid works today. It is intende
|
|||||||
- rich description/content
|
- rich description/content
|
||||||
- Task detail auto-saves changes.
|
- Task detail auto-saves changes.
|
||||||
- Some saves are debounced rather than written instantly.
|
- 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.
|
- 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
|
### 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.
|
- Some collaboratively shared tasks show update indicators.
|
||||||
- There is also a `Collapse until updated` behavior for reducing clutter on ongoing shared work.
|
- 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 and Progress
|
||||||
|
|
||||||
- Task status is numeric from `0` to `10`.
|
- 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.
|
- 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.
|
- 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
|
### Context Types
|
||||||
|
|
||||||
@@ -331,9 +349,11 @@ These behaviors are important because a user may feel a task has disappeared whe
|
|||||||
- `My Bucket`
|
- `My Bucket`
|
||||||
- pinned shared buckets
|
- pinned shared buckets
|
||||||
- supervised personal contexts shared with the current user
|
- 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.
|
- Mobile shows it through the floating control.
|
||||||
|
|
||||||
|
## Buckets and Supervision
|
||||||
|
|
||||||
### Shared Buckets
|
### Shared Buckets
|
||||||
|
|
||||||
- Shared buckets are team contexts created in Settings.
|
- 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
|
- an `@bucket` tag
|
||||||
- a `#note` tag
|
- a `#note` tag
|
||||||
- This is the primary sharing model.
|
- 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.
|
- 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
|
### Collaborative vs Handoff
|
||||||
|
|
||||||
- TasGrid uses two important context policies:
|
- 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.
|
- Some system-backed context tags are intentionally hidden in normal UI.
|
||||||
- Hidden does not mean inactive.
|
- 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.
|
- 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.
|
- 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
|
### 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
|
- trash
|
||||||
- whether the task belongs to an unpinned bucket
|
- whether the task belongs to an unpinned bucket
|
||||||
|
|
||||||
## Notes
|
## Notes Basics
|
||||||
|
|
||||||
### Notes Mode
|
### Notes Mode
|
||||||
|
|
||||||
@@ -466,6 +501,8 @@ These behaviors are important because a user may feel a task has disappeared whe
|
|||||||
- matching `noteRefs`
|
- matching `noteRefs`
|
||||||
- matching note-based `shareRefs`
|
- matching note-based `shareRefs`
|
||||||
|
|
||||||
|
## Note Sharing and Linked Work
|
||||||
|
|
||||||
### Public, Private, and Share Links
|
### Public, Private, and Share Links
|
||||||
|
|
||||||
- Notes can be public or private.
|
- 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.
|
- 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 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.
|
- 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
|
### Child Tabs
|
||||||
|
|
||||||
@@ -508,6 +547,7 @@ Important privacy caveat:
|
|||||||
- A task counts as linked if either is true:
|
- A task counts as linked if either is true:
|
||||||
- it has a matching `noteRef`
|
- it has a matching `noteRef`
|
||||||
- it has the matching `#note` tag
|
- 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 tasks are shown with incomplete items first, then ranked by combined score.
|
||||||
|
|
||||||
Linked task actions include:
|
Linked task actions include:
|
||||||
@@ -537,7 +577,7 @@ Current behavior caveats:
|
|||||||
- TasGrid also renames the corresponding `#note` tag definition after the note rename.
|
- 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.
|
- This helps linked tasks continue to point to the correct note workspace after the name changes.
|
||||||
|
|
||||||
## Rich Editor
|
## Rich Editor Basics
|
||||||
|
|
||||||
### Shared Editor Model
|
### Shared Editor Model
|
||||||
|
|
||||||
@@ -618,6 +658,8 @@ Current caveat:
|
|||||||
- in task detail, `Open Note` is wired to actual note navigation
|
- 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
|
- 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
|
### Uploads and Embedded Media
|
||||||
|
|
||||||
- Upload flows are available through:
|
- 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.
|
- 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.
|
- `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.
|
- 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.
|
- Context tags may be hidden while still actively controlling task behavior.
|
||||||
- Creating work in the wrong active context can place it somewhere unexpected.
|
- Creating work in the wrong active context can place it somewhere unexpected.
|
||||||
- Focus and Priority are not substantially different in current sorting behavior.
|
- 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.
|
- 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.
|
- 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
|
- SDK and API documentation
|
||||||
- integration-developer workflows
|
- integration-developer workflows
|
||||||
@@ -835,6 +881,16 @@ Background update behavior:
|
|||||||
- admin-only repair or maintenance flows
|
- admin-only repair or maintenance flows
|
||||||
- inactive legacy settings UI hidden behind disabled conditions
|
- 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
|
## Troubleshooting Checklist
|
||||||
|
|
||||||
When a user thinks something is missing or not working, check these in order:
|
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.
|
6. Check whether the task belongs to an unpinned bucket or hidden system context.
|
||||||
7. For stale UI behavior, try `Update App`.
|
7. For stale UI behavior, try `Update App`.
|
||||||
8. For persistent cache issues, use `Reset PWA`.
|
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
|
|
||||||
|
|||||||
+13
-12
@@ -5,7 +5,7 @@ import { EmbedAuthWrapper } from './components/EmbedAuthWrapper';
|
|||||||
import { CriticalView } from './views/CriticalView';
|
import { CriticalView } from './views/CriticalView';
|
||||||
import { AuthCallback } from './components/Auth';
|
import { AuthCallback } from './components/Auth';
|
||||||
import { pb } from './lib/pocketbase';
|
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';
|
import { generateAIHelpTip, hasFireworksApiKey } from './lib/ai-help';
|
||||||
|
|
||||||
// const CriticalView = lazy(() => import('./views/CriticalView').then(m => ({ default: m.CriticalView })));
|
// const CriticalView = lazy(() => import('./views/CriticalView').then(m => ({ default: m.CriticalView })));
|
||||||
@@ -45,19 +45,23 @@ const App: Component = () => {
|
|||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const userId = authUserId();
|
const userId = authUserId();
|
||||||
const ready = isAuthenticated() && !!userId && isDesktop() && !store.isInitializing;
|
const authenticated = isAuthenticated();
|
||||||
if (!ready) return;
|
const desktop = isDesktop();
|
||||||
if (!hasFireworksApiKey()) return;
|
const initializing = store.isInitializing;
|
||||||
if (tipGeneratedForUserId() === userId) return;
|
const hasApiKey = hasFireworksApiKey();
|
||||||
|
const alreadyGeneratedForUser = tipGeneratedForUserId() === userId;
|
||||||
|
const prefsLoadedForUser = !!userId && store.prefId === userId;
|
||||||
|
|
||||||
|
if (!authenticated || !userId || !desktop || initializing || !prefsLoadedForUser || !hasApiKey || alreadyGeneratedForUser) return;
|
||||||
|
|
||||||
setTipGeneratedForUserId(userId);
|
setTipGeneratedForUserId(userId);
|
||||||
const historySnapshot = untrack(() => [...store.aiHelpTipHistory]);
|
const sectionIndex = untrack(() => store.aiHelpTipSectionIndex);
|
||||||
void (async () => {
|
void (async () => {
|
||||||
try {
|
try {
|
||||||
const nextTip = await generateAIHelpTip(historySnapshot);
|
const nextTip = await generateAIHelpTip(sectionIndex);
|
||||||
if (!nextTip) return;
|
if (!nextTip) return;
|
||||||
setAiHelpTip(nextTip);
|
setAiHelpTip(nextTip);
|
||||||
await appendAIHelpTipHistory(nextTip);
|
await advanceAIHelpTipSectionIndex();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("Tip generation skipped", err);
|
console.warn("Tip generation skipped", err);
|
||||||
}
|
}
|
||||||
@@ -162,9 +166,7 @@ const App: Component = () => {
|
|||||||
const url = new URL(loc);
|
const url = new URL(loc);
|
||||||
const path = url.pathname;
|
const path = url.pathname;
|
||||||
const hash = url.hash;
|
const hash = url.hash;
|
||||||
const res = path.startsWith('/embed/') || hash.startsWith('#/embed/');
|
return path.startsWith('/embed/') || hash.startsWith('#/embed/');
|
||||||
console.log('[DEBUG] isEmbed check:', { res, path, hash, href: loc });
|
|
||||||
return res;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getEmbedView = () => {
|
const getEmbedView = () => {
|
||||||
@@ -174,7 +176,6 @@ const App: Component = () => {
|
|||||||
let view = null;
|
let view = null;
|
||||||
if (fullPath.includes('/notes')) view = 'embed_notes';
|
if (fullPath.includes('/notes')) view = 'embed_notes';
|
||||||
else if (fullPath.includes('/quick-add')) view = 'embed_quick_add';
|
else if (fullPath.includes('/quick-add')) view = 'embed_quick_add';
|
||||||
console.log('[DEBUG] getEmbedView detected:', view, 'Full URL context:', fullPath);
|
|
||||||
return view;
|
return view;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+192
-23
@@ -19,9 +19,23 @@ When helpful, mention the relevant section name from the guide.
|
|||||||
TASGRID REFERENCE GUIDE
|
TASGRID REFERENCE GUIDE
|
||||||
${helpGuide}`;
|
${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) =>
|
const normalizeLine = (value: string) =>
|
||||||
value
|
value
|
||||||
.trim()
|
.trim()
|
||||||
|
.replace(/^tip:\s*/i, "")
|
||||||
.replace(/^["'\s]+|["'\s]+$/g, "")
|
.replace(/^["'\s]+|["'\s]+$/g, "")
|
||||||
.replace(/\s+/g, " ")
|
.replace(/\s+/g, " ")
|
||||||
.toLowerCase();
|
.toLowerCase();
|
||||||
@@ -32,8 +46,97 @@ const keepSingleSentence = (value: string) => {
|
|||||||
return (sentenceMatch?.[0] || trimmed).trim();
|
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;
|
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(/<think>[\s\S]*?<\/think>/gi, " ")
|
||||||
|
.replace(/<reasoning>[\s\S]*?<\/reasoning>/gi, " ")
|
||||||
|
.replace(/<\|[^>]+?\|>/g, " ")
|
||||||
|
.replace(/<\/?s>/gi, " ")
|
||||||
|
.replace(/\s+/g, " ")
|
||||||
|
.trim();
|
||||||
|
|
||||||
const extractTextFromContent = (content: unknown): string => {
|
const extractTextFromContent = (content: unknown): string => {
|
||||||
if (typeof content === "string") {
|
if (typeof content === "string") {
|
||||||
return content.trim();
|
return content.trim();
|
||||||
@@ -44,6 +147,9 @@ const extractTextFromContent = (content: unknown): string => {
|
|||||||
.map(part => {
|
.map(part => {
|
||||||
if (typeof part === "string") return part;
|
if (typeof part === "string") return part;
|
||||||
if (part && typeof part === "object") {
|
if (part && typeof part === "object") {
|
||||||
|
if (isReasoningLikeType((part as any).type)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
if ("text" in part && typeof (part as any).text === "string") {
|
if ("text" in part && typeof (part as any).text === "string") {
|
||||||
return (part as any).text;
|
return (part as any).text;
|
||||||
}
|
}
|
||||||
@@ -58,10 +164,41 @@ const extractTextFromContent = (content: unknown): string => {
|
|||||||
.trim();
|
.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 "";
|
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, {
|
const response = await fetch(FIREWORKS_API_URL, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -70,9 +207,10 @@ const requestFireworks = async (messages: ChatRequestMessage[], options?: { maxT
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model: FIREWORKS_HELP_MODEL,
|
model: FIREWORKS_HELP_MODEL,
|
||||||
reasoning_effort: "low",
|
reasoning_effort: options?.reasoningEffort ?? "low",
|
||||||
temperature: options?.temperature ?? 0.2,
|
temperature: options?.temperature ?? 0.2,
|
||||||
max_tokens: options?.maxTokens ?? 700,
|
max_tokens: options?.maxTokens ?? 700,
|
||||||
|
raw_output: true,
|
||||||
messages
|
messages
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
@@ -85,16 +223,41 @@ const requestFireworks = async (messages: ChatRequestMessage[], options?: { maxT
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const choice = data?.choices?.[0];
|
const choice = data?.choices?.[0];
|
||||||
const content = extractTextFromContent(choice?.message?.content)
|
const content = extractTextFromContent(choice?.message?.content)
|
||||||
|| extractTextFromContent(choice?.message?.reasoning_content)
|
|
||||||
|| extractTextFromContent(choice?.delta?.content)
|
|| extractTextFromContent(choice?.delta?.content)
|
||||||
|| extractTextFromContent(choice?.text)
|
|| extractTextFromContent(choice?.text)
|
||||||
|| extractTextFromContent(data?.output_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) {
|
if (!answer) {
|
||||||
throw new Error("The help assistant did not return any text.");
|
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 }>) =>
|
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 },
|
{ role: "system", content: HELP_SYSTEM_PROMPT },
|
||||||
...messages
|
...messages
|
||||||
],
|
],
|
||||||
{ maxTokens: 700, temperature: 0.2 }
|
{ maxTokens: 700, temperature: 0.2, reasoningEffort: "low" }
|
||||||
);
|
);
|
||||||
|
|
||||||
export const generateAIHelpTip = async (history: string[]) => {
|
export const generateAIHelpTip = async (sectionIndex: number) => {
|
||||||
const recentHistory = history.slice(-10);
|
const sectionDebug = getAIHelpTipSectionDebug(sectionIndex);
|
||||||
const historyBlock = recentHistory.length > 0
|
const section = sectionDebug.resolvedIndex >= 0
|
||||||
? recentHistory.map((tip, index) => `${index + 1}. ${tip}`).join("\n")
|
? HELP_GUIDE_SECTIONS[sectionDebug.resolvedIndex]
|
||||||
: "No previous tips.";
|
: 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:
|
Rules:
|
||||||
- Maximum one sentence.
|
- Maximum one sentence.
|
||||||
- Return plain text only.
|
- Return plain text only.
|
||||||
- No title, no bullet, no quotes.
|
- No title, no bullet, no quotes.
|
||||||
- Keep it subtle and practical.
|
- 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.
|
- Use only this guide for all information and never assume you know something that isn't mentioned in the guide.
|
||||||
|
- Start the sentence with "Tip: ".
|
||||||
Previous tips to avoid:
|
- Base the tip on this topic: ${sectionLabel}.
|
||||||
${historyBlock}`;
|
`;
|
||||||
|
|
||||||
const seen = new Set(recentHistory.map(normalizeLine));
|
|
||||||
|
|
||||||
for (let attempt = 0; attempt < 2; attempt += 1) {
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
||||||
const response = await requestFireworks(
|
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 }
|
{ role: "user", content: tipPrompt }
|
||||||
],
|
],
|
||||||
{ maxTokens: 80, temperature: 0.5 }
|
{ maxTokens: 192, temperature: 0.5, reasoningEffort: "low" }
|
||||||
);
|
);
|
||||||
|
|
||||||
const firstLine = response.split("\n")[0]?.trim() || "";
|
const firstLine = response.split("\n")[0]?.trim() || "";
|
||||||
@@ -145,9 +314,9 @@ ${historyBlock}`;
|
|||||||
const normalized = normalizeLine(cleaned);
|
const normalized = normalizeLine(cleaned);
|
||||||
|
|
||||||
if (!cleaned || !normalized) continue;
|
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;
|
return null;
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export const syncPreferences = async () => {
|
|||||||
collapsedUntilUpdatedTasks: store.collapsedUntilUpdatedTasks,
|
collapsedUntilUpdatedTasks: store.collapsedUntilUpdatedTasks,
|
||||||
subscribedBuckets: store.subscribedBuckets,
|
subscribedBuckets: store.subscribedBuckets,
|
||||||
supervisorUserIds: store.personalContextSupervisorIds,
|
supervisorUserIds: store.personalContextSupervisorIds,
|
||||||
aiHelpTipHistory: store.aiHelpTipHistory,
|
aiHelpTipSectionIndex: store.aiHelpTipSectionIndex,
|
||||||
lastCompletedTaskCleanupDate: store.lastCompletedTaskCleanupDate
|
lastCompletedTaskCleanupDate: store.lastCompletedTaskCleanupDate
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -176,14 +176,8 @@ export const clearNoteFilter = () => {
|
|||||||
setStore("noteFilter", createDefaultFilter());
|
setStore("noteFilter", createDefaultFilter());
|
||||||
};
|
};
|
||||||
|
|
||||||
export const appendAIHelpTipHistory = async (tip: string) => {
|
export const advanceAIHelpTipSectionIndex = async () => {
|
||||||
const normalizedTip = tip.trim();
|
setStore("aiHelpTipSectionIndex", prev => prev + 1);
|
||||||
if (!normalizedTip) return;
|
|
||||||
|
|
||||||
setStore("aiHelpTipHistory", prev => {
|
|
||||||
const next = [...prev.filter(existing => existing !== normalizedTip), normalizedTip];
|
|
||||||
return next.slice(-10);
|
|
||||||
});
|
|
||||||
|
|
||||||
await syncPreferences();
|
await syncPreferences();
|
||||||
};
|
};
|
||||||
|
|||||||
+4
-4
@@ -183,7 +183,7 @@ export interface TaskStore {
|
|||||||
buckets: Bucket[];
|
buckets: Bucket[];
|
||||||
subscribedBuckets: string[];
|
subscribedBuckets: string[];
|
||||||
personalContextSupervisorIds: string[];
|
personalContextSupervisorIds: string[];
|
||||||
aiHelpTipHistory: string[];
|
aiHelpTipSectionIndex: number;
|
||||||
notes: Note[];
|
notes: Note[];
|
||||||
isNotepadMode: boolean;
|
isNotepadMode: boolean;
|
||||||
quickloadTasks: string[];
|
quickloadTasks: string[];
|
||||||
@@ -203,7 +203,7 @@ export interface ScopedTaskgridPrefs {
|
|||||||
noteFilter?: Filter;
|
noteFilter?: Filter;
|
||||||
subscribedBuckets?: string[];
|
subscribedBuckets?: string[];
|
||||||
supervisorUserIds?: string[];
|
supervisorUserIds?: string[];
|
||||||
aiHelpTipHistory?: string[];
|
aiHelpTipSectionIndex?: number;
|
||||||
dismissedTaskUpdateIndicators?: Record<string, string>;
|
dismissedTaskUpdateIndicators?: Record<string, string>;
|
||||||
collapsedUntilUpdatedTasks?: Record<string, string>;
|
collapsedUntilUpdatedTasks?: Record<string, string>;
|
||||||
migratedStructuredTagsV2?: boolean;
|
migratedStructuredTagsV2?: boolean;
|
||||||
@@ -248,7 +248,7 @@ export const [store, setStore] = createStore<TaskStore>({
|
|||||||
buckets: [],
|
buckets: [],
|
||||||
subscribedBuckets: [],
|
subscribedBuckets: [],
|
||||||
personalContextSupervisorIds: [],
|
personalContextSupervisorIds: [],
|
||||||
aiHelpTipHistory: [],
|
aiHelpTipSectionIndex: 0,
|
||||||
notes: [],
|
notes: [],
|
||||||
isNotepadMode: false,
|
isNotepadMode: false,
|
||||||
quickloadTasks: [],
|
quickloadTasks: [],
|
||||||
@@ -298,7 +298,7 @@ export const applyScopedPrefsToStore = (prefs: ScopedTaskgridPrefs) => {
|
|||||||
prefId: currentUserId,
|
prefId: currentUserId,
|
||||||
subscribedBuckets: prefs.subscribedBuckets || [],
|
subscribedBuckets: prefs.subscribedBuckets || [],
|
||||||
personalContextSupervisorIds: personalContext?.supervisorUserIds || prefs.supervisorUserIds || [],
|
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("@")),
|
tagDefinitions: (prefs.tagDefinitions || []).filter((def: TagDefinition) => !def.name.startsWith("@")),
|
||||||
filterTemplates: prefs.filterTemplates || [],
|
filterTemplates: prefs.filterTemplates || [],
|
||||||
quickloadTasks: prefs.quickloadTasks || [],
|
quickloadTasks: prefs.quickloadTasks || [],
|
||||||
|
|||||||
Reference in New Issue
Block a user