tips working and optimized
This commit is contained in:
+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