@agent-native/toolkit 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -1
- package/agent-native.eject.json +411 -0
- package/dist/chat-history/ChatHistoryList.d.ts +103 -0
- package/dist/chat-history/ChatHistoryList.d.ts.map +1 -0
- package/dist/chat-history/ChatHistoryList.js +109 -0
- package/dist/chat-history/ChatHistoryList.js.map +1 -0
- package/dist/chat-history/ChatHistoryList.spec.d.ts +2 -0
- package/dist/chat-history/ChatHistoryList.spec.d.ts.map +1 -0
- package/dist/chat-history/ChatHistoryList.spec.js +283 -0
- package/dist/chat-history/ChatHistoryList.spec.js.map +1 -0
- package/dist/chat-history/index.d.ts +2 -0
- package/dist/chat-history/index.d.ts.map +1 -0
- package/dist/chat-history/index.js +2 -0
- package/dist/chat-history/index.js.map +1 -0
- package/dist/chat-history.css +314 -0
- package/package.json +20 -1
- package/src/app-shell/header-actions.tsx +84 -0
- package/src/app-shell/index.ts +1 -0
- package/src/chat-history/ChatHistoryList.spec.tsx +453 -0
- package/src/chat-history/ChatHistoryList.tsx +469 -0
- package/src/chat-history/index.ts +7 -0
- package/src/chat-history.css +314 -0
- package/src/collab-ui/AgentPresenceChip.tsx +69 -0
- package/src/collab-ui/LiveCursorOverlay.tsx +241 -0
- package/src/collab-ui/PresenceBar.tsx +346 -0
- package/src/collab-ui/RecentEditHighlights.tsx +225 -0
- package/src/collab-ui/RemoteSelectionRings.tsx +227 -0
- package/src/collab-ui/agent-identity.ts +16 -0
- package/src/collab-ui/index.ts +39 -0
- package/src/collab-ui/lead-client.ts +32 -0
- package/src/collab-ui/types.spec.ts +18 -0
- package/src/collab-ui/types.ts +96 -0
- package/src/composer/AgentComposerFrame.tsx +59 -0
- package/src/composer/ComposerPlusMenu.tsx +983 -0
- package/src/composer/MentionPopover.tsx +412 -0
- package/src/composer/PastedTextChip.tsx +150 -0
- package/src/composer/PromptComposer.spec.ts +73 -0
- package/src/composer/PromptComposer.tsx +718 -0
- package/src/composer/RealtimeVoiceMode.spec.tsx +771 -0
- package/src/composer/RealtimeVoiceMode.tsx +856 -0
- package/src/composer/TiptapComposer.spec.ts +390 -0
- package/src/composer/TiptapComposer.tsx +2936 -0
- package/src/composer/VoiceButton.spec.ts +33 -0
- package/src/composer/VoiceButton.tsx +255 -0
- package/src/composer/asset-picker-url.spec.ts +56 -0
- package/src/composer/asset-picker-url.ts +80 -0
- package/src/composer/attachment-accept.spec.ts +22 -0
- package/src/composer/attachment-accept.ts +71 -0
- package/src/composer/draft-key.spec.ts +19 -0
- package/src/composer/draft-key.ts +7 -0
- package/src/composer/extensions/FileReference.tsx +55 -0
- package/src/composer/extensions/MentionReference.tsx +93 -0
- package/src/composer/extensions/SkillReference.tsx +56 -0
- package/src/composer/index.ts +79 -0
- package/src/composer/pasted-text.spec.ts +162 -0
- package/src/composer/pasted-text.ts +160 -0
- package/src/composer/prompt-attachments.spec.ts +80 -0
- package/src/composer/prompt-attachments.ts +93 -0
- package/src/composer/realtime-voice-audio-level.spec.ts +32 -0
- package/src/composer/realtime-voice-audio-level.ts +64 -0
- package/src/composer/realtime-voice-transcript.spec.ts +95 -0
- package/src/composer/realtime-voice-transcript.ts +134 -0
- package/src/composer/runtime-adapters.tsx +343 -0
- package/src/composer/types.ts +66 -0
- package/src/composer/use-file-search.ts +64 -0
- package/src/composer/use-mention-search.ts +90 -0
- package/src/composer/use-skills.ts +45 -0
- package/src/composer/useRealtimeVoiceMode.spec.ts +818 -0
- package/src/composer/useRealtimeVoiceMode.tsx +2117 -0
- package/src/composer/useVoiceDictation.spec.ts +45 -0
- package/src/composer/useVoiceDictation.ts +1191 -0
- package/src/context-ui/ContextMeter.tsx +101 -0
- package/src/context-ui/ContextSegmentRow.tsx +184 -0
- package/src/context-ui/ContextTreemap.tsx +125 -0
- package/src/context-ui/ContextXRayPanel.tsx +377 -0
- package/src/context-ui/format.ts +42 -0
- package/src/context-ui/index.ts +15 -0
- package/src/context-ui/types.ts +43 -0
- package/src/design-tweaks/index.ts +1 -0
- package/src/design-tweaks/visual-style-controls.tsx +1084 -0
- package/src/editor/BubbleToolbar.tsx +253 -0
- package/src/editor/CodeBlockNode.tsx +254 -0
- package/src/editor/DragHandle.spec.ts +588 -0
- package/src/editor/DragHandle.ts +1437 -0
- package/src/editor/ImageExtension.roundtrip.spec.ts +117 -0
- package/src/editor/ImageExtension.ts +306 -0
- package/src/editor/RegistryBlockContext.tsx +107 -0
- package/src/editor/RegistryBlockNode.spec.ts +279 -0
- package/src/editor/RegistryBlockNode.tsx +652 -0
- package/src/editor/RichMarkdownEditor.collab.spec.ts +79 -0
- package/src/editor/RichMarkdownEditor.roundtrip.spec.ts +226 -0
- package/src/editor/RichMarkdownEditor.tsx +81 -0
- package/src/editor/RunId.ts +61 -0
- package/src/editor/SharedRichEditor.tsx +321 -0
- package/src/editor/SlashCommandMenu.tsx +310 -0
- package/src/editor/disableHistory.spec.ts +85 -0
- package/src/editor/extensions.ts +402 -0
- package/src/editor/gfmDoc.spec.ts +230 -0
- package/src/editor/gfmDoc.ts +92 -0
- package/src/editor/index.ts +93 -0
- package/src/editor/registrySlashCommands.ts +131 -0
- package/src/editor/structuralReorderUndo.spec.ts +144 -0
- package/src/editor/surgical-apply.spec.ts +279 -0
- package/src/editor/surgical-apply.ts +192 -0
- package/src/editor/useCollabReconcile.concurrent.spec.ts +422 -0
- package/src/editor/useCollabReconcile.idempotent.spec.ts +324 -0
- package/src/editor/useCollabReconcile.ts +589 -0
- package/src/editor.css +510 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/use-mobile.tsx +21 -0
- package/src/hooks/use-toast.ts +188 -0
- package/src/index.ts +8 -0
- package/src/onboarding/index.ts +1 -0
- package/src/provider.tsx +47 -0
- package/src/sharing/VisibilityBadge.spec.ts +96 -0
- package/src/sharing/VisibilityBadge.tsx +37 -0
- package/src/sharing/index.ts +4 -0
- package/src/styles.css +21 -0
- package/src/ui/accordion.tsx +56 -0
- package/src/ui/alert-dialog.tsx +141 -0
- package/src/ui/alert.tsx +59 -0
- package/src/ui/aspect-ratio.tsx +5 -0
- package/src/ui/avatar.tsx +48 -0
- package/src/ui/badge.tsx +37 -0
- package/src/ui/breadcrumb.tsx +115 -0
- package/src/ui/button.tsx +78 -0
- package/src/ui/calendar.tsx +214 -0
- package/src/ui/card.tsx +86 -0
- package/src/ui/carousel.tsx +260 -0
- package/src/ui/chart.tsx +375 -0
- package/src/ui/checkbox.tsx +28 -0
- package/src/ui/collapsible.tsx +9 -0
- package/src/ui/command.spec.tsx +51 -0
- package/src/ui/command.tsx +174 -0
- package/src/ui/context-menu.tsx +202 -0
- package/src/ui/date-picker.tsx +64 -0
- package/src/ui/dialog.spec.tsx +43 -0
- package/src/ui/dialog.tsx +146 -0
- package/src/ui/drawer.tsx +126 -0
- package/src/ui/dropdown-menu.tsx +218 -0
- package/src/ui/form.tsx +177 -0
- package/src/ui/hover-card.tsx +27 -0
- package/src/ui/index.ts +52 -0
- package/src/ui/input-otp.tsx +69 -0
- package/src/ui/input.tsx +22 -0
- package/src/ui/label.tsx +24 -0
- package/src/ui/logo.tsx +36 -0
- package/src/ui/menubar.tsx +235 -0
- package/src/ui/navigation-menu.tsx +128 -0
- package/src/ui/pagination.tsx +121 -0
- package/src/ui/popover.tsx +44 -0
- package/src/ui/progress.tsx +26 -0
- package/src/ui/radio-group.tsx +42 -0
- package/src/ui/resizable.tsx +43 -0
- package/src/ui/scroll-area.tsx +46 -0
- package/src/ui/select.tsx +158 -0
- package/src/ui/separator.tsx +29 -0
- package/src/ui/sheet.tsx +160 -0
- package/src/ui/sidebar.tsx +777 -0
- package/src/ui/skeleton.tsx +18 -0
- package/src/ui/slider.tsx +26 -0
- package/src/ui/sonner.tsx +58 -0
- package/src/ui/spinner.tsx +17 -0
- package/src/ui/switch.tsx +27 -0
- package/src/ui/table.tsx +117 -0
- package/src/ui/tabs.tsx +53 -0
- package/src/ui/textarea.tsx +23 -0
- package/src/ui/toast.tsx +127 -0
- package/src/ui/toaster.tsx +33 -0
- package/src/ui/toggle-group.tsx +59 -0
- package/src/ui/toggle.tsx +43 -0
- package/src/ui/tooltip.tsx +53 -0
- package/src/ui/use-toast.ts +3 -0
- package/src/utils.ts +8 -0
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
import {
|
|
2
|
+
IconDots,
|
|
3
|
+
IconPencil,
|
|
4
|
+
IconPinned,
|
|
5
|
+
IconPinnedOff,
|
|
6
|
+
IconSearch,
|
|
7
|
+
IconTrash,
|
|
8
|
+
} from "@tabler/icons-react";
|
|
9
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
10
|
+
|
|
11
|
+
import { cn } from "../utils.js";
|
|
12
|
+
|
|
13
|
+
/** A single row in a chat history list. Purely data — no fetching, no
|
|
14
|
+
* assistant-ui context. Formatting (relative time, "Active"/"Open" labels,
|
|
15
|
+
* status spinners) is the caller's responsibility; hand in the already
|
|
16
|
+
* rendered node via `timestamp`. */
|
|
17
|
+
export interface ChatHistoryItem {
|
|
18
|
+
id: string;
|
|
19
|
+
title: React.ReactNode;
|
|
20
|
+
/** Plain-text form of `title`, used to seed the inline rename input.
|
|
21
|
+
* Only needed when `onRename` is supplied. */
|
|
22
|
+
titleText?: string;
|
|
23
|
+
/** Secondary line — e.g. a message preview. */
|
|
24
|
+
subtitle?: React.ReactNode;
|
|
25
|
+
/** Tertiary line — e.g. a scope/folder label. */
|
|
26
|
+
detail?: React.ReactNode;
|
|
27
|
+
/** Pre-formatted trailing value: a relative time, "Active"/"Open", or a
|
|
28
|
+
* status node (spinner, unread dot). This component does not format
|
|
29
|
+
* dates or compute status itself. */
|
|
30
|
+
timestamp?: React.ReactNode;
|
|
31
|
+
pinned?: boolean;
|
|
32
|
+
disabled?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ChatHistorySection {
|
|
36
|
+
id: string;
|
|
37
|
+
label?: React.ReactNode;
|
|
38
|
+
items: ChatHistoryItem[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ChatHistoryListLabels {
|
|
42
|
+
options: string | ((item: ChatHistoryItem) => string);
|
|
43
|
+
renameInput: string | ((item: ChatHistoryItem) => string);
|
|
44
|
+
rename: string;
|
|
45
|
+
pin: string;
|
|
46
|
+
unpin: string;
|
|
47
|
+
delete: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ChatHistoryListProps {
|
|
51
|
+
/** Flat item list. Ignored when `sections` is provided. */
|
|
52
|
+
items?: ChatHistoryItem[];
|
|
53
|
+
/** Grouped item list (e.g. "This deck" / "All chats", or pinned/recent). */
|
|
54
|
+
sections?: ChatHistorySection[];
|
|
55
|
+
/** Id of the currently active/selected item — highlighted in the list. */
|
|
56
|
+
activeId?: string | null;
|
|
57
|
+
onSelect: (id: string) => void;
|
|
58
|
+
/** Fired on double-click / explicit "open" gesture, distinct from select. */
|
|
59
|
+
onOpen?: (id: string) => void;
|
|
60
|
+
/** Presence enables the pin/unpin row action. */
|
|
61
|
+
onTogglePin?: (id: string) => void;
|
|
62
|
+
/** Presence enables the inline "Rename" row action. */
|
|
63
|
+
onRename?: (id: string, nextTitle: string) => void;
|
|
64
|
+
/** Optional character limit for the inline rename input. */
|
|
65
|
+
renameMaxLength?: number;
|
|
66
|
+
/** Presence enables the "Delete" row action. */
|
|
67
|
+
onDelete?: (id: string) => void;
|
|
68
|
+
/** Escape hatch: fully custom row action menu content, replacing the
|
|
69
|
+
* built-in rename/pin/delete items. Still gated by the trigger button,
|
|
70
|
+
* which shows whenever this or any of the on* callbacks above is set. */
|
|
71
|
+
renderRowActions?: (item: ChatHistoryItem) => React.ReactNode;
|
|
72
|
+
/** App-specific actions inserted after pin and before delete. Call
|
|
73
|
+
* `closeMenu` before starting the action. */
|
|
74
|
+
renderAdditionalRowActions?: (
|
|
75
|
+
item: ChatHistoryItem,
|
|
76
|
+
closeMenu: () => void,
|
|
77
|
+
) => React.ReactNode;
|
|
78
|
+
|
|
79
|
+
/** Controlled search input. Omit `onSearchChange` to hide the search box
|
|
80
|
+
* entirely and let the host render its own (results are always supplied
|
|
81
|
+
* by the caller via `items`/`sections` either way). */
|
|
82
|
+
searchValue?: string;
|
|
83
|
+
onSearchChange?: (value: string) => void;
|
|
84
|
+
searchPlaceholder?: string;
|
|
85
|
+
searchInputRef?: React.Ref<HTMLInputElement>;
|
|
86
|
+
|
|
87
|
+
/** True while results are being fetched — shows `loadingLabel` instead of
|
|
88
|
+
* the list. */
|
|
89
|
+
loading?: boolean;
|
|
90
|
+
loadingLabel?: React.ReactNode;
|
|
91
|
+
/** Shown instead of the list when set (takes priority over `loading`). */
|
|
92
|
+
error?: React.ReactNode;
|
|
93
|
+
emptyLabel?: React.ReactNode;
|
|
94
|
+
/** Shown instead of `emptyLabel` when `searchValue` is non-empty. */
|
|
95
|
+
emptySearchLabel?: React.ReactNode;
|
|
96
|
+
|
|
97
|
+
/** Optional localized labels for row actions and their accessible controls. */
|
|
98
|
+
labels?: Partial<ChatHistoryListLabels>;
|
|
99
|
+
|
|
100
|
+
/** Rendered after the list, inside the scroll container (e.g. a
|
|
101
|
+
* "Load older chats" button). */
|
|
102
|
+
footer?: React.ReactNode;
|
|
103
|
+
|
|
104
|
+
/** "popover" (default) matches core's compact HistoryPopover list.
|
|
105
|
+
* "rail" is a denser variant with a solid active background, closer to
|
|
106
|
+
* a sidebar run rail. */
|
|
107
|
+
variant?: "popover" | "rail";
|
|
108
|
+
className?: string;
|
|
109
|
+
listClassName?: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const DEFAULT_LABELS: ChatHistoryListLabels = {
|
|
113
|
+
options: "Chat options",
|
|
114
|
+
renameInput: "Rename chat",
|
|
115
|
+
rename: "Rename",
|
|
116
|
+
pin: "Pin to top",
|
|
117
|
+
unpin: "Unpin from top",
|
|
118
|
+
delete: "Delete",
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Presentational chat history list shared by the Core `HistoryPopover`
|
|
123
|
+
* and Agent-Native Code's run rail
|
|
124
|
+
* (code-agents-ui). Styling is driven by stable `an-chat-history*` class
|
|
125
|
+
* names (see `@agent-native/toolkit/chat-history.css`) rather than Tailwind utilities
|
|
126
|
+
* so the same component renders correctly in a Tailwind host and in
|
|
127
|
+
* code-agents-ui's plain-CSS host.
|
|
128
|
+
*
|
|
129
|
+
* This component owns no data: search filtering, pin/rename persistence,
|
|
130
|
+
* and time formatting all stay with the caller. It only renders whatever
|
|
131
|
+
* `items`/`sections` it is given, plus the optional search box and
|
|
132
|
+
* loading/empty/error states.
|
|
133
|
+
*/
|
|
134
|
+
export function ChatHistoryList({
|
|
135
|
+
items,
|
|
136
|
+
sections,
|
|
137
|
+
activeId = null,
|
|
138
|
+
onSelect,
|
|
139
|
+
onOpen,
|
|
140
|
+
onTogglePin,
|
|
141
|
+
onRename,
|
|
142
|
+
renameMaxLength,
|
|
143
|
+
onDelete,
|
|
144
|
+
renderRowActions,
|
|
145
|
+
renderAdditionalRowActions,
|
|
146
|
+
searchValue,
|
|
147
|
+
onSearchChange,
|
|
148
|
+
searchPlaceholder = "Search chats...",
|
|
149
|
+
searchInputRef,
|
|
150
|
+
loading = false,
|
|
151
|
+
loadingLabel = "Searching...",
|
|
152
|
+
error,
|
|
153
|
+
emptyLabel = "No chats yet",
|
|
154
|
+
emptySearchLabel = "No matching chats",
|
|
155
|
+
labels,
|
|
156
|
+
footer,
|
|
157
|
+
variant = "popover",
|
|
158
|
+
className,
|
|
159
|
+
listClassName,
|
|
160
|
+
}: ChatHistoryListProps) {
|
|
161
|
+
const resolvedLabels: ChatHistoryListLabels = {
|
|
162
|
+
options: labels?.options ?? DEFAULT_LABELS.options,
|
|
163
|
+
renameInput: labels?.renameInput ?? DEFAULT_LABELS.renameInput,
|
|
164
|
+
rename: labels?.rename ?? DEFAULT_LABELS.rename,
|
|
165
|
+
pin: labels?.pin ?? DEFAULT_LABELS.pin,
|
|
166
|
+
unpin: labels?.unpin ?? DEFAULT_LABELS.unpin,
|
|
167
|
+
delete: labels?.delete ?? DEFAULT_LABELS.delete,
|
|
168
|
+
};
|
|
169
|
+
const resolvedSections: ChatHistorySection[] =
|
|
170
|
+
sections ?? (items ? [{ id: "default", items }] : []);
|
|
171
|
+
const totalCount = resolvedSections.reduce(
|
|
172
|
+
(sum, section) => sum + section.items.length,
|
|
173
|
+
0,
|
|
174
|
+
);
|
|
175
|
+
const hasSearchValue = Boolean(searchValue?.trim());
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<div
|
|
179
|
+
className={cn(
|
|
180
|
+
"an-chat-history",
|
|
181
|
+
variant === "rail" && "an-chat-history--rail",
|
|
182
|
+
className,
|
|
183
|
+
)}
|
|
184
|
+
data-agent-native="chat-history-list"
|
|
185
|
+
>
|
|
186
|
+
{onSearchChange && (
|
|
187
|
+
<div className="an-chat-history__search">
|
|
188
|
+
<IconSearch size={13} className="an-chat-history__search-icon" />
|
|
189
|
+
<input
|
|
190
|
+
ref={searchInputRef}
|
|
191
|
+
type="text"
|
|
192
|
+
value={searchValue ?? ""}
|
|
193
|
+
onChange={(event) => onSearchChange(event.target.value)}
|
|
194
|
+
placeholder={searchPlaceholder}
|
|
195
|
+
aria-label={searchPlaceholder}
|
|
196
|
+
className="an-chat-history__search-input"
|
|
197
|
+
/>
|
|
198
|
+
</div>
|
|
199
|
+
)}
|
|
200
|
+
<div className={cn("an-chat-history__list", listClassName)}>
|
|
201
|
+
{error ? (
|
|
202
|
+
<div className="an-chat-history__state an-chat-history__state--error">
|
|
203
|
+
{error}
|
|
204
|
+
</div>
|
|
205
|
+
) : loading ? (
|
|
206
|
+
<div className="an-chat-history__state">{loadingLabel}</div>
|
|
207
|
+
) : totalCount === 0 ? (
|
|
208
|
+
<div className="an-chat-history__state">
|
|
209
|
+
{hasSearchValue ? emptySearchLabel : emptyLabel}
|
|
210
|
+
</div>
|
|
211
|
+
) : (
|
|
212
|
+
resolvedSections.map(
|
|
213
|
+
(section) =>
|
|
214
|
+
section.items.length > 0 && (
|
|
215
|
+
<div key={section.id} className="an-chat-history__section">
|
|
216
|
+
{section.label && (
|
|
217
|
+
<div className="an-chat-history__section-label">
|
|
218
|
+
{section.label}
|
|
219
|
+
</div>
|
|
220
|
+
)}
|
|
221
|
+
{section.items.map((item) => (
|
|
222
|
+
<ChatHistoryRow
|
|
223
|
+
key={item.id}
|
|
224
|
+
item={item}
|
|
225
|
+
active={item.id === activeId}
|
|
226
|
+
onSelect={onSelect}
|
|
227
|
+
onOpen={onOpen}
|
|
228
|
+
onTogglePin={onTogglePin}
|
|
229
|
+
onRename={onRename}
|
|
230
|
+
renameMaxLength={renameMaxLength}
|
|
231
|
+
onDelete={onDelete}
|
|
232
|
+
renderRowActions={renderRowActions}
|
|
233
|
+
renderAdditionalRowActions={renderAdditionalRowActions}
|
|
234
|
+
labels={resolvedLabels}
|
|
235
|
+
/>
|
|
236
|
+
))}
|
|
237
|
+
</div>
|
|
238
|
+
),
|
|
239
|
+
)
|
|
240
|
+
)}
|
|
241
|
+
{footer}
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function ChatHistoryRow({
|
|
248
|
+
item,
|
|
249
|
+
active,
|
|
250
|
+
onSelect,
|
|
251
|
+
onOpen,
|
|
252
|
+
onTogglePin,
|
|
253
|
+
onRename,
|
|
254
|
+
renameMaxLength,
|
|
255
|
+
onDelete,
|
|
256
|
+
renderRowActions,
|
|
257
|
+
renderAdditionalRowActions,
|
|
258
|
+
labels,
|
|
259
|
+
}: {
|
|
260
|
+
item: ChatHistoryItem;
|
|
261
|
+
active: boolean;
|
|
262
|
+
onSelect: (id: string) => void;
|
|
263
|
+
onOpen?: (id: string) => void;
|
|
264
|
+
onTogglePin?: (id: string) => void;
|
|
265
|
+
onRename?: (id: string, nextTitle: string) => void;
|
|
266
|
+
renameMaxLength?: number;
|
|
267
|
+
onDelete?: (id: string) => void;
|
|
268
|
+
renderRowActions?: (item: ChatHistoryItem) => React.ReactNode;
|
|
269
|
+
renderAdditionalRowActions?: (
|
|
270
|
+
item: ChatHistoryItem,
|
|
271
|
+
closeMenu: () => void,
|
|
272
|
+
) => React.ReactNode;
|
|
273
|
+
labels: ChatHistoryListLabels;
|
|
274
|
+
}) {
|
|
275
|
+
const [isRenaming, setIsRenaming] = useState(false);
|
|
276
|
+
const [draftTitle, setDraftTitle] = useState("");
|
|
277
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
278
|
+
const renameInputRef = useRef<HTMLInputElement | null>(null);
|
|
279
|
+
const menuRef = useRef<HTMLDivElement | null>(null);
|
|
280
|
+
|
|
281
|
+
const hasMenu = Boolean(
|
|
282
|
+
onTogglePin ||
|
|
283
|
+
onRename ||
|
|
284
|
+
onDelete ||
|
|
285
|
+
renderRowActions ||
|
|
286
|
+
renderAdditionalRowActions,
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
useEffect(() => {
|
|
290
|
+
if (!menuOpen) return;
|
|
291
|
+
function handlePointerDown(event: PointerEvent) {
|
|
292
|
+
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
|
|
293
|
+
setMenuOpen(false);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
function handleKeyDown(event: KeyboardEvent) {
|
|
297
|
+
if (event.key === "Escape") setMenuOpen(false);
|
|
298
|
+
}
|
|
299
|
+
document.addEventListener("pointerdown", handlePointerDown);
|
|
300
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
301
|
+
return () => {
|
|
302
|
+
document.removeEventListener("pointerdown", handlePointerDown);
|
|
303
|
+
document.removeEventListener("keydown", handleKeyDown);
|
|
304
|
+
};
|
|
305
|
+
}, [menuOpen]);
|
|
306
|
+
|
|
307
|
+
function startRename() {
|
|
308
|
+
setDraftTitle(item.titleText ?? "");
|
|
309
|
+
setIsRenaming(true);
|
|
310
|
+
setMenuOpen(false);
|
|
311
|
+
window.requestAnimationFrame(() => {
|
|
312
|
+
renameInputRef.current?.select();
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function commitRename() {
|
|
317
|
+
const trimmed = draftTitle.trim();
|
|
318
|
+
setIsRenaming(false);
|
|
319
|
+
if (trimmed && trimmed !== item.titleText) {
|
|
320
|
+
onRename?.(item.id, trimmed);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function handleRenameKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {
|
|
325
|
+
if (event.key === "Enter") {
|
|
326
|
+
event.preventDefault();
|
|
327
|
+
commitRename();
|
|
328
|
+
} else if (event.key === "Escape") {
|
|
329
|
+
event.preventDefault();
|
|
330
|
+
setIsRenaming(false);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const optionsLabel =
|
|
335
|
+
typeof labels.options === "function"
|
|
336
|
+
? labels.options(item)
|
|
337
|
+
: labels.options;
|
|
338
|
+
const renameInputLabel =
|
|
339
|
+
typeof labels.renameInput === "function"
|
|
340
|
+
? labels.renameInput(item)
|
|
341
|
+
: labels.renameInput;
|
|
342
|
+
|
|
343
|
+
return (
|
|
344
|
+
<div
|
|
345
|
+
className={cn(
|
|
346
|
+
"an-chat-history-row",
|
|
347
|
+
active && "an-chat-history-row--active",
|
|
348
|
+
item.pinned && "an-chat-history-row--pinned",
|
|
349
|
+
isRenaming && "an-chat-history-row--renaming",
|
|
350
|
+
item.disabled && "an-chat-history-row--disabled",
|
|
351
|
+
)}
|
|
352
|
+
>
|
|
353
|
+
{isRenaming ? (
|
|
354
|
+
<div className="an-chat-history-row__rename">
|
|
355
|
+
<input
|
|
356
|
+
ref={renameInputRef}
|
|
357
|
+
className="an-chat-history-row__rename-input"
|
|
358
|
+
value={draftTitle}
|
|
359
|
+
maxLength={renameMaxLength}
|
|
360
|
+
onChange={(event) => setDraftTitle(event.target.value)}
|
|
361
|
+
onKeyDown={handleRenameKeyDown}
|
|
362
|
+
onBlur={commitRename}
|
|
363
|
+
autoFocus
|
|
364
|
+
aria-label={renameInputLabel}
|
|
365
|
+
/>
|
|
366
|
+
</div>
|
|
367
|
+
) : (
|
|
368
|
+
<button
|
|
369
|
+
type="button"
|
|
370
|
+
className="an-chat-history-row__button"
|
|
371
|
+
onClick={() => !item.disabled && onSelect(item.id)}
|
|
372
|
+
onDoubleClick={() => !item.disabled && onOpen?.(item.id)}
|
|
373
|
+
disabled={item.disabled}
|
|
374
|
+
>
|
|
375
|
+
<div className="an-chat-history-row__topline">
|
|
376
|
+
<span className="an-chat-history-row__title">{item.title}</span>
|
|
377
|
+
{item.timestamp != null && (
|
|
378
|
+
<span className="an-chat-history-row__timestamp">
|
|
379
|
+
{item.timestamp}
|
|
380
|
+
</span>
|
|
381
|
+
)}
|
|
382
|
+
</div>
|
|
383
|
+
{item.subtitle != null && (
|
|
384
|
+
<div className="an-chat-history-row__subtitle">{item.subtitle}</div>
|
|
385
|
+
)}
|
|
386
|
+
{item.detail != null && (
|
|
387
|
+
<div className="an-chat-history-row__detail">{item.detail}</div>
|
|
388
|
+
)}
|
|
389
|
+
</button>
|
|
390
|
+
)}
|
|
391
|
+
|
|
392
|
+
{!isRenaming && hasMenu && (
|
|
393
|
+
<div className="an-chat-history-row__menu" ref={menuRef}>
|
|
394
|
+
<button
|
|
395
|
+
type="button"
|
|
396
|
+
className={cn(
|
|
397
|
+
"an-chat-history-row__menu-trigger",
|
|
398
|
+
item.pinned && "an-chat-history-row__menu-trigger--pinned",
|
|
399
|
+
)}
|
|
400
|
+
aria-label={optionsLabel}
|
|
401
|
+
aria-haspopup="menu"
|
|
402
|
+
aria-expanded={menuOpen}
|
|
403
|
+
onClick={() => setMenuOpen((open) => !open)}
|
|
404
|
+
>
|
|
405
|
+
{item.pinned ? (
|
|
406
|
+
<IconPinned size={13} strokeWidth={1.8} />
|
|
407
|
+
) : (
|
|
408
|
+
<IconDots size={14} strokeWidth={1.8} />
|
|
409
|
+
)}
|
|
410
|
+
</button>
|
|
411
|
+
{menuOpen && (
|
|
412
|
+
<div className="an-chat-history-row__menu-content" role="menu">
|
|
413
|
+
{renderRowActions ? (
|
|
414
|
+
renderRowActions(item)
|
|
415
|
+
) : (
|
|
416
|
+
<>
|
|
417
|
+
{onRename && (
|
|
418
|
+
<button
|
|
419
|
+
type="button"
|
|
420
|
+
role="menuitem"
|
|
421
|
+
className="an-chat-history-row__menu-item"
|
|
422
|
+
onClick={startRename}
|
|
423
|
+
>
|
|
424
|
+
<IconPencil size={13} strokeWidth={1.8} />
|
|
425
|
+
<span>{labels.rename}</span>
|
|
426
|
+
</button>
|
|
427
|
+
)}
|
|
428
|
+
{onTogglePin && (
|
|
429
|
+
<button
|
|
430
|
+
type="button"
|
|
431
|
+
role="menuitem"
|
|
432
|
+
className="an-chat-history-row__menu-item"
|
|
433
|
+
onClick={() => {
|
|
434
|
+
setMenuOpen(false);
|
|
435
|
+
onTogglePin(item.id);
|
|
436
|
+
}}
|
|
437
|
+
>
|
|
438
|
+
{item.pinned ? (
|
|
439
|
+
<IconPinnedOff size={13} strokeWidth={1.8} />
|
|
440
|
+
) : (
|
|
441
|
+
<IconPinned size={13} strokeWidth={1.8} />
|
|
442
|
+
)}
|
|
443
|
+
<span>{item.pinned ? labels.unpin : labels.pin}</span>
|
|
444
|
+
</button>
|
|
445
|
+
)}
|
|
446
|
+
{renderAdditionalRowActions?.(item, () => setMenuOpen(false))}
|
|
447
|
+
{onDelete && (
|
|
448
|
+
<button
|
|
449
|
+
type="button"
|
|
450
|
+
role="menuitem"
|
|
451
|
+
className="an-chat-history-row__menu-item an-chat-history-row__menu-item--danger"
|
|
452
|
+
onClick={() => {
|
|
453
|
+
setMenuOpen(false);
|
|
454
|
+
onDelete(item.id);
|
|
455
|
+
}}
|
|
456
|
+
>
|
|
457
|
+
<IconTrash size={13} strokeWidth={1.8} />
|
|
458
|
+
<span>{labels.delete}</span>
|
|
459
|
+
</button>
|
|
460
|
+
)}
|
|
461
|
+
</>
|
|
462
|
+
)}
|
|
463
|
+
</div>
|
|
464
|
+
)}
|
|
465
|
+
</div>
|
|
466
|
+
)}
|
|
467
|
+
</div>
|
|
468
|
+
);
|
|
469
|
+
}
|