@preset.nz/ux-kit 0.1.0 → 0.1.2
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 +31 -42
- package/THIRD_PARTY_NOTICES.md +253 -0
- package/package.json +13 -13
- package/src/chat/index.ts +9 -0
- package/src/chat/message.tsx +80 -0
- package/src/chat/prompt-input.tsx +121 -0
- package/src/chat/reasoning.tsx +182 -0
- package/src/chat/scroller.tsx +147 -0
- package/src/chat/suggestion.tsx +65 -0
- package/src/chat/tool.tsx +143 -0
- package/src/chat/types.ts +8 -0
- package/src/components/alert-shell.tsx +66 -0
- package/src/components/avatar.tsx +1 -1
- package/src/components/badge.tsx +6 -6
- package/src/components/breadcrumb.tsx +8 -10
- package/src/components/button.tsx +1 -1
- package/src/components/card.tsx +4 -21
- package/src/components/checkbox.tsx +3 -5
- package/src/components/color-field.tsx +20 -7
- package/src/components/color-swatch.tsx +3 -3
- package/src/components/confirm-dialog.tsx +93 -0
- package/src/components/context-menu.tsx +16 -13
- package/src/components/dialog.tsx +8 -9
- package/src/components/download-prompt.tsx +108 -0
- package/src/components/dropdown-menu.tsx +16 -13
- package/src/components/empty-state.tsx +6 -3
- package/src/components/input.tsx +2 -2
- package/src/components/label.tsx +3 -2
- package/src/components/list.tsx +205 -0
- package/src/components/navigation-menu.tsx +17 -10
- package/src/components/number-field.tsx +30 -26
- package/src/components/popover.tsx +6 -10
- package/src/components/progress.tsx +77 -0
- package/src/components/select.tsx +16 -28
- package/src/components/separator.tsx +2 -6
- package/src/components/sheet.tsx +16 -8
- package/src/components/side-panel.tsx +31 -8
- package/src/components/sidebar.tsx +19 -52
- package/src/components/skeleton.tsx +1 -1
- package/src/components/slider.tsx +4 -8
- package/src/components/snackbar.tsx +39 -36
- package/src/components/status-bar.tsx +10 -3
- package/src/components/tabs.tsx +5 -6
- package/src/components/textarea.tsx +2 -2
- package/src/components/toggle-group.tsx +4 -4
- package/src/components/toggle.tsx +3 -1
- package/src/components/toolbar.tsx +8 -11
- package/src/components/tooltip.tsx +1 -1
- package/src/graphics/adsr.ts +21 -13
- package/src/graphics/curve-editor.tsx +56 -14
- package/src/graphics/curve-edits.ts +20 -3
- package/src/graphics/index.ts +6 -6
- package/src/graphics/scale.ts +4 -1
- package/src/graphics/use-canvas.ts +1 -1
- package/src/graphics/use-tokens.ts +4 -1
- package/src/index.css +9 -1
- package/src/index.ts +7 -5
- package/src/lib/use-mobile.ts +1 -1
- package/src/lib/utils.ts +1 -1
- package/src/shadcn-tailwind.css +665 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2023 Vercel, Inc.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*
|
|
16
|
+
* Adapted from Vercel AI Elements (https://github.com/vercel/ai-elements), reasoning.tsx.
|
|
17
|
+
* Modified by preset.nz, 2026-10-14: own state instead of Radix's useControllableState, Base UI Collapsible, CSS shimmer instead of motion, streamdown core only, Phosphor icons, NZ English strings.
|
|
18
|
+
*/
|
|
19
|
+
import { Collapsible } from "@base-ui/react/collapsible"
|
|
20
|
+
import { BrainIcon, CaretDownIcon } from "@phosphor-icons/react"
|
|
21
|
+
import type { ReactNode } from "react"
|
|
22
|
+
import { createContext, memo, useContext, useEffect, useMemo, useRef, useState } from "react"
|
|
23
|
+
import { Streamdown } from "streamdown"
|
|
24
|
+
|
|
25
|
+
import { cn } from "../lib/utils"
|
|
26
|
+
|
|
27
|
+
type ReasoningContextValue = {
|
|
28
|
+
isStreaming: boolean
|
|
29
|
+
isOpen: boolean
|
|
30
|
+
duration: number | undefined
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const ReasoningContext = createContext<ReasoningContextValue | null>(null)
|
|
34
|
+
|
|
35
|
+
function useReasoning() {
|
|
36
|
+
const context = useContext(ReasoningContext)
|
|
37
|
+
if (!context) throw new Error("Reasoning parts must sit inside <Reasoning>")
|
|
38
|
+
return context
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type ReasoningProps = Omit<Collapsible.Root.Props, "open" | "defaultOpen"> & {
|
|
42
|
+
/** True while the thinking is still arriving. Opens the block, and folds it again a moment after it ends. */
|
|
43
|
+
isStreaming?: boolean
|
|
44
|
+
open?: boolean
|
|
45
|
+
defaultOpen?: boolean
|
|
46
|
+
/** Seconds spent thinking. Measured from `isStreaming` when not given. */
|
|
47
|
+
duration?: number
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const AUTO_CLOSE_DELAY = 1000
|
|
51
|
+
|
|
52
|
+
/** A folded "Thinking…" block. */
|
|
53
|
+
export const Reasoning = memo(function Reasoning({
|
|
54
|
+
className,
|
|
55
|
+
isStreaming = false,
|
|
56
|
+
open,
|
|
57
|
+
defaultOpen,
|
|
58
|
+
onOpenChange,
|
|
59
|
+
duration: durationProp,
|
|
60
|
+
children,
|
|
61
|
+
...props
|
|
62
|
+
}: ReasoningProps) {
|
|
63
|
+
const [innerOpen, setInnerOpen] = useState(defaultOpen ?? isStreaming)
|
|
64
|
+
const [measured, setMeasured] = useState<number | undefined>(undefined)
|
|
65
|
+
const isOpen = open ?? innerOpen
|
|
66
|
+
const duration = durationProp ?? measured
|
|
67
|
+
|
|
68
|
+
const everStreamed = useRef(isStreaming)
|
|
69
|
+
const autoClosed = useRef(false)
|
|
70
|
+
const startedAt = useRef<number | null>(null)
|
|
71
|
+
|
|
72
|
+
// A host that passes `open` owns it; the automatic open and fold only run when it does not.
|
|
73
|
+
const uncontrolled = open === undefined
|
|
74
|
+
const setAuto = (next: boolean) => setInnerOpen(next)
|
|
75
|
+
const setAutoRef = useRef(setAuto)
|
|
76
|
+
setAutoRef.current = setAuto
|
|
77
|
+
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (isStreaming) {
|
|
80
|
+
everStreamed.current = true
|
|
81
|
+
startedAt.current ??= Date.now()
|
|
82
|
+
if (uncontrolled && defaultOpen !== false && !isOpen) setAutoRef.current(true)
|
|
83
|
+
} else if (startedAt.current !== null) {
|
|
84
|
+
setMeasured(Math.ceil((Date.now() - startedAt.current) / 1000))
|
|
85
|
+
startedAt.current = null
|
|
86
|
+
}
|
|
87
|
+
}, [isStreaming, isOpen, defaultOpen, uncontrolled])
|
|
88
|
+
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (!uncontrolled || !everStreamed.current || isStreaming || !isOpen || autoClosed.current) {
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
const timer = setTimeout(() => {
|
|
94
|
+
autoClosed.current = true
|
|
95
|
+
setAutoRef.current(false)
|
|
96
|
+
}, AUTO_CLOSE_DELAY)
|
|
97
|
+
return () => clearTimeout(timer)
|
|
98
|
+
}, [isStreaming, isOpen, uncontrolled])
|
|
99
|
+
|
|
100
|
+
const value = useMemo(() => ({ duration, isOpen, isStreaming }), [duration, isOpen, isStreaming])
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<ReasoningContext.Provider value={value}>
|
|
104
|
+
<Collapsible.Root
|
|
105
|
+
data-slot="reasoning"
|
|
106
|
+
className={cn("w-full", className)}
|
|
107
|
+
open={isOpen}
|
|
108
|
+
onOpenChange={(next, details) => {
|
|
109
|
+
setInnerOpen(next)
|
|
110
|
+
onOpenChange?.(next, details)
|
|
111
|
+
}}
|
|
112
|
+
{...props}
|
|
113
|
+
>
|
|
114
|
+
{children}
|
|
115
|
+
</Collapsible.Root>
|
|
116
|
+
</ReasoningContext.Provider>
|
|
117
|
+
)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
function defaultThinkingMessage(isStreaming: boolean, duration?: number): ReactNode {
|
|
121
|
+
if (isStreaming || duration === 0) return <span className="shimmer">Thinking…</span>
|
|
122
|
+
if (duration === undefined) return <span>Thought for a few seconds</span>
|
|
123
|
+
return <span>Thought for {duration} seconds</span>
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export type ReasoningTriggerProps = Collapsible.Trigger.Props & {
|
|
127
|
+
getThinkingMessage?: (isStreaming: boolean, duration?: number) => ReactNode
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export const ReasoningTrigger = memo(function ReasoningTrigger({
|
|
131
|
+
className,
|
|
132
|
+
children,
|
|
133
|
+
getThinkingMessage = defaultThinkingMessage,
|
|
134
|
+
...props
|
|
135
|
+
}: ReasoningTriggerProps) {
|
|
136
|
+
const { isStreaming, isOpen, duration } = useReasoning()
|
|
137
|
+
return (
|
|
138
|
+
<Collapsible.Trigger
|
|
139
|
+
data-slot="reasoning-trigger"
|
|
140
|
+
className={cn(
|
|
141
|
+
"flex w-full items-center gap-2 text-xs text-muted-foreground transition-colors outline-none hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring/50",
|
|
142
|
+
className,
|
|
143
|
+
)}
|
|
144
|
+
{...props}
|
|
145
|
+
>
|
|
146
|
+
{children ?? (
|
|
147
|
+
<>
|
|
148
|
+
<BrainIcon className="size-3.5" />
|
|
149
|
+
{getThinkingMessage(isStreaming, duration)}
|
|
150
|
+
<CaretDownIcon
|
|
151
|
+
className={cn("size-3.5 transition-transform", isOpen ? "rotate-180" : "rotate-0")}
|
|
152
|
+
/>
|
|
153
|
+
</>
|
|
154
|
+
)}
|
|
155
|
+
</Collapsible.Trigger>
|
|
156
|
+
)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
export type ReasoningContentProps = Omit<Collapsible.Panel.Props, "children"> & {
|
|
160
|
+
/** The thinking so far, as markdown. */
|
|
161
|
+
children: string
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export const ReasoningContent = memo(function ReasoningContent({
|
|
165
|
+
className,
|
|
166
|
+
children,
|
|
167
|
+
...props
|
|
168
|
+
}: ReasoningContentProps) {
|
|
169
|
+
const { isStreaming } = useReasoning()
|
|
170
|
+
return (
|
|
171
|
+
<Collapsible.Panel
|
|
172
|
+
data-slot="reasoning-content"
|
|
173
|
+
className={cn(
|
|
174
|
+
"mt-2 border-l border-border pl-3 text-xs text-muted-foreground outline-none",
|
|
175
|
+
className,
|
|
176
|
+
)}
|
|
177
|
+
{...props}
|
|
178
|
+
>
|
|
179
|
+
<Streamdown isAnimating={isStreaming}>{children}</Streamdown>
|
|
180
|
+
</Collapsible.Panel>
|
|
181
|
+
)
|
|
182
|
+
})
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MIT License
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) 2023 shadcn
|
|
5
|
+
*
|
|
6
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
* in the Software without restriction, including without limitation the rights
|
|
9
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
* furnished to do so, subject to the following conditions:
|
|
12
|
+
*
|
|
13
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
* copies or substantial portions of the Software.
|
|
15
|
+
*
|
|
16
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
* SOFTWARE.
|
|
23
|
+
*
|
|
24
|
+
* Adapted from shadcn/ui's MessageScroller (https://github.com/shadcn-ui/ui),
|
|
25
|
+
* on the headless core in @shadcn/react.
|
|
26
|
+
* Modified by preset.nz, 2026-10-14: one ChatScroller that assembles the parts,
|
|
27
|
+
* Phosphor icon, ux-kit Button, no utilities the kit's stylesheet lacks.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { ArrowDownIcon } from "@phosphor-icons/react"
|
|
31
|
+
import {
|
|
32
|
+
MessageScroller as Primitive,
|
|
33
|
+
useMessageScroller,
|
|
34
|
+
useMessageScrollerScrollable,
|
|
35
|
+
useMessageScrollerVisibility,
|
|
36
|
+
} from "@shadcn/react/message-scroller"
|
|
37
|
+
import type { ComponentProps } from "react"
|
|
38
|
+
|
|
39
|
+
import { Button } from "../components/button"
|
|
40
|
+
import { cn } from "../lib/utils"
|
|
41
|
+
|
|
42
|
+
export type ChatScrollerProps = ComponentProps<typeof Primitive.Root> &
|
|
43
|
+
Pick<
|
|
44
|
+
ComponentProps<typeof Primitive.Provider>,
|
|
45
|
+
"autoScroll" | "defaultScrollPosition" | "scrollEdgeThreshold"
|
|
46
|
+
> & {
|
|
47
|
+
/** Classes for the scrolling list inside, which holds the items. */
|
|
48
|
+
contentClassName?: string
|
|
49
|
+
/** Show the jump-to-latest button when scrolled away from the end. Default true. */
|
|
50
|
+
scrollButton?: boolean
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The conversation's scrolling container. It follows a streamed reply to the
|
|
55
|
+
* end and lets go as soon as the person scrolls up. Fill it with
|
|
56
|
+
* `ChatScrollerItem`s; give it a height (it fills its parent).
|
|
57
|
+
*/
|
|
58
|
+
export function ChatScroller({
|
|
59
|
+
className,
|
|
60
|
+
contentClassName,
|
|
61
|
+
children,
|
|
62
|
+
autoScroll = true,
|
|
63
|
+
defaultScrollPosition = "end",
|
|
64
|
+
scrollEdgeThreshold,
|
|
65
|
+
scrollButton = true,
|
|
66
|
+
...props
|
|
67
|
+
}: ChatScrollerProps) {
|
|
68
|
+
return (
|
|
69
|
+
<Primitive.Provider
|
|
70
|
+
autoScroll={autoScroll}
|
|
71
|
+
defaultScrollPosition={defaultScrollPosition}
|
|
72
|
+
scrollEdgeThreshold={scrollEdgeThreshold}
|
|
73
|
+
>
|
|
74
|
+
<Primitive.Root
|
|
75
|
+
data-slot="chat-scroller"
|
|
76
|
+
className={cn("relative flex size-full min-h-0 flex-col overflow-hidden", className)}
|
|
77
|
+
{...props}
|
|
78
|
+
>
|
|
79
|
+
<Primitive.Viewport
|
|
80
|
+
data-slot="chat-scroller-viewport"
|
|
81
|
+
className="size-full min-h-0 min-w-0 overflow-y-auto overscroll-contain data-pending-scroll:invisible"
|
|
82
|
+
>
|
|
83
|
+
<Primitive.Content
|
|
84
|
+
data-slot="chat-scroller-content"
|
|
85
|
+
className={cn("flex h-max min-h-full flex-col gap-4 p-3", contentClassName)}
|
|
86
|
+
>
|
|
87
|
+
{children}
|
|
88
|
+
</Primitive.Content>
|
|
89
|
+
</Primitive.Viewport>
|
|
90
|
+
{scrollButton && <ChatScrollerButton />}
|
|
91
|
+
</Primitive.Root>
|
|
92
|
+
</Primitive.Provider>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export type ChatScrollerItemProps = ComponentProps<typeof Primitive.Item>
|
|
97
|
+
|
|
98
|
+
/** One entry in the list. Mark a user turn with `scrollAnchor` to pin the view to it as the reply grows. */
|
|
99
|
+
export function ChatScrollerItem({
|
|
100
|
+
className,
|
|
101
|
+
scrollAnchor = false,
|
|
102
|
+
...props
|
|
103
|
+
}: ChatScrollerItemProps) {
|
|
104
|
+
return (
|
|
105
|
+
<Primitive.Item
|
|
106
|
+
data-slot="chat-scroller-item"
|
|
107
|
+
scrollAnchor={scrollAnchor}
|
|
108
|
+
className={cn("min-w-0 shrink-0", className)}
|
|
109
|
+
{...props}
|
|
110
|
+
/>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export type ChatScrollerButtonProps = Omit<ComponentProps<typeof Primitive.Button>, "render">
|
|
115
|
+
|
|
116
|
+
/** The jump-to-latest button. `ChatScroller` includes one; use this to place your own. */
|
|
117
|
+
export function ChatScrollerButton({
|
|
118
|
+
direction = "end",
|
|
119
|
+
className,
|
|
120
|
+
children,
|
|
121
|
+
...props
|
|
122
|
+
}: ChatScrollerButtonProps) {
|
|
123
|
+
return (
|
|
124
|
+
<Primitive.Button
|
|
125
|
+
data-slot="chat-scroller-button"
|
|
126
|
+
data-direction={direction}
|
|
127
|
+
direction={direction}
|
|
128
|
+
render={<Button variant="outline" size="icon-sm" />}
|
|
129
|
+
className={cn(
|
|
130
|
+
"absolute left-1/2 -translate-x-1/2 transition-[translate,scale,opacity] duration-200 data-[active=false]:pointer-events-none data-[active=false]:scale-95 data-[active=false]:opacity-0 data-[direction=end]:bottom-3 data-[direction=start]:top-3 data-[direction=start]:[&_svg]:rotate-180",
|
|
131
|
+
className,
|
|
132
|
+
)}
|
|
133
|
+
{...props}
|
|
134
|
+
>
|
|
135
|
+
{children ?? (
|
|
136
|
+
<>
|
|
137
|
+
<ArrowDownIcon />
|
|
138
|
+
<span className="sr-only">
|
|
139
|
+
{direction === "end" ? "Jump to latest" : "Jump to start"}
|
|
140
|
+
</span>
|
|
141
|
+
</>
|
|
142
|
+
)}
|
|
143
|
+
</Primitive.Button>
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export { useMessageScroller, useMessageScrollerScrollable, useMessageScrollerVisibility }
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2023 Vercel, Inc.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*
|
|
16
|
+
* Adapted from Vercel AI Elements (https://github.com/vercel/ai-elements), suggestion.tsx.
|
|
17
|
+
* Modified by preset.nz, 2026-10-14: own Button and a plain scrolling row instead of ScrollArea, ux-kit tokens.
|
|
18
|
+
*/
|
|
19
|
+
import type { ComponentProps } from "react"
|
|
20
|
+
|
|
21
|
+
import { Button } from "../components/button"
|
|
22
|
+
import { cn } from "../lib/utils"
|
|
23
|
+
|
|
24
|
+
export type SuggestionsProps = ComponentProps<"div">
|
|
25
|
+
|
|
26
|
+
/** A row of starter prompts. Scrolls sideways when it does not fit. */
|
|
27
|
+
export function Suggestions({ className, ...props }: SuggestionsProps) {
|
|
28
|
+
return (
|
|
29
|
+
<div
|
|
30
|
+
data-slot="suggestions"
|
|
31
|
+
className={cn("flex w-full flex-nowrap items-center gap-2 overflow-x-auto pb-1", className)}
|
|
32
|
+
{...props}
|
|
33
|
+
/>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type SuggestionProps = Omit<ComponentProps<typeof Button>, "onClick"> & {
|
|
38
|
+
/** The text handed to `onClick`. Shown too, unless children are given. */
|
|
39
|
+
suggestion: string
|
|
40
|
+
onClick?: (suggestion: string) => void
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function Suggestion({
|
|
44
|
+
suggestion,
|
|
45
|
+
onClick,
|
|
46
|
+
className,
|
|
47
|
+
variant = "outline",
|
|
48
|
+
size = "sm",
|
|
49
|
+
children,
|
|
50
|
+
...props
|
|
51
|
+
}: SuggestionProps) {
|
|
52
|
+
return (
|
|
53
|
+
<Button
|
|
54
|
+
data-slot="suggestion"
|
|
55
|
+
type="button"
|
|
56
|
+
variant={variant}
|
|
57
|
+
size={size}
|
|
58
|
+
className={cn("shrink-0 rounded-full normal-case tracking-normal", className)}
|
|
59
|
+
onClick={() => onClick?.(suggestion)}
|
|
60
|
+
{...props}
|
|
61
|
+
>
|
|
62
|
+
{children || suggestion}
|
|
63
|
+
</Button>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2023 Vercel, Inc.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*
|
|
16
|
+
* Adapted from Vercel AI Elements (https://github.com/vercel/ai-elements), tool.tsx.
|
|
17
|
+
* Modified by preset.nz, 2026-10-14: own types instead of the `ai` package, Base UI Collapsible, Phosphor icons, no code-block dependency.
|
|
18
|
+
*/
|
|
19
|
+
import { Collapsible } from "@base-ui/react/collapsible"
|
|
20
|
+
import {
|
|
21
|
+
CaretDownIcon,
|
|
22
|
+
CheckCircleIcon,
|
|
23
|
+
ClockIcon,
|
|
24
|
+
ProhibitIcon,
|
|
25
|
+
WrenchIcon,
|
|
26
|
+
} from "@phosphor-icons/react"
|
|
27
|
+
import type { ComponentProps, ReactNode } from "react"
|
|
28
|
+
|
|
29
|
+
import { cn } from "../lib/utils"
|
|
30
|
+
import type { ToolCallState } from "./types"
|
|
31
|
+
|
|
32
|
+
const STATE_LABELS: Record<ToolCallState, string> = {
|
|
33
|
+
running: "Running",
|
|
34
|
+
done: "Done",
|
|
35
|
+
refused: "Refused",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const STATE_ICONS: Record<ToolCallState, ReactNode> = {
|
|
39
|
+
running: <ClockIcon className="size-3.5 animate-pulse" />,
|
|
40
|
+
done: <CheckCircleIcon className="size-3.5" weight="fill" />,
|
|
41
|
+
refused: <ProhibitIcon className="size-3.5 text-destructive" weight="fill" />,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type ToolCallProps = Collapsible.Root.Props
|
|
45
|
+
|
|
46
|
+
/** A collapsible row for one tool call: a header that always shows, a body that folds. */
|
|
47
|
+
export function ToolCall({ className, ...props }: ToolCallProps) {
|
|
48
|
+
return (
|
|
49
|
+
<Collapsible.Root
|
|
50
|
+
data-slot="tool-call"
|
|
51
|
+
className={cn("group w-full rounded-md border border-border", className)}
|
|
52
|
+
{...props}
|
|
53
|
+
/>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type ToolCallHeaderProps = Omit<Collapsible.Trigger.Props, "children"> & {
|
|
58
|
+
/** The tool's name, as the model called it. */
|
|
59
|
+
name: string
|
|
60
|
+
state: ToolCallState
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function ToolCallHeader({ className, name, state, ...props }: ToolCallHeaderProps) {
|
|
64
|
+
return (
|
|
65
|
+
<Collapsible.Trigger
|
|
66
|
+
data-slot="tool-call-header"
|
|
67
|
+
className={cn(
|
|
68
|
+
"flex w-full items-center justify-between gap-2 p-2 text-left outline-none focus-visible:ring-1 focus-visible:ring-ring/50",
|
|
69
|
+
className,
|
|
70
|
+
)}
|
|
71
|
+
{...props}
|
|
72
|
+
>
|
|
73
|
+
<span className="flex min-w-0 items-center gap-2">
|
|
74
|
+
<WrenchIcon className="size-3.5 shrink-0 text-muted-foreground" />
|
|
75
|
+
<span className="truncate font-mono text-xs font-medium">{name}</span>
|
|
76
|
+
</span>
|
|
77
|
+
<span className="flex shrink-0 items-center gap-1.5 text-xs text-muted-foreground">
|
|
78
|
+
{STATE_ICONS[state]}
|
|
79
|
+
{STATE_LABELS[state]}
|
|
80
|
+
<CaretDownIcon className="size-3.5 transition-transform group-data-[open]:rotate-180" />
|
|
81
|
+
</span>
|
|
82
|
+
</Collapsible.Trigger>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export type ToolCallContentProps = Collapsible.Panel.Props
|
|
87
|
+
|
|
88
|
+
export function ToolCallContent({ className, ...props }: ToolCallContentProps) {
|
|
89
|
+
return (
|
|
90
|
+
<Collapsible.Panel
|
|
91
|
+
data-slot="tool-call-content"
|
|
92
|
+
className={cn("space-y-3 p-2 pt-0 text-popover-foreground outline-none", className)}
|
|
93
|
+
{...props}
|
|
94
|
+
/>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function asText(value: unknown): string {
|
|
99
|
+
return typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export type ToolCallInputProps = ComponentProps<"div"> & {
|
|
103
|
+
/** The arguments the model sent. Objects are shown as JSON. */
|
|
104
|
+
input: unknown
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function ToolCallInput({ className, input, ...props }: ToolCallInputProps) {
|
|
108
|
+
return (
|
|
109
|
+
<div className={cn("space-y-1.5 overflow-hidden", className)} {...props}>
|
|
110
|
+
<h4 className="text-xs font-medium tracking-wide text-muted-foreground uppercase">
|
|
111
|
+
Arguments
|
|
112
|
+
</h4>
|
|
113
|
+
<pre className="overflow-x-auto rounded-sm bg-muted/50 p-2 font-mono text-xs">
|
|
114
|
+
{asText(input)}
|
|
115
|
+
</pre>
|
|
116
|
+
</div>
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export type ToolCallOutputProps = ComponentProps<"div"> & {
|
|
121
|
+
output?: unknown
|
|
122
|
+
/** Set when the call failed or was refused; shown in place of the output. */
|
|
123
|
+
errorText?: string
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function ToolCallOutput({ className, output, errorText, ...props }: ToolCallOutputProps) {
|
|
127
|
+
if (output === undefined && !errorText) return null
|
|
128
|
+
return (
|
|
129
|
+
<div className={cn("space-y-1.5", className)} {...props}>
|
|
130
|
+
<h4 className="text-xs font-medium tracking-wide text-muted-foreground uppercase">
|
|
131
|
+
{errorText ? "Error" : "Result"}
|
|
132
|
+
</h4>
|
|
133
|
+
<pre
|
|
134
|
+
className={cn(
|
|
135
|
+
"overflow-x-auto rounded-sm p-2 font-mono text-xs",
|
|
136
|
+
errorText ? "bg-destructive/10 text-destructive" : "bg-muted/50 text-foreground",
|
|
137
|
+
)}
|
|
138
|
+
>
|
|
139
|
+
{errorText ?? asText(output)}
|
|
140
|
+
</pre>
|
|
141
|
+
</div>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Who a message is from. */
|
|
2
|
+
export type ChatRole = "user" | "assistant"
|
|
3
|
+
|
|
4
|
+
/** Where a tool call is: still running, finished, or declined by the host. */
|
|
5
|
+
export type ToolCallState = "running" | "done" | "refused"
|
|
6
|
+
|
|
7
|
+
/** What the conversation is doing. The host owns it; the components only read it. */
|
|
8
|
+
export type ChatStatus = "idle" | "streaming" | "stopped" | "error"
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog"
|
|
2
|
+
import type * as React from "react"
|
|
3
|
+
import { cn } from "../lib/utils"
|
|
4
|
+
import { DialogFooter, DialogHeader, DialogOverlay } from "./dialog"
|
|
5
|
+
|
|
6
|
+
// AlertShell: the markup ConfirmDialog and DownloadPrompt share. Internal, not
|
|
7
|
+
// exported from the package. It is answered, not dismissed: no close "x", and a
|
|
8
|
+
// click outside does nothing, so the footer buttons and Escape are the ways out.
|
|
9
|
+
|
|
10
|
+
interface AlertShellProps {
|
|
11
|
+
open: boolean
|
|
12
|
+
/** Escape. The caller decides what that means (and may ignore it). */
|
|
13
|
+
onDismiss: () => void
|
|
14
|
+
title: string
|
|
15
|
+
/** The body, below the title. */
|
|
16
|
+
children?: React.ReactNode
|
|
17
|
+
/** The buttons, secondary first and primary last. */
|
|
18
|
+
footer: React.ReactNode
|
|
19
|
+
/** The element that takes focus when the dialog opens. */
|
|
20
|
+
initialFocus?: React.RefObject<HTMLElement | null>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function AlertShell({ open, onDismiss, title, children, footer, initialFocus }: AlertShellProps) {
|
|
24
|
+
return (
|
|
25
|
+
<AlertDialogPrimitive.Root
|
|
26
|
+
data-slot="confirm-dialog"
|
|
27
|
+
open={open}
|
|
28
|
+
onOpenChange={(next) => {
|
|
29
|
+
if (!next) onDismiss()
|
|
30
|
+
}}
|
|
31
|
+
>
|
|
32
|
+
<AlertDialogPrimitive.Portal data-slot="confirm-dialog-portal">
|
|
33
|
+
<DialogOverlay />
|
|
34
|
+
<AlertDialogPrimitive.Popup
|
|
35
|
+
data-slot="confirm-dialog-content"
|
|
36
|
+
initialFocus={initialFocus}
|
|
37
|
+
className={cn(
|
|
38
|
+
"fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-6 rounded-xl bg-popover p-6 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-md data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
|
39
|
+
)}
|
|
40
|
+
>
|
|
41
|
+
<DialogHeader>
|
|
42
|
+
<AlertDialogPrimitive.Title
|
|
43
|
+
data-slot="dialog-title"
|
|
44
|
+
className="font-heading leading-none font-medium"
|
|
45
|
+
>
|
|
46
|
+
{title}
|
|
47
|
+
</AlertDialogPrimitive.Title>
|
|
48
|
+
{children ? (
|
|
49
|
+
<AlertDialogPrimitive.Description
|
|
50
|
+
render={<div />}
|
|
51
|
+
data-slot="dialog-description"
|
|
52
|
+
className="text-sm text-muted-foreground"
|
|
53
|
+
>
|
|
54
|
+
{children}
|
|
55
|
+
</AlertDialogPrimitive.Description>
|
|
56
|
+
) : null}
|
|
57
|
+
</DialogHeader>
|
|
58
|
+
<DialogFooter>{footer}</DialogFooter>
|
|
59
|
+
</AlertDialogPrimitive.Popup>
|
|
60
|
+
</AlertDialogPrimitive.Portal>
|
|
61
|
+
</AlertDialogPrimitive.Root>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export type { AlertShellProps }
|
|
66
|
+
export { AlertShell }
|
package/src/components/badge.tsx
CHANGED
|
@@ -4,13 +4,16 @@ import { cva, type VariantProps } from "class-variance-authority"
|
|
|
4
4
|
|
|
5
5
|
import { cn } from "../lib/utils"
|
|
6
6
|
|
|
7
|
+
// Badge: a plain span. The `render` prop replaces Radix's `asChild`.
|
|
8
|
+
|
|
7
9
|
const badgeVariants = cva(
|
|
8
10
|
"inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-md border px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3",
|
|
9
11
|
{
|
|
10
12
|
variants: {
|
|
11
13
|
variant: {
|
|
12
14
|
default: "border-transparent bg-primary text-primary-foreground [a]:hover:bg-primary/90",
|
|
13
|
-
secondary:
|
|
15
|
+
secondary:
|
|
16
|
+
"border-transparent bg-secondary text-secondary-foreground [a]:hover:bg-secondary/90",
|
|
14
17
|
destructive:
|
|
15
18
|
"border-transparent bg-destructive text-destructive-foreground focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/90",
|
|
16
19
|
outline: "text-foreground [a]:hover:bg-accent [a]:hover:text-accent-foreground",
|
|
@@ -19,7 +22,7 @@ const badgeVariants = cva(
|
|
|
19
22
|
defaultVariants: {
|
|
20
23
|
variant: "default",
|
|
21
24
|
},
|
|
22
|
-
}
|
|
25
|
+
},
|
|
23
26
|
)
|
|
24
27
|
|
|
25
28
|
function Badge({
|
|
@@ -30,10 +33,7 @@ function Badge({
|
|
|
30
33
|
}: useRender.ComponentProps<"span"> & VariantProps<typeof badgeVariants>) {
|
|
31
34
|
return useRender({
|
|
32
35
|
defaultTagName: "span",
|
|
33
|
-
props: mergeProps<"span">(
|
|
34
|
-
{ className: cn(badgeVariants({ variant }), className) },
|
|
35
|
-
props
|
|
36
|
-
),
|
|
36
|
+
props: mergeProps<"span">({ className: cn(badgeVariants({ variant }), className) }, props),
|
|
37
37
|
render,
|
|
38
38
|
state: { slot: "badge", variant },
|
|
39
39
|
})
|