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
+13 -12
View File
@@ -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
View File
@@ -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;
+3 -9
View File
@@ -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
View File
@@ -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 || [],