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.
|
||||
|
||||
## 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/<section-id>`.
|
||||
|
||||
## 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
|
||||
|
||||
+13
-12
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
+192
-23
@@ -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(/<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 => {
|
||||
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;
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
+4
-4
@@ -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<string, string>;
|
||||
collapsedUntilUpdatedTasks?: Record<string, string>;
|
||||
migratedStructuredTagsV2?: boolean;
|
||||
@@ -248,7 +248,7 @@ export const [store, setStore] = createStore<TaskStore>({
|
||||
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 || [],
|
||||
|
||||
Reference in New Issue
Block a user