ai-design-system 0.1.116 → 0.1.118
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/components/composites/AccountDialog/AccountDialog.tsx +1 -1
- package/components/composites/ApprovalCard/ApprovalCard.tsx +42 -12
- package/components/composites/PromptInput/PromptInput.tsx +6 -6
- package/components/composites/SessionHeader/SessionHeader.tsx +30 -6
- package/components/composites/StoriesBar/StoriesBar.stories.tsx +6 -25
- package/components/composites/StoriesBar/StoriesBar.tsx +26 -21
- package/components/features/ChatPanel/ChatPanel.tsx +19 -0
- package/components/index.ts +3 -0
- package/dist/index.cjs +106 -41
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +23 -15
- package/dist/index.d.ts +144 -86
- package/dist/index.js +103 -42
- package/dist/index.js.map +1 -1
- package/lib/date.ts +70 -0
- package/package.json +1 -1
|
@@ -55,7 +55,7 @@ export const AccountButton = React.forwardRef<HTMLButtonElement, AccountButtonPr
|
|
|
55
55
|
<div
|
|
56
56
|
className={cn(
|
|
57
57
|
"w-6 h-6 rounded-full flex items-center justify-center text-xs font-semibold select-none text-white",
|
|
58
|
-
avatarColor ? "" : "bg-emerald-
|
|
58
|
+
avatarColor ? "" : "bg-gradient-to-br from-teal-800 to-emerald-600"
|
|
59
59
|
)}
|
|
60
60
|
style={avatarColor ? { backgroundColor: avatarColor } : undefined}
|
|
61
61
|
>
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { useState, useRef, useEffect } from "react";
|
|
5
5
|
import { Icon } from "@/components/primitives/Icon";
|
|
6
|
+
import { Button } from "@/components/primitives/Button";
|
|
6
7
|
import { Input } from "@/components/primitives/Input";
|
|
7
8
|
import { Textarea } from "@/components/primitives/Textarea";
|
|
8
9
|
import { Label } from "@/components/primitives/Label";
|
|
@@ -48,6 +49,12 @@ export interface ApprovalCardProps {
|
|
|
48
49
|
state?: ToolUIState;
|
|
49
50
|
approval?: ToolApproval;
|
|
50
51
|
className?: string;
|
|
52
|
+
/**
|
|
53
|
+
* Opens the file a question is about (`args.file_path`). With it, the
|
|
54
|
+
* question shows the file's name as a link; without it, or when the question
|
|
55
|
+
* names no file, nothing is shown.
|
|
56
|
+
*/
|
|
57
|
+
onOpenFile?: (path: string) => void;
|
|
51
58
|
}
|
|
52
59
|
|
|
53
60
|
function formatValue(value: unknown): string {
|
|
@@ -92,6 +99,7 @@ export const ApprovalCard = React.memo<ApprovalCardProps>(
|
|
|
92
99
|
state = "approval-requested",
|
|
93
100
|
approval = {},
|
|
94
101
|
className,
|
|
102
|
+
onOpenFile,
|
|
95
103
|
}) => {
|
|
96
104
|
const [isEditing, setIsEditing] = useState(false);
|
|
97
105
|
const [prevArgs, setPrevArgs] = useState(actionRequest.args);
|
|
@@ -99,6 +107,12 @@ export const ApprovalCard = React.memo<ApprovalCardProps>(
|
|
|
99
107
|
const [rejectReason, setRejectReason] = useState("");
|
|
100
108
|
const [showRejectInput, setShowRejectInput] = useState(false);
|
|
101
109
|
|
|
110
|
+
// The file the question is about, when the asker named one.
|
|
111
|
+
const questionFile =
|
|
112
|
+
typeof actionRequest.args?.file_path === "string" && actionRequest.args.file_path.trim()
|
|
113
|
+
? actionRequest.args.file_path
|
|
114
|
+
: null;
|
|
115
|
+
|
|
102
116
|
const hideSkipButton = (actionRequest.args?.hide_skip_button as boolean) ?? false;
|
|
103
117
|
const submitButtonText = (actionRequest.args?.button_submit_text as string) || 'Continue';
|
|
104
118
|
const skipButtonText = (actionRequest.args?.button_skip_text as string) || 'Skip';
|
|
@@ -267,29 +281,45 @@ export const ApprovalCard = React.memo<ApprovalCardProps>(
|
|
|
267
281
|
<StreamingMarkdown controls={false}>
|
|
268
282
|
{currentQuestion.question}
|
|
269
283
|
</StreamingMarkdown>
|
|
284
|
+
{questionFile && onOpenFile && (
|
|
285
|
+
<Button
|
|
286
|
+
type="button"
|
|
287
|
+
variant="link"
|
|
288
|
+
onClick={() => onOpenFile(questionFile)}
|
|
289
|
+
className="mt-2 h-auto p-0 inline-flex max-w-full items-center gap-1.5 text-sm font-medium text-primary hover:underline"
|
|
290
|
+
data-testid="approval-card-file-link"
|
|
291
|
+
>
|
|
292
|
+
<Icon name="file-text" className="h-4 w-4 flex-shrink-0" />
|
|
293
|
+
<span className="truncate">{questionFile.split("/").pop()}</span>
|
|
294
|
+
</Button>
|
|
295
|
+
)}
|
|
270
296
|
</div>
|
|
271
297
|
</div>
|
|
272
298
|
{questions.length > 1 && (
|
|
273
299
|
<div className="flex items-center gap-1.5 text-xs text-muted-foreground flex-shrink-0 font-normal">
|
|
274
|
-
<
|
|
300
|
+
<Button
|
|
275
301
|
type="button"
|
|
302
|
+
variant="ghost"
|
|
303
|
+
size="icon-sm"
|
|
276
304
|
onClick={() => setCurrentQuestionIndex((prev) => Math.max(0, prev - 1))}
|
|
277
305
|
disabled={currentQuestionIndex === 0}
|
|
278
|
-
className="hover:text-foreground disabled:opacity-30
|
|
306
|
+
className="h-6 w-6 p-0 hover:text-foreground disabled:opacity-30 transition-colors"
|
|
279
307
|
>
|
|
280
308
|
<Icon name="chevron-left" size="sm" />
|
|
281
|
-
</
|
|
309
|
+
</Button>
|
|
282
310
|
<span className="font-medium">{currentQuestionIndex + 1} of {questions.length}</span>
|
|
283
|
-
<
|
|
311
|
+
<Button
|
|
284
312
|
type="button"
|
|
313
|
+
variant="ghost"
|
|
314
|
+
size="icon-sm"
|
|
285
315
|
onClick={() =>
|
|
286
316
|
setCurrentQuestionIndex((prev) => Math.min(questions.length - 1, prev + 1))
|
|
287
317
|
}
|
|
288
318
|
disabled={currentQuestionIndex === questions.length - 1}
|
|
289
|
-
className="hover:text-foreground disabled:opacity-30
|
|
319
|
+
className="h-6 w-6 p-0 hover:text-foreground disabled:opacity-30 transition-colors"
|
|
290
320
|
>
|
|
291
321
|
<Icon name="chevron-right" size="sm" />
|
|
292
|
-
</
|
|
322
|
+
</Button>
|
|
293
323
|
</div>
|
|
294
324
|
)}
|
|
295
325
|
</div>
|
|
@@ -301,8 +331,8 @@ export const ApprovalCard = React.memo<ApprovalCardProps>(
|
|
|
301
331
|
const isOther = index === allOptions.length - 1;
|
|
302
332
|
const isSelected = isMultiSelect
|
|
303
333
|
? (selectedAnswers[currentQuestionIndex] as string[])?.includes(
|
|
304
|
-
|
|
305
|
-
|
|
334
|
+
isOther ? otherTexts[currentQuestionIndex] || "" : option
|
|
335
|
+
)
|
|
306
336
|
: selectedOptionIndices[currentQuestionIndex] === index;
|
|
307
337
|
|
|
308
338
|
return (
|
|
@@ -500,7 +530,7 @@ export const ApprovalCard = React.memo<ApprovalCardProps>(
|
|
|
500
530
|
}}
|
|
501
531
|
disabled={isProcessing}
|
|
502
532
|
>
|
|
503
|
-
|
|
533
|
+
<Icon name="check" size="sm" className="mr-2" />
|
|
504
534
|
Save & Approve
|
|
505
535
|
</ConfirmationAction>
|
|
506
536
|
</ConfirmationActions>
|
|
@@ -576,7 +606,7 @@ export const ApprovalCard = React.memo<ApprovalCardProps>(
|
|
|
576
606
|
}}
|
|
577
607
|
disabled={isProcessing}
|
|
578
608
|
>
|
|
579
|
-
|
|
609
|
+
<Icon name="x" size="sm" className="mr-2" />
|
|
580
610
|
Confirm Rejection
|
|
581
611
|
</ConfirmationAction>
|
|
582
612
|
</>
|
|
@@ -589,7 +619,7 @@ export const ApprovalCard = React.memo<ApprovalCardProps>(
|
|
|
589
619
|
onClick={() => setShowRejectInput(true)}
|
|
590
620
|
disabled={isProcessing}
|
|
591
621
|
>
|
|
592
|
-
|
|
622
|
+
<Icon name="x" size="sm" className="mr-2" />
|
|
593
623
|
{skipButtonText}
|
|
594
624
|
</ConfirmationAction>
|
|
595
625
|
)}
|
|
@@ -610,7 +640,7 @@ export const ApprovalCard = React.memo<ApprovalCardProps>(
|
|
|
610
640
|
onClick={onApprove}
|
|
611
641
|
disabled={isProcessing}
|
|
612
642
|
>
|
|
613
|
-
|
|
643
|
+
<Icon name="check" size="sm" className="mr-2" />
|
|
614
644
|
{submitButtonText}
|
|
615
645
|
</ConfirmationAction>
|
|
616
646
|
</>
|
|
@@ -145,7 +145,7 @@ export const PromptInput = React.memo<PromptInputBlockProps>(
|
|
|
145
145
|
className={cn(
|
|
146
146
|
"[&>[data-slot=input-group]]:rounded-2xl",
|
|
147
147
|
className?.includes("border") &&
|
|
148
|
-
|
|
148
|
+
"[&>[data-slot=input-group]]:border-0 [&>[data-slot=input-group]]:bg-transparent [&>[data-slot=input-group]]:shadow-none",
|
|
149
149
|
className
|
|
150
150
|
)}
|
|
151
151
|
{...props}
|
|
@@ -155,15 +155,15 @@ export const PromptInput = React.memo<PromptInputBlockProps>(
|
|
|
155
155
|
placeholder={placeholder}
|
|
156
156
|
disabled={disabled}
|
|
157
157
|
onChange={isControlled ? handleControlledChange : undefined}
|
|
158
|
-
className="min-h-
|
|
158
|
+
className="min-h-12 py-3 pl-3 pr-2 text-sm resize-none border-0 shadow-none focus-visible:ring-0 bg-transparent flex-1 field-sizing-content"
|
|
159
159
|
/>
|
|
160
|
-
<InputGroupAddon align="inline-end" className="self-end pb-
|
|
160
|
+
<InputGroupAddon align="inline-end" className="self-end pb-3 pr-3 pt-3 shrink-0 has-[>button]:mr-0 !mr-0">
|
|
161
161
|
<PromptInputSubmit
|
|
162
162
|
disabled={disabled || (loading && !onStop)}
|
|
163
163
|
status={loading ? (onStop ? "streaming" : "submitted") : undefined}
|
|
164
164
|
onClick={isStopping ? (e: React.MouseEvent) => { e.preventDefault(); onStop?.(); } : undefined}
|
|
165
165
|
className={cn(
|
|
166
|
-
"rounded-lg
|
|
166
|
+
"rounded-lg text-white transition-colors",
|
|
167
167
|
isStopping && "bg-transparent hover:bg-accent"
|
|
168
168
|
)}
|
|
169
169
|
>
|
|
@@ -181,7 +181,7 @@ export const PromptInput = React.memo<PromptInputBlockProps>(
|
|
|
181
181
|
onError={onError}
|
|
182
182
|
className={cn(
|
|
183
183
|
className?.includes("border") &&
|
|
184
|
-
|
|
184
|
+
"[&>[data-slot=input-group]]:border-0 [&>[data-slot=input-group]]:bg-transparent [&>[data-slot=input-group]]:shadow-none",
|
|
185
185
|
className
|
|
186
186
|
)}
|
|
187
187
|
{...props}
|
|
@@ -218,7 +218,7 @@ export const PromptInput = React.memo<PromptInputBlockProps>(
|
|
|
218
218
|
status={loading ? (onStop ? "streaming" : "submitted") : undefined}
|
|
219
219
|
onClick={isStopping ? (e: React.MouseEvent) => { e.preventDefault(); onStop?.(); } : undefined}
|
|
220
220
|
className={cn(
|
|
221
|
-
"rounded-lg
|
|
221
|
+
"rounded-lg text-white transition-colors",
|
|
222
222
|
isStopping && "bg-transparent hover:bg-accent"
|
|
223
223
|
)}
|
|
224
224
|
>
|
|
@@ -17,10 +17,24 @@ export interface ChatSessionInfo {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
import type { FileDownloadResult } from '@/components/composites/FileTreeExplorer';
|
|
20
|
+
import { formatDayLabel } from '@/lib/date';
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
export interface SessionHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
23
24
|
title?: string;
|
|
25
|
+
/**
|
|
26
|
+
* What a session is called.
|
|
27
|
+
* - "title": the session's own title (default).
|
|
28
|
+
* - "date": the day it was created, as "21 Aug 2025", in the reader's time
|
|
29
|
+
* zone. For a surface that keeps one session per day, where the day is
|
|
30
|
+
* what tells sessions apart and a first message is not.
|
|
31
|
+
* @default "title"
|
|
32
|
+
*/
|
|
33
|
+
titleFormat?: 'title' | 'date';
|
|
34
|
+
/**
|
|
35
|
+
* IANA time zone for "date" titles. Defaults to the reader's own.
|
|
36
|
+
*/
|
|
37
|
+
timeZone?: string;
|
|
24
38
|
sessions?: ChatSessionInfo[];
|
|
25
39
|
activeSessionId?: string | null;
|
|
26
40
|
onNewSession?: () => void;
|
|
@@ -48,7 +62,7 @@ export interface SessionHeaderProps extends React.HTMLAttributes<HTMLDivElement>
|
|
|
48
62
|
* and a dropdown history of past sessions.
|
|
49
63
|
*/
|
|
50
64
|
export const SessionHeader = React.forwardRef<HTMLDivElement, SessionHeaderProps>(
|
|
51
|
-
({ title, sessions, activeSessionId, onNewSession, onCloseSession, onSelectSession, onDownloadSession, showActions = true, showNewSession = true, showDownloadSession = true, onOverview, showOverview = true, className, ...props }, ref) => {
|
|
65
|
+
({ title, titleFormat = 'title', timeZone, sessions, activeSessionId, onNewSession, onCloseSession, onSelectSession, onDownloadSession, showActions = true, showNewSession = true, showDownloadSession = true, onOverview, showOverview = true, className, ...props }, ref) => {
|
|
52
66
|
const activeSession = sessions?.find(s => s.id === activeSessionId);
|
|
53
67
|
const downloadRef = React.useRef<HTMLAnchorElement>(null);
|
|
54
68
|
|
|
@@ -66,9 +80,16 @@ export const SessionHeader = React.forwardRef<HTMLDivElement, SessionHeaderProps
|
|
|
66
80
|
URL.revokeObjectURL(url);
|
|
67
81
|
}, [onDownloadSession]);
|
|
68
82
|
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
83
|
+
const byDate = titleFormat === 'date';
|
|
84
|
+
const sessionLabel = (session: ChatSessionInfo | undefined): string => {
|
|
85
|
+
if (!byDate) return session?.title || 'Untitled Session';
|
|
86
|
+
// A session not yet in the list is the one being started now: today's.
|
|
87
|
+
return formatDayLabel(session?.created_at ?? new Date(), { timeZone }) || 'Untitled Session';
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const displayTitle = title || (activeSessionId
|
|
91
|
+
? sessionLabel(activeSession)
|
|
92
|
+
: byDate ? formatDayLabel(new Date(), { timeZone }) : 'New Session');
|
|
72
93
|
|
|
73
94
|
return (
|
|
74
95
|
<div
|
|
@@ -121,8 +142,11 @@ export const SessionHeader = React.forwardRef<HTMLDivElement, SessionHeaderProps
|
|
|
121
142
|
onClick={() => onSelectSession?.(session.id)}
|
|
122
143
|
className="flex flex-col items-start py-2 cursor-pointer"
|
|
123
144
|
>
|
|
124
|
-
<span className="text-sm font-medium truncate w-full">{session
|
|
125
|
-
|
|
145
|
+
<span className="text-sm font-medium truncate w-full">{sessionLabel(session)}</span>
|
|
146
|
+
{/* The date is the title in "date" mode; repeating it underneath says nothing. */}
|
|
147
|
+
{!byDate && (
|
|
148
|
+
<span className="text-xs text-muted-foreground">{new Date(session.created_at).toLocaleString()}</span>
|
|
149
|
+
)}
|
|
126
150
|
</DropdownMenuItem>
|
|
127
151
|
))
|
|
128
152
|
) : (
|
|
@@ -21,31 +21,12 @@ export default meta;
|
|
|
21
21
|
type Story = StoryObj<typeof meta>;
|
|
22
22
|
|
|
23
23
|
const mockStories: StoryItem[] = [
|
|
24
|
-
{
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
id: "story-2",
|
|
31
|
-
title: "Waypoint",
|
|
32
|
-
status: "ready",
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
id: "story-3",
|
|
36
|
-
title: "Design Sys",
|
|
37
|
-
status: "generating",
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
id: "story-4",
|
|
41
|
-
title: "Workspace",
|
|
42
|
-
status: "ready",
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
id: "story-5",
|
|
46
|
-
title: "ZeroOps",
|
|
47
|
-
status: "draft",
|
|
48
|
-
},
|
|
24
|
+
{ id: "story-1", title: "Devin iOS", status: "ready", unread: true },
|
|
25
|
+
{ id: "story-2", title: "Waypoint", status: "ready" },
|
|
26
|
+
{ id: "story-3", title: "Design Sys", status: "working" },
|
|
27
|
+
{ id: "story-4", title: "Workspace", status: "working", unread: true },
|
|
28
|
+
{ id: "story-5", title: "Render", status: "failed", unread: true },
|
|
29
|
+
{ id: "story-6", title: "ZeroOps", status: "stopped" },
|
|
49
30
|
];
|
|
50
31
|
|
|
51
32
|
/**
|
|
@@ -13,7 +13,20 @@ export interface StoryItem {
|
|
|
13
13
|
subtitle?: string;
|
|
14
14
|
icon?: string;
|
|
15
15
|
time?: string;
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* The video's state, drawn as the story's colour:
|
|
18
|
+
* - "failed": red — the video has stopped on an error
|
|
19
|
+
* - "ready": green — the finished video is available
|
|
20
|
+
* - "stopped": grey — not ready, and nothing is running to finish it
|
|
21
|
+
* - "working": orange — in progress
|
|
22
|
+
* "generating" and "draft" are the earlier names of "working" and "stopped".
|
|
23
|
+
*/
|
|
24
|
+
status?: "failed" | "ready" | "stopped" | "working" | "generating" | "draft";
|
|
25
|
+
/**
|
|
26
|
+
* Whether the story holds a notification the user has not seen. Shown as the
|
|
27
|
+
* badge, on any status, and only then.
|
|
28
|
+
*/
|
|
29
|
+
unread?: boolean;
|
|
17
30
|
thumbnailUrl?: string;
|
|
18
31
|
avatarColor?: string;
|
|
19
32
|
avatarInitials?: string;
|
|
@@ -28,22 +41,17 @@ export interface StoriesBarProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
28
41
|
|
|
29
42
|
function getStoryStatusStyle(status?: StoryItem["status"]) {
|
|
30
43
|
switch (status) {
|
|
31
|
-
case "
|
|
32
|
-
return {
|
|
33
|
-
iconColor: "text-amber-500",
|
|
34
|
-
badgeBg: "bg-amber-500",
|
|
35
|
-
};
|
|
44
|
+
case "failed":
|
|
45
|
+
return { iconColor: "text-red-500", badgeBg: "bg-red-500" };
|
|
36
46
|
case "ready":
|
|
37
|
-
return {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
};
|
|
47
|
+
return { iconColor: "text-emerald-500", badgeBg: "bg-emerald-500" };
|
|
48
|
+
case "working":
|
|
49
|
+
case "generating":
|
|
50
|
+
return { iconColor: "text-amber-500", badgeBg: "bg-amber-500" };
|
|
51
|
+
case "stopped":
|
|
41
52
|
case "draft":
|
|
42
53
|
default:
|
|
43
|
-
return {
|
|
44
|
-
iconColor: "text-muted-foreground",
|
|
45
|
-
badgeBg: "bg-muted-foreground",
|
|
46
|
-
};
|
|
54
|
+
return { iconColor: "text-muted-foreground", badgeBg: "bg-muted-foreground" };
|
|
47
55
|
}
|
|
48
56
|
}
|
|
49
57
|
|
|
@@ -61,7 +69,6 @@ export const StoriesBar = React.forwardRef<HTMLDivElement, StoriesBarProps>(
|
|
|
61
69
|
<div className="flex items-center gap-2.5 px-3 py-1.5 overflow-x-auto no-scrollbar scroll-smooth">
|
|
62
70
|
{stories.map((story) => {
|
|
63
71
|
const isActive = story.id === activeStoryId;
|
|
64
|
-
const isGenerating = story.status === "generating";
|
|
65
72
|
const statusStyle = getStoryStatusStyle(story.status);
|
|
66
73
|
const iconName = story.icon || defaultIcon;
|
|
67
74
|
|
|
@@ -100,17 +107,15 @@ export const StoriesBar = React.forwardRef<HTMLDivElement, StoriesBarProps>(
|
|
|
100
107
|
)}
|
|
101
108
|
</div>
|
|
102
109
|
|
|
103
|
-
{
|
|
104
|
-
|
|
105
|
-
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-amber-500 opacity-75" />
|
|
106
|
-
<span className="relative inline-flex rounded-full h-2 w-2 bg-amber-500" />
|
|
107
|
-
</span>
|
|
108
|
-
) : (
|
|
110
|
+
{/* The badge: an unseen notification, on any status. */}
|
|
111
|
+
{story.unread && (
|
|
109
112
|
<span
|
|
110
113
|
className={cn(
|
|
111
114
|
"absolute bottom-0 right-0 inline-flex rounded-full h-2 w-2 ring-1.5 ring-background",
|
|
112
115
|
statusStyle.badgeBg
|
|
113
116
|
)}
|
|
117
|
+
data-testid={`story-badge-${story.id}`}
|
|
118
|
+
aria-label="New notification"
|
|
114
119
|
/>
|
|
115
120
|
)}
|
|
116
121
|
</div>
|
|
@@ -95,6 +95,8 @@ export interface ChatPanelProps {
|
|
|
95
95
|
* Handler to edit HITL approval request arguments
|
|
96
96
|
*/
|
|
97
97
|
onApprovalEdit?: (editedArgs: Record<string, unknown>) => void;
|
|
98
|
+
/** Opens the file a pending question is about (its `file_path`). The card shows the link only when this is given. */
|
|
99
|
+
onOpenFile?: (path: string) => void;
|
|
98
100
|
/**
|
|
99
101
|
* Processing state for HITL approval request
|
|
100
102
|
*/
|
|
@@ -220,6 +222,17 @@ export interface ChatPanelProps {
|
|
|
220
222
|
* Title override for the session header
|
|
221
223
|
*/
|
|
222
224
|
sessionTitle?: string;
|
|
225
|
+
/**
|
|
226
|
+
* What sessions are called in the header and the history list: their own
|
|
227
|
+
* title ("title", default) or the day they were created ("date", shown as
|
|
228
|
+
* "21 Aug 2025" in the reader's time zone).
|
|
229
|
+
* @default "title"
|
|
230
|
+
*/
|
|
231
|
+
sessionTitleFormat?: 'title' | 'date';
|
|
232
|
+
/**
|
|
233
|
+
* IANA time zone for "date" session titles. Defaults to the reader's own.
|
|
234
|
+
*/
|
|
235
|
+
sessionTimeZone?: string;
|
|
223
236
|
/**
|
|
224
237
|
* Whether to show action buttons in the session header
|
|
225
238
|
* @default true
|
|
@@ -273,6 +286,7 @@ export const ChatPanel = React.memo<ChatPanelProps>(
|
|
|
273
286
|
onApprovalApprove,
|
|
274
287
|
onApprovalReject,
|
|
275
288
|
onApprovalEdit,
|
|
289
|
+
onOpenFile,
|
|
276
290
|
isApprovalProcessing = false,
|
|
277
291
|
loading = false,
|
|
278
292
|
onStop,
|
|
@@ -304,6 +318,8 @@ export const ChatPanel = React.memo<ChatPanelProps>(
|
|
|
304
318
|
enableSpeech = true,
|
|
305
319
|
showHeader = true,
|
|
306
320
|
sessionTitle,
|
|
321
|
+
sessionTitleFormat = 'title',
|
|
322
|
+
sessionTimeZone,
|
|
307
323
|
showSessionActions = true,
|
|
308
324
|
showNewSession = true,
|
|
309
325
|
showDownloadSession = true,
|
|
@@ -433,6 +449,7 @@ export const ChatPanel = React.memo<ChatPanelProps>(
|
|
|
433
449
|
isProcessing={isApprovalProcessing}
|
|
434
450
|
state={approvalCardState}
|
|
435
451
|
approval={approvalCardApproval}
|
|
452
|
+
onOpenFile={onOpenFile}
|
|
436
453
|
/>
|
|
437
454
|
</div>
|
|
438
455
|
);
|
|
@@ -468,6 +485,8 @@ export const ChatPanel = React.memo<ChatPanelProps>(
|
|
|
468
485
|
{showHeader && (
|
|
469
486
|
<SessionHeader
|
|
470
487
|
title={sessionTitle}
|
|
488
|
+
titleFormat={sessionTitleFormat}
|
|
489
|
+
timeZone={sessionTimeZone}
|
|
471
490
|
showActions={showSessionActions}
|
|
472
491
|
showNewSession={showNewSession}
|
|
473
492
|
showDownloadSession={showDownloadSession}
|
package/components/index.ts
CHANGED
|
@@ -94,6 +94,9 @@ export type { ExternalToast, ToastT, ToasterProps } from 'sonner';
|
|
|
94
94
|
|
|
95
95
|
// Utilities
|
|
96
96
|
export { cn } from '@/lib/utils';
|
|
97
|
+
// Calendar days in the reader's time zone
|
|
98
|
+
export { formatDayLabel, dayKey, isSameDay, userTimeZone } from '@/lib/date';
|
|
99
|
+
export type { DayOptions } from '@/lib/date';
|
|
97
100
|
export { ButtonSwitcher } from './composites';
|
|
98
101
|
export type { ButtonSwitcherProps, ButtonSwitcherItem } from './composites';
|
|
99
102
|
|