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.
Files changed (32) hide show
  1. package/components/blocks/AIConversation/AIConversation.tsx +49 -1
  2. package/components/blocks/WorkflowCanvas/WorkflowCanvas.tsx +0 -79
  3. package/components/composites/FilePreviewDialog/FilePreviewDialog.stories.tsx +23 -1
  4. package/components/composites/OrchestratorMessage/OrchestratorMessage.tsx +51 -2
  5. package/components/composites/ProjectSwitcher/ProjectSwitcher.stories.tsx +20 -1
  6. package/components/composites/ProjectSwitcher/ProjectSwitcher.tsx +7 -1
  7. package/components/composites/PromptInput/PromptInput.stories.tsx +63 -0
  8. package/components/composites/PromptInput/PromptInput.tsx +77 -0
  9. package/components/composites/PromptInput/index.ts +27 -1
  10. package/components/composites/TaskQueue/TaskQueue.stories.tsx +241 -0
  11. package/components/composites/TaskQueue/TaskQueue.tsx +270 -0
  12. package/components/composites/TaskQueue/index.ts +2 -0
  13. package/components/composites/TaskQueue/interfaces.ts +103 -0
  14. package/components/composites/UserMessage/UserMessage.tsx +40 -13
  15. package/components/composites/WorkflowToolbar/WorkflowToolbar.tsx +23 -1
  16. package/components/composites/WorkflowToolbar/interfaces.ts +14 -0
  17. package/components/composites/WorkspaceEmptyState/WorkspaceEmptyState.stories.tsx +38 -0
  18. package/components/composites/WorkspaceEmptyState/WorkspaceEmptyState.tsx +60 -0
  19. package/components/composites/WorkspaceEmptyState/index.ts +2 -0
  20. package/components/composites/index.ts +29 -0
  21. package/components/features/ChatPanel/ChatPanel.tsx +18 -1
  22. package/components/features/TextEditor/TextEditor.tsx +43 -2
  23. package/components/features/TextEditor/index.ts +1 -0
  24. package/components/features/index.ts +1 -1
  25. package/components/index.ts +25 -0
  26. package/dist/index.cjs +838 -84
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.css +68 -0
  29. package/dist/index.d.ts +369 -138
  30. package/dist/index.js +828 -85
  31. package/dist/index.js.map +1 -1
  32. 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: {} as any,
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: {} as any,
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
- ({ projects, selectedProjectId, onSelectProject, onCreateProject, className }) => {
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
+