ai-design-system 0.1.91 → 0.1.93
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/blocks/AIConversation/AIConversation.tsx +49 -1
- package/components/blocks/WorkflowCanvas/WorkflowCanvas.tsx +0 -79
- package/components/composites/FilePreviewDialog/FilePreviewDialog.stories.tsx +23 -1
- package/components/composites/OrchestratorMessage/OrchestratorMessage.tsx +51 -2
- package/components/composites/ProjectSwitcher/ProjectSwitcher.stories.tsx +20 -1
- package/components/composites/ProjectSwitcher/ProjectSwitcher.tsx +7 -1
- package/components/composites/PromptInput/PromptInput.stories.tsx +63 -0
- package/components/composites/PromptInput/PromptInput.tsx +77 -0
- package/components/composites/PromptInput/index.ts +27 -1
- package/components/composites/TaskQueue/TaskQueue.stories.tsx +241 -0
- package/components/composites/TaskQueue/TaskQueue.tsx +270 -0
- package/components/composites/TaskQueue/index.ts +2 -0
- package/components/composites/TaskQueue/interfaces.ts +103 -0
- package/components/composites/UserMessage/UserMessage.tsx +40 -13
- package/components/composites/WorkflowToolbar/WorkflowToolbar.tsx +23 -1
- package/components/composites/WorkflowToolbar/interfaces.ts +14 -0
- package/components/composites/WorkspaceEmptyState/WorkspaceEmptyState.stories.tsx +38 -0
- package/components/composites/WorkspaceEmptyState/WorkspaceEmptyState.tsx +60 -0
- package/components/composites/WorkspaceEmptyState/index.ts +2 -0
- package/components/composites/index.ts +29 -0
- package/components/features/ChatPanel/ChatPanel.tsx +18 -1
- package/components/features/TextEditor/TextEditor.tsx +43 -2
- package/components/features/TextEditor/index.ts +1 -0
- package/components/features/index.ts +1 -1
- package/components/index.ts +25 -0
- package/dist/index.cjs +838 -84
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +68 -0
- package/dist/index.d.ts +369 -138
- package/dist/index.js +828 -85
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -56,6 +56,7 @@ interface AIMessage {
|
|
|
56
56
|
isLoading?: boolean;
|
|
57
57
|
blocks?: AIMessageBlock[];
|
|
58
58
|
checkpointId?: string;
|
|
59
|
+
restoreLabel?: string;
|
|
59
60
|
attachments?: UserMessageAttachment[];
|
|
60
61
|
}
|
|
61
62
|
|
|
@@ -98,6 +99,17 @@ export interface AIConversationProps
|
|
|
98
99
|
* Callback fired when user restores a conversation checkpoint
|
|
99
100
|
*/
|
|
100
101
|
onRestoreCheckpoint?: (messageId: string, checkpointId: string) => void
|
|
102
|
+
/**
|
|
103
|
+
* Save the workspace as it stands after an orchestrator reply.
|
|
104
|
+
*
|
|
105
|
+
* Paired with `onRestoreCheckpoint`: undo sits on what the user asked for,
|
|
106
|
+
* save sits on what the agent produced. Omit it and no reply offers to save.
|
|
107
|
+
*/
|
|
108
|
+
onSaveWorkspace?: () => void
|
|
109
|
+
/** True when the current version is already kept — every Save reads as saved. */
|
|
110
|
+
isCurrentVersionSaved?: boolean
|
|
111
|
+
/** A save is in flight — the control says so and refuses a second one. */
|
|
112
|
+
isSavingWorkspace?: boolean
|
|
101
113
|
}
|
|
102
114
|
|
|
103
115
|
/**
|
|
@@ -113,6 +125,9 @@ export const AIConversation = React.memo<AIConversationProps>(
|
|
|
113
125
|
onToolAction,
|
|
114
126
|
renderSystemMessage,
|
|
115
127
|
onRestoreCheckpoint,
|
|
128
|
+
onSaveWorkspace,
|
|
129
|
+
isCurrentVersionSaved,
|
|
130
|
+
isSavingWorkspace,
|
|
116
131
|
...conversationProps
|
|
117
132
|
}) => {
|
|
118
133
|
const isEmpty = React.useMemo(
|
|
@@ -242,6 +257,7 @@ export const AIConversation = React.memo<AIConversationProps>(
|
|
|
242
257
|
subAgents: msg.subAgents ? [...msg.subAgents] : undefined,
|
|
243
258
|
blocks: msg.blocks ? [...msg.blocks] : undefined,
|
|
244
259
|
checkpointId: msg.checkpointId,
|
|
260
|
+
restoreLabel: msg.restoreLabel,
|
|
245
261
|
};
|
|
246
262
|
result.push(currentGroup);
|
|
247
263
|
}
|
|
@@ -257,6 +273,27 @@ export const AIConversation = React.memo<AIConversationProps>(
|
|
|
257
273
|
return result;
|
|
258
274
|
}, [messages]);
|
|
259
275
|
|
|
276
|
+
/**
|
|
277
|
+
* The last orchestrator reply, and the ONLY one that may offer to save.
|
|
278
|
+
*
|
|
279
|
+
* Saving anchors to the most recent request, so it captures the workspace as
|
|
280
|
+
* it is NOW — not as it was when some earlier reply was written. Offering the
|
|
281
|
+
* control on every reply implies each one saves its own moment, which is
|
|
282
|
+
* false and unrecoverable once acted on: the user would believe they had kept
|
|
283
|
+
* a point they never kept.
|
|
284
|
+
*
|
|
285
|
+
* `isCurrentVersionSaved` is likewise one fact about the workspace, not a
|
|
286
|
+
* property of each message. Rendered on every reply it read as "all of these
|
|
287
|
+
* are saved"; it belongs on the single reply the save would come from.
|
|
288
|
+
*/
|
|
289
|
+
const lastReplyIndex = React.useMemo(() => {
|
|
290
|
+
for (let i = groupedMessages.length - 1; i >= 0; i--) {
|
|
291
|
+
const m = groupedMessages[i]
|
|
292
|
+
if (m.role === "orchestrator" || m.role === "assistant" || (!m.role && m.type === "ai")) return i
|
|
293
|
+
}
|
|
294
|
+
return -1
|
|
295
|
+
}, [groupedMessages])
|
|
296
|
+
|
|
260
297
|
const renderedMessages = React.useMemo(
|
|
261
298
|
() =>
|
|
262
299
|
groupedMessages.map((message, index) => {
|
|
@@ -291,6 +328,7 @@ export const AIConversation = React.memo<AIConversationProps>(
|
|
|
291
328
|
avatarName: message.avatarName,
|
|
292
329
|
attachments: message.attachments,
|
|
293
330
|
checkpointId: message.checkpointId,
|
|
331
|
+
restoreLabel: message.restoreLabel,
|
|
294
332
|
}}
|
|
295
333
|
showAvatar={showAvatars}
|
|
296
334
|
onRestore={onRestoreCheckpoint}
|
|
@@ -335,6 +373,16 @@ export const AIConversation = React.memo<AIConversationProps>(
|
|
|
335
373
|
isLoading: message.isLoading && !hasReasoning,
|
|
336
374
|
}}
|
|
337
375
|
showAvatar={showAvatars && hasDisplayContent}
|
|
376
|
+
// Only on a settled reply with something to read: a save
|
|
377
|
+
// offered beside a streaming or empty message would anchor a
|
|
378
|
+
// snapshot to a turn that has not produced anything yet.
|
|
379
|
+
onSave={
|
|
380
|
+
index === lastReplyIndex && hasDisplayContent && !isStreaming
|
|
381
|
+
? onSaveWorkspace
|
|
382
|
+
: undefined
|
|
383
|
+
}
|
|
384
|
+
isSaved={index === lastReplyIndex && isCurrentVersionSaved}
|
|
385
|
+
isSaving={index === lastReplyIndex && isSavingWorkspace}
|
|
338
386
|
>
|
|
339
387
|
{/* Render reasoning-section for hidden tool results and subagents */}
|
|
340
388
|
{hasReasoning && (
|
|
@@ -425,7 +473,7 @@ export const AIConversation = React.memo<AIConversationProps>(
|
|
|
425
473
|
</React.Fragment>
|
|
426
474
|
)
|
|
427
475
|
}),
|
|
428
|
-
[groupedMessages, showAvatars, onToolAction, onRestoreCheckpoint]
|
|
476
|
+
[groupedMessages, showAvatars, onToolAction, onRestoreCheckpoint, onSaveWorkspace, isCurrentVersionSaved, isSavingWorkspace, lastReplyIndex]
|
|
429
477
|
)
|
|
430
478
|
|
|
431
479
|
return (
|
|
@@ -6,8 +6,6 @@ import {
|
|
|
6
6
|
type Connection,
|
|
7
7
|
type Node,
|
|
8
8
|
useReactFlow,
|
|
9
|
-
useStoreApi,
|
|
10
|
-
useUpdateNodeInternals,
|
|
11
9
|
} from "@xyflow/react";
|
|
12
10
|
import { useCallback, useEffect } from "react";
|
|
13
11
|
import { Canvas } from "@/components/ai-elements/canvas";
|
|
@@ -24,22 +22,6 @@ import { AgentAnnotation } from "@/components/composites/AgentAnnotation";
|
|
|
24
22
|
import type { WorkflowCanvasProps, WorkflowEdge } from "@/lib/workflow/interfaces";
|
|
25
23
|
import "@xyflow/react/dist/style.css";
|
|
26
24
|
|
|
27
|
-
// Bounded retry for node measurement — see the effect in WorkflowCanvasInner.
|
|
28
|
-
//
|
|
29
|
-
// ~30s, which sounds long and is not. The loop stops the instant every node
|
|
30
|
-
// reports handleBounds, so in the ordinary case it ends after one pass; the cap
|
|
31
|
-
// only bounds the pathological case where a node never appears at all.
|
|
32
|
-
//
|
|
33
|
-
// It was 3s, and that was too short for the case this exists to fix. The grid's
|
|
34
|
-
// device nodes each carry an iframe, and on a cold sandbox they are not in the
|
|
35
|
-
// DOM until Metro has served a bundle. `useUpdateNodeInternals` silently drops
|
|
36
|
-
// every id it cannot resolve, so a window that closes before the iframes mount
|
|
37
|
-
// spends all its attempts on nothing and gives up — leaving exactly the blank
|
|
38
|
-
// canvas of edgeless nodes it was added to prevent. Verified live: all five
|
|
39
|
-
// nodes resolvable, all five still `handleBounds: NULL`.
|
|
40
|
-
const MEASURE_RETRY_MS = 250;
|
|
41
|
-
const MEASURE_MAX_ATTEMPTS = 120;
|
|
42
|
-
|
|
43
25
|
const edgeTypes = {
|
|
44
26
|
straight: Edge.Strict,
|
|
45
27
|
animated: Edge.Animated,
|
|
@@ -76,67 +58,6 @@ function WorkflowCanvasInner({
|
|
|
76
58
|
className,
|
|
77
59
|
}: WorkflowCanvasProps) {
|
|
78
60
|
const { fitView } = useReactFlow();
|
|
79
|
-
const updateNodeInternals = useUpdateNodeInternals();
|
|
80
|
-
const store = useStoreApi();
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Force xyflow to measure the nodes it is showing.
|
|
84
|
-
*
|
|
85
|
-
* xyflow will not draw an edge until BOTH endpoints have `handleBounds` —
|
|
86
|
-
* the measured position of each handle within its node — and those come from
|
|
87
|
-
* one place only: the per-node ResizeObserver. Publishing `initialWidth` /
|
|
88
|
-
* `initialHeight` (which the preview nodes do) satisfies the separate
|
|
89
|
-
* `nodeHasDimensions` check that controls VISIBILITY, and does nothing for
|
|
90
|
-
* `handleBounds`. The two are easy to conflate and fail very differently.
|
|
91
|
-
*
|
|
92
|
-
* When that observer does not deliver — it fires as an unsynchronized
|
|
93
|
-
* callback (see the fitView effect below, which works around the same race),
|
|
94
|
-
* and reports nothing for a node that is re-created but never changes size —
|
|
95
|
-
* the result is not a missing node. It is a canvas of perfectly normal nodes
|
|
96
|
-
* with EVERY edge silently absent: `edgeLookup` holds them all, each with
|
|
97
|
-
* valid endpoints and handle ids, and not one is rendered. Observed live with
|
|
98
|
-
* all five nodes at `measured: {}` and `handleBounds: null` while their DOM
|
|
99
|
-
* elements had real size, and xyflow logged no warning.
|
|
100
|
-
*
|
|
101
|
-
* Keyed on the node ids rather than the `nodes` array, which is a new
|
|
102
|
-
* reference on nearly every render.
|
|
103
|
-
*
|
|
104
|
-
* RETRIED, not fired once, because the request is silently dropped when it
|
|
105
|
-
* is early. `useUpdateNodeInternals` resolves each id through
|
|
106
|
-
* `domNode.querySelector('.react-flow__node[data-id=...]')` and keeps only
|
|
107
|
-
* the ones it finds — a node that has not mounted yet is skipped with no
|
|
108
|
-
* error and no retry, and since the id list has not changed, the effect
|
|
109
|
-
* never runs again. One shot at 60ms worked for the single-device view and
|
|
110
|
-
* missed the grid every time, where five nodes carrying iframes mount later.
|
|
111
|
-
* So it repeats until the store actually reports `handleBounds`, and stops
|
|
112
|
-
* the moment it does.
|
|
113
|
-
*/
|
|
114
|
-
const nodeIdSignature = nodes.map((n) => n.id).join("|");
|
|
115
|
-
useEffect(() => {
|
|
116
|
-
if (!nodeIdSignature) return;
|
|
117
|
-
const ids = nodeIdSignature.split("|");
|
|
118
|
-
let attempts = 0;
|
|
119
|
-
let timer: ReturnType<typeof setTimeout>;
|
|
120
|
-
|
|
121
|
-
const measured = () => {
|
|
122
|
-
const lookup = store.getState().nodeLookup;
|
|
123
|
-
return ids.every((id) => !!lookup.get(id)?.internals?.handleBounds);
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
const pass = () => {
|
|
127
|
-
updateNodeInternals(ids);
|
|
128
|
-
attempts += 1;
|
|
129
|
-
// `updateNodeInternals` defers its own work to a rAF, so the result is
|
|
130
|
-
// only readable on a later tick — hence check on the next timer rather
|
|
131
|
-
// than immediately after the call.
|
|
132
|
-
timer = setTimeout(() => {
|
|
133
|
-
if (!measured() && attempts < MEASURE_MAX_ATTEMPTS) pass();
|
|
134
|
-
}, MEASURE_RETRY_MS);
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
timer = setTimeout(pass, 60);
|
|
138
|
-
return () => clearTimeout(timer);
|
|
139
|
-
}, [nodeIdSignature, updateNodeInternals, store]);
|
|
140
61
|
|
|
141
62
|
// Opt-in re-fit, e.g. after the device-preview toolbar's Route select
|
|
142
63
|
// switches to "All" and the node count/extent changes underneath the
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react'
|
|
2
|
+
import { fn } from '@storybook/test'
|
|
2
3
|
import { FilePreviewDialog } from './FilePreviewDialog'
|
|
3
4
|
|
|
4
5
|
const meta = {
|
|
@@ -14,5 +15,26 @@ export default meta
|
|
|
14
15
|
type Story = StoryObj<typeof meta>
|
|
15
16
|
|
|
16
17
|
export const Default: Story = {
|
|
17
|
-
args: {
|
|
18
|
+
args: {
|
|
19
|
+
open: true,
|
|
20
|
+
onOpenChange: fn(),
|
|
21
|
+
title: 'Sprint checklist',
|
|
22
|
+
description: 'specs/sprint-plan.md',
|
|
23
|
+
content: [
|
|
24
|
+
'## Tasks',
|
|
25
|
+
'- [x] Draft API contract',
|
|
26
|
+
'- [ ] Land UI stubs',
|
|
27
|
+
'- [ ] Wire preview',
|
|
28
|
+
].join('\n'),
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const Empty: Story = {
|
|
33
|
+
args: {
|
|
34
|
+
open: true,
|
|
35
|
+
onOpenChange: fn(),
|
|
36
|
+
title: 'Untitled spec',
|
|
37
|
+
description: 'specs/untitled.md',
|
|
38
|
+
content: undefined,
|
|
39
|
+
},
|
|
18
40
|
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
MessageTypingIndicator,
|
|
12
12
|
} from "@/components/ai-elements/message"
|
|
13
13
|
import { Response } from "@/components/composites/response"
|
|
14
|
+
import { cn } from "@/lib/utils"
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* OrchestratorMessage Block
|
|
@@ -36,6 +37,23 @@ export interface OrchestratorMessageProps {
|
|
|
36
37
|
* Whether to show avatar
|
|
37
38
|
*/
|
|
38
39
|
showAvatar?: boolean
|
|
40
|
+
/**
|
|
41
|
+
* Save the workspace as it stands after this reply.
|
|
42
|
+
*
|
|
43
|
+
* Sits here and not only in the toolbar because a snapshot is a point in the
|
|
44
|
+
* CONVERSATION, not a global act: the reply is what tells the user something
|
|
45
|
+
* is worth keeping, so the control belongs where they read that. It pairs
|
|
46
|
+
* with the undo control on user messages — save beside what the agent
|
|
47
|
+
* produced, undo beside what you asked for.
|
|
48
|
+
*
|
|
49
|
+
* Absent when there is nothing to save to, so the control simply does not
|
|
50
|
+
* appear rather than offering an action that cannot work.
|
|
51
|
+
*/
|
|
52
|
+
onSave?: () => void
|
|
53
|
+
/** True once this version is kept — the control says so instead of repeating. */
|
|
54
|
+
isSaved?: boolean
|
|
55
|
+
/** A save is in flight. Refuses a second press: two presses is two snapshots. */
|
|
56
|
+
isSaving?: boolean
|
|
39
57
|
/**
|
|
40
58
|
* Child components (SpecialistMessage, ToolCallDisplay)
|
|
41
59
|
*/
|
|
@@ -46,14 +64,14 @@ export interface OrchestratorMessageProps {
|
|
|
46
64
|
* OrchestratorMessage component - displays coordinator messages with nested specialists and tools
|
|
47
65
|
*/
|
|
48
66
|
export const OrchestratorMessage = React.memo<OrchestratorMessageProps>(
|
|
49
|
-
({ message, showAvatar = true, children }) => {
|
|
67
|
+
({ message, showAvatar = true, children, onSave, isSaved, isSaving }) => {
|
|
50
68
|
const hasContent = React.useMemo(
|
|
51
69
|
() => message.content && message.content.trim() !== "",
|
|
52
70
|
[message.content]
|
|
53
71
|
)
|
|
54
72
|
|
|
55
73
|
return (
|
|
56
|
-
<Message from="assistant">
|
|
74
|
+
<Message from="assistant" className="group">
|
|
57
75
|
{showAvatar && (message.avatarSrc
|
|
58
76
|
? <MessageAvatar
|
|
59
77
|
src={message.avatarSrc}
|
|
@@ -80,6 +98,37 @@ export const OrchestratorMessage = React.memo<OrchestratorMessageProps>(
|
|
|
80
98
|
|
|
81
99
|
{/* Child components (specialists and tool calls) */}
|
|
82
100
|
{children && <div className="mt-4 space-y-4">{children}</div>}
|
|
101
|
+
|
|
102
|
+
{/* Revealed on hover or keyboard focus, like the undo control on a
|
|
103
|
+
user message: keeping a version is deliberate and occasional, and
|
|
104
|
+
a button standing on every reply reads as something you are
|
|
105
|
+
expected to press. Hidden entirely while streaming — there is no
|
|
106
|
+
settled version to keep yet. */}
|
|
107
|
+
{onSave && !message.isLoading && (
|
|
108
|
+
<button
|
|
109
|
+
type="button"
|
|
110
|
+
aria-label={isSaved ? "This version is already saved" : "Save this version"}
|
|
111
|
+
title={isSaved ? "This version is already saved" : "Save this version"}
|
|
112
|
+
disabled={isSaved || isSaving}
|
|
113
|
+
onClick={onSave}
|
|
114
|
+
className={cn(
|
|
115
|
+
"mt-2 inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-xs",
|
|
116
|
+
"text-muted-foreground transition-opacity",
|
|
117
|
+
"hover:bg-accent hover:text-foreground",
|
|
118
|
+
"focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
119
|
+
isSaved || isSaving ? "opacity-100" : "opacity-0 group-hover:opacity-100",
|
|
120
|
+
"disabled:pointer-events-none"
|
|
121
|
+
)}
|
|
122
|
+
>
|
|
123
|
+
<Icon
|
|
124
|
+
name={isSaved ? "check" : isSaving ? "loader-2" : "save"}
|
|
125
|
+
size="sm"
|
|
126
|
+
className={isSaving ? "animate-spin" : undefined}
|
|
127
|
+
aria-hidden
|
|
128
|
+
/>
|
|
129
|
+
{isSaved ? "Saved" : isSaving ? "Saving…" : "Save"}
|
|
130
|
+
</button>
|
|
131
|
+
)}
|
|
83
132
|
</div>
|
|
84
133
|
</Message>
|
|
85
134
|
)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react'
|
|
2
|
+
import { fn } from '@storybook/test'
|
|
2
3
|
import { ProjectSwitcher } from './ProjectSwitcher'
|
|
3
4
|
|
|
4
5
|
const meta = {
|
|
@@ -14,5 +15,23 @@ export default meta
|
|
|
14
15
|
type Story = StoryObj<typeof meta>
|
|
15
16
|
|
|
16
17
|
export const Default: Story = {
|
|
17
|
-
args: {
|
|
18
|
+
args: {
|
|
19
|
+
projects: [
|
|
20
|
+
{ id: '1', name: 'Atlas' },
|
|
21
|
+
{ id: '2', name: 'Beacon' },
|
|
22
|
+
{ id: '3', name: 'Cascade' },
|
|
23
|
+
],
|
|
24
|
+
selectedProjectId: '1',
|
|
25
|
+
onSelectProject: fn(),
|
|
26
|
+
onCreateProject: fn(),
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const Empty: Story = {
|
|
31
|
+
args: {
|
|
32
|
+
projects: [],
|
|
33
|
+
selectedProjectId: null,
|
|
34
|
+
onSelectProject: fn(),
|
|
35
|
+
onCreateProject: fn(),
|
|
36
|
+
},
|
|
18
37
|
}
|
|
@@ -31,7 +31,13 @@ export interface ProjectSwitcherProps {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
export const ProjectSwitcher = React.memo<ProjectSwitcherProps>(
|
|
34
|
-
({
|
|
34
|
+
({
|
|
35
|
+
projects = [],
|
|
36
|
+
selectedProjectId,
|
|
37
|
+
onSelectProject = () => {},
|
|
38
|
+
onCreateProject = () => {},
|
|
39
|
+
className,
|
|
40
|
+
}) => {
|
|
35
41
|
const [open, setOpen] = React.useState(false)
|
|
36
42
|
|
|
37
43
|
const selectedProject = projects.find((p) => String(p.id) === String(selectedProjectId))
|
|
@@ -273,3 +273,66 @@ export const ToggleDialog: Story = {
|
|
|
273
273
|
},
|
|
274
274
|
},
|
|
275
275
|
};
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* With Context Window
|
|
279
|
+
* Displays context token consumption, progress ring, breakdown, and estimated cost
|
|
280
|
+
*/
|
|
281
|
+
export const WithContextWindow: Story = {
|
|
282
|
+
args: {
|
|
283
|
+
placeholder: "Ask a question about the codebase...",
|
|
284
|
+
context: {
|
|
285
|
+
usedTokens: 42500,
|
|
286
|
+
maxTokens: 128000,
|
|
287
|
+
modelId: "gpt-4o",
|
|
288
|
+
usage: {
|
|
289
|
+
inputTokens: 38000,
|
|
290
|
+
outputTokens: 3500,
|
|
291
|
+
reasoningTokens: 1000,
|
|
292
|
+
cachedInputTokens: 15000,
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
onSubmit: (message) => {
|
|
296
|
+
console.log("Submitted:", message);
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
parameters: {
|
|
300
|
+
docs: {
|
|
301
|
+
description: {
|
|
302
|
+
story:
|
|
303
|
+
"Displays context window usage via the AI Elements Context compound component in the toolbar. Shows token usage percentage, circular progress, input/output/reasoning/cache breakdown, and estimated USD cost on hover.",
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* With Near-Limit Context Window
|
|
311
|
+
* Demonstrates high context utilization nearing the maximum context window
|
|
312
|
+
*/
|
|
313
|
+
export const WithNearLimitContext: Story = {
|
|
314
|
+
args: {
|
|
315
|
+
placeholder: "Ask a question with high context utilization...",
|
|
316
|
+
context: {
|
|
317
|
+
usedTokens: 118500,
|
|
318
|
+
maxTokens: 128000,
|
|
319
|
+
modelId: "gpt-4o",
|
|
320
|
+
usage: {
|
|
321
|
+
inputTokens: 112000,
|
|
322
|
+
outputTokens: 6500,
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
onSubmit: (message) => {
|
|
326
|
+
console.log("Submitted:", message);
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
parameters: {
|
|
330
|
+
docs: {
|
|
331
|
+
description: {
|
|
332
|
+
story:
|
|
333
|
+
"Demonstrates context window indicator when the model is near its token capacity (92.6% utilized).",
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import type { ReactNode } from "react";
|
|
5
|
+
import type { LanguageModelUsage } from "ai";
|
|
5
6
|
import {
|
|
6
7
|
PromptInput as AIPromptInput,
|
|
7
8
|
PromptInputAttachment,
|
|
@@ -18,10 +19,29 @@ import {
|
|
|
18
19
|
usePromptInputController,
|
|
19
20
|
useOptionalPromptInputController,
|
|
20
21
|
} from "@/components/ai-elements/prompt-input";
|
|
22
|
+
import {
|
|
23
|
+
Context,
|
|
24
|
+
ContextTrigger,
|
|
25
|
+
ContextContent,
|
|
26
|
+
ContextContentHeader,
|
|
27
|
+
ContextContentBody,
|
|
28
|
+
ContextContentFooter,
|
|
29
|
+
ContextInputUsage,
|
|
30
|
+
ContextOutputUsage,
|
|
31
|
+
ContextReasoningUsage,
|
|
32
|
+
ContextCacheUsage,
|
|
33
|
+
} from "@/components/ai-elements/context";
|
|
21
34
|
import type { FormEvent } from "react";
|
|
22
35
|
import { Button } from "@/components/primitives/Button";
|
|
23
36
|
import { Icon } from "@/components/primitives/Icon";
|
|
24
37
|
|
|
38
|
+
export interface PromptInputContextProps {
|
|
39
|
+
usedTokens: number;
|
|
40
|
+
maxTokens: number;
|
|
41
|
+
usage?: LanguageModelUsage;
|
|
42
|
+
modelId?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
25
45
|
export interface PromptInputBlockProps
|
|
26
46
|
extends Omit<
|
|
27
47
|
AIPromptInputProps,
|
|
@@ -38,6 +58,8 @@ export interface PromptInputBlockProps
|
|
|
38
58
|
dialog?: ReactNode;
|
|
39
59
|
loading?: boolean;
|
|
40
60
|
onStop?: () => void;
|
|
61
|
+
context?: PromptInputContextProps | ReactNode;
|
|
62
|
+
tools?: ReactNode;
|
|
41
63
|
}
|
|
42
64
|
|
|
43
65
|
export const PromptInput = React.memo<PromptInputBlockProps>(
|
|
@@ -50,6 +72,8 @@ export const PromptInput = React.memo<PromptInputBlockProps>(
|
|
|
50
72
|
dialog,
|
|
51
73
|
loading = false,
|
|
52
74
|
onStop,
|
|
75
|
+
context,
|
|
76
|
+
tools,
|
|
53
77
|
accept = "image/*",
|
|
54
78
|
multiple = true,
|
|
55
79
|
maxFiles,
|
|
@@ -111,6 +135,13 @@ export const PromptInput = React.memo<PromptInputBlockProps>(
|
|
|
111
135
|
<PromptInputFooter>
|
|
112
136
|
<PromptInputTools>
|
|
113
137
|
<AttachButton disabled={disabled || loading} />
|
|
138
|
+
{tools}
|
|
139
|
+
{context ? (
|
|
140
|
+
<PromptInputContextIndicator
|
|
141
|
+
context={context}
|
|
142
|
+
disabled={disabled || loading}
|
|
143
|
+
/>
|
|
144
|
+
) : null}
|
|
114
145
|
</PromptInputTools>
|
|
115
146
|
<PromptInputSubmit
|
|
116
147
|
disabled={disabled || (loading && !onStop)}
|
|
@@ -187,4 +218,50 @@ function ExternalValueSync({ value }: { value?: string }) {
|
|
|
187
218
|
return null;
|
|
188
219
|
}
|
|
189
220
|
|
|
221
|
+
/**
|
|
222
|
+
* Renders the context window usage indicator.
|
|
223
|
+
* Supports either a PromptInputContextProps object (auto-rendered using Context compound elements)
|
|
224
|
+
* or a custom ReactNode.
|
|
225
|
+
*/
|
|
226
|
+
function PromptInputContextIndicator({
|
|
227
|
+
context,
|
|
228
|
+
disabled,
|
|
229
|
+
}: {
|
|
230
|
+
context: PromptInputContextProps | ReactNode;
|
|
231
|
+
disabled?: boolean;
|
|
232
|
+
}) {
|
|
233
|
+
if (React.isValidElement(context)) {
|
|
234
|
+
return context;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const ctx = context as PromptInputContextProps;
|
|
238
|
+
if (!ctx || typeof ctx.usedTokens !== "number" || typeof ctx.maxTokens !== "number") {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return (
|
|
243
|
+
<Context
|
|
244
|
+
usedTokens={ctx.usedTokens}
|
|
245
|
+
maxTokens={ctx.maxTokens}
|
|
246
|
+
usage={ctx.usage}
|
|
247
|
+
modelId={ctx.modelId}
|
|
248
|
+
>
|
|
249
|
+
<ContextTrigger
|
|
250
|
+
className="h-8 px-2 text-xs font-normal gap-1.5"
|
|
251
|
+
disabled={disabled}
|
|
252
|
+
/>
|
|
253
|
+
<ContextContent>
|
|
254
|
+
<ContextContentHeader />
|
|
255
|
+
<ContextContentBody className="space-y-1.5">
|
|
256
|
+
<ContextInputUsage />
|
|
257
|
+
<ContextOutputUsage />
|
|
258
|
+
<ContextReasoningUsage />
|
|
259
|
+
<ContextCacheUsage />
|
|
260
|
+
</ContextContentBody>
|
|
261
|
+
{ctx.modelId && <ContextContentFooter />}
|
|
262
|
+
</ContextContent>
|
|
263
|
+
</Context>
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
190
267
|
PromptInput.displayName = "PromptInput";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { PromptInput } from "./PromptInput";
|
|
2
|
-
export type { PromptInputBlockProps } from "./PromptInput";
|
|
2
|
+
export type { PromptInputBlockProps, PromptInputContextProps } from "./PromptInput";
|
|
3
3
|
|
|
4
4
|
export {
|
|
5
5
|
PromptInputProvider,
|
|
@@ -15,3 +15,29 @@ export type {
|
|
|
15
15
|
PromptInputProviderProps,
|
|
16
16
|
} from "@/components/ai-elements/prompt-input";
|
|
17
17
|
|
|
18
|
+
export {
|
|
19
|
+
Context,
|
|
20
|
+
ContextTrigger,
|
|
21
|
+
ContextContent,
|
|
22
|
+
ContextContentHeader,
|
|
23
|
+
ContextContentBody,
|
|
24
|
+
ContextContentFooter,
|
|
25
|
+
ContextInputUsage,
|
|
26
|
+
ContextOutputUsage,
|
|
27
|
+
ContextReasoningUsage,
|
|
28
|
+
ContextCacheUsage,
|
|
29
|
+
} from "@/components/ai-elements/context";
|
|
30
|
+
|
|
31
|
+
export type {
|
|
32
|
+
ContextProps,
|
|
33
|
+
ContextTriggerProps,
|
|
34
|
+
ContextContentProps,
|
|
35
|
+
ContextContentHeaderProps,
|
|
36
|
+
ContextContentBodyProps,
|
|
37
|
+
ContextContentFooterProps,
|
|
38
|
+
ContextInputUsageProps,
|
|
39
|
+
ContextOutputUsageProps,
|
|
40
|
+
ContextReasoningUsageProps,
|
|
41
|
+
ContextCacheUsageProps,
|
|
42
|
+
} from "@/components/ai-elements/context";
|
|
43
|
+
|