* feat(desktop): add native host scaffold * feat(webui): track turns and usage in gateway * feat(webui): polish desktop chat experience * feat(apps): add ArcGIS and Joplin logos * feat(desktop): polish shell and shared surfaces * fix(webui): avoid preview chips for glob references * test: align CI expectations for token fallback * feat(webui): preview prompt rail entries * feat(webui): add prompt navigator drawer * style(webui): refine prompt navigator placement * style(webui): align prompt navigator with header actions * style(webui): simplify prompt navigator header * refactor(webui): clean thread resource refresh * feat(desktop): add native reply notifications * fix(webui): preserve desktop restart and replay state * fix(desktop): harden gateway proxy startup * fix(web): fall back when readability is unavailable * fix(desktop): hide window instead of closing on macos * fix(webui): unify desktop header actions * fix(webui): simplify prompt history rows * fix(desktop): log notification delivery failures * chore(desktop): clean source package artifacts * fix(cron): support one-time relative reminders * fix(webui): reveal scroll button in place * Revert "fix(cron): support one-time relative reminders" This reverts commit 4c4661da120a3c7283e0768412bae48604e7390b. * refactor(webui): extract token usage heatmap * docs(desktop): clarify contributor guides --------- Co-authored-by: chengyongru <2755839590@qq.com>
65 lines
2.0 KiB
TypeScript
65 lines
2.0 KiB
TypeScript
import type { UIMessage } from "@/lib/types";
|
|
|
|
export interface PromptAnchor {
|
|
id: string;
|
|
label: string;
|
|
preview: string;
|
|
createdAt: number;
|
|
index: number;
|
|
}
|
|
|
|
export function userPromptAnchors(messages: UIMessage[]): PromptAnchor[] {
|
|
let index = 0;
|
|
return messages.flatMap((message) => {
|
|
if (message.role !== "user") return [];
|
|
const anchor: PromptAnchor = {
|
|
id: message.id,
|
|
label: promptLabel(message.content, index),
|
|
preview: promptPreview(message.content, index),
|
|
createdAt: message.createdAt,
|
|
index,
|
|
};
|
|
index += 1;
|
|
return [anchor];
|
|
});
|
|
}
|
|
|
|
export function promptLabel(content: string, index: number): string {
|
|
const text = content.replace(/\s+/g, " ").trim();
|
|
if (!text) return `Prompt ${index + 1}`;
|
|
return text.length > 80 ? `${text.slice(0, 77)}...` : text;
|
|
}
|
|
|
|
export function promptPreview(content: string, index: number): string {
|
|
const text = content.replace(/\n{3,}/g, "\n\n").trim();
|
|
if (!text) return `Prompt ${index + 1}`;
|
|
return text.length > 320 ? `${text.slice(0, 317)}...` : text;
|
|
}
|
|
|
|
export function jumpToPrompt(scrollEl: HTMLElement | null, promptId: string | undefined): void {
|
|
if (!scrollEl || !promptId) return;
|
|
const target = findPromptElement(scrollEl, promptId);
|
|
if (!target) return;
|
|
scrollEl.scrollTo({
|
|
top: Math.max(0, promptTop(scrollEl, target) - 16),
|
|
behavior: "smooth",
|
|
});
|
|
}
|
|
|
|
export function findPromptElement(scrollEl: HTMLElement, promptId: string): HTMLElement | null {
|
|
const candidates = scrollEl.querySelectorAll<HTMLElement>("[data-user-prompt-id]");
|
|
return Array.from(candidates).find(
|
|
(candidate) => candidate.dataset.userPromptId === promptId,
|
|
) ?? null;
|
|
}
|
|
|
|
export function promptTop(scrollEl: HTMLElement, target: HTMLElement): number {
|
|
const scrollRect = scrollEl.getBoundingClientRect();
|
|
const targetRect = target.getBoundingClientRect();
|
|
const hasLayoutRect = scrollRect.top !== 0 || targetRect.top !== 0;
|
|
if (hasLayoutRect) {
|
|
return targetRect.top - scrollRect.top + scrollEl.scrollTop;
|
|
}
|
|
return target.offsetTop;
|
|
}
|