tips working and optimized
CI / build (push) Has been skipped
CI / deploy (push) Successful in 1m19s

This commit is contained in:
2026-03-25 19:37:30 -05:00
parent 7653c6bf0e
commit e45bc9e2b6
5 changed files with 278 additions and 68 deletions
+66 -20
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+3 -9
View File
@@ -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
View File
@@ -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 || [],