AIChat (elements/ai/AIChat) is a complete LLM chat UI built on new hooks: - useAIChat: streaming conversation state (send, stop, regenerate, edit, feedback); `send` may return a string, async iterable, ReadableStream or fetch Response - useAIChatHistory: saved conversations with optional localStorage persistence and generated titles, shown in AIChatSidebar - useAIChatSpeech: read aloud via browser speech synthesis or a custom `speak` returning audio - useAIChatDictation: voice input via browser recognition or a custom `transcribe` Also adds a model picker, message editing, thumbs up/down, markdown export, scoped markdown styles in base.css, the twuiReadTextStream, twuiStripMarkdown and twuiGroupByRecency utils (with tests), and a playground page. AIPromptBlock is kept and marked deprecated. Co-Authored-By: Claude Opus 5.5 <[email protected]>
47 lines
1.4 KiB
TypeScript
47 lines
1.4 KiB
TypeScript
export type TWUIRecencyGroup<T> = {
|
|
label: string;
|
|
items: T[];
|
|
};
|
|
|
|
const DAY = 24 * 60 * 60 * 1000;
|
|
|
|
/**
|
|
* # Group by recency
|
|
* Buckets items into "Today", "Yesterday", "Previous 7 days",
|
|
* "Previous 30 days" and then by month, newest first - the grouping chat
|
|
* apps use for conversation lists. Empty groups are left out.
|
|
*/
|
|
export default function twuiGroupByRecency<T>(
|
|
items: T[],
|
|
getTime: (item: T) => number,
|
|
now: number = Date.now(),
|
|
): TWUIRecencyGroup<T>[] {
|
|
const startOfToday = new Date(now);
|
|
startOfToday.setHours(0, 0, 0, 0);
|
|
const today = startOfToday.getTime();
|
|
|
|
const groups = new Map<string, T[]>();
|
|
|
|
const label = (time: number) => {
|
|
if (time >= today) return "Today";
|
|
if (time >= today - DAY) return "Yesterday";
|
|
if (time >= today - 7 * DAY) return "Previous 7 days";
|
|
if (time >= today - 30 * DAY) return "Previous 30 days";
|
|
return new Date(time).toLocaleDateString(undefined, {
|
|
month: "long",
|
|
year: "numeric",
|
|
});
|
|
};
|
|
|
|
[...items]
|
|
.sort((a, b) => getTime(b) - getTime(a))
|
|
.forEach((item) => {
|
|
const key = label(getTime(item));
|
|
const list = groups.get(key);
|
|
if (list) list.push(item);
|
|
else groups.set(key, [item]);
|
|
});
|
|
|
|
return Array.from(groups, ([label, items]) => ({ label, items }));
|
|
}
|