@tulipfarm/sprout 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/attachment.js +2 -2
- package/dist/components/brand-icon.js +4 -0
- package/dist/components/canvas.js +2 -2
- package/dist/components/chat-input.d.ts +94 -0
- package/dist/components/chat-input.js +279 -0
- package/dist/components/code-block.js +2 -33
- package/dist/components/file-explorer.d.ts +27 -0
- package/dist/components/file-explorer.js +300 -0
- package/dist/components/file-viewer.d.ts +18 -1
- package/dist/components/file-viewer.js +35 -8
- package/dist/components/questionnaire.js +2 -1
- package/dist/components/search-dialog.js +5 -4
- package/dist/components/state-panel.js +2 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/internal/brand-glyphs.d.ts +28 -0
- package/dist/internal/brand-glyphs.js +24 -0
- package/dist/internal/code-tokens.d.ts +4 -0
- package/dist/internal/code-tokens.js +34 -0
- package/dist/styles/accordion.css +2 -2
- package/dist/styles/alert-dialog.css +2 -2
- package/dist/styles/alert.css +2 -2
- package/dist/styles/attachment.css +4 -24
- package/dist/styles/badge.css +2 -2
- package/dist/styles/brand-icon.css +2 -2
- package/dist/styles/breadcrumb.css +2 -2
- package/dist/styles/button.css +2 -2
- package/dist/styles/calendar.css +3 -3
- package/dist/styles/canvas.css +4 -18
- package/dist/styles/carousel.css +2 -2
- package/dist/styles/chart.css +2 -2
- package/dist/styles/chat-confirmation.css +2 -2
- package/dist/styles/chat-input.css +240 -0
- package/dist/styles/chat-sources.css +2 -2
- package/dist/styles/checkbox.css +2 -3
- package/dist/styles/choice-list.css +2 -2
- package/dist/styles/code-block.css +2 -2
- package/dist/styles/collapsible.css +2 -2
- package/dist/styles/combobox.css +6 -6
- package/dist/styles/command.css +2 -2
- package/dist/styles/context-menu.css +3 -3
- package/dist/styles/dialog.css +2 -2
- package/dist/styles/dropdown-menu.css +1 -1
- package/dist/styles/field.css +2 -3
- package/dist/styles/file-explorer.css +517 -0
- package/dist/styles/file-viewer.css +113 -16
- package/dist/styles/hover-card.css +2 -2
- package/dist/styles/index.css +6 -0
- package/dist/styles/input-group.css +6 -11
- package/dist/styles/input-otp.css +2 -2
- package/dist/styles/input.css +3 -4
- package/dist/styles/mention-list.css +1 -1
- package/dist/styles/menubar.css +3 -3
- package/dist/styles/message-response.css +4 -4
- package/dist/styles/message-scroller.css +2 -2
- package/dist/styles/message.css +4 -4
- package/dist/styles/navigation-menu.css +2 -2
- package/dist/styles/page-header.css +4 -4
- package/dist/styles/plan-outline.css +6 -6
- package/dist/styles/prompt-list.css +2 -2
- package/dist/styles/questionnaire.css +2 -17
- package/dist/styles/radio-group.css +2 -3
- package/dist/styles/reasoning-effort.css +2 -2
- package/dist/styles/review-card.css +2 -2
- package/dist/styles/rich-text-editor.css +2 -2
- package/dist/styles/schema-diagram.css +4 -4
- package/dist/styles/scroll-area.css +2 -2
- package/dist/styles/search-dialog.css +0 -83
- package/dist/styles/select.css +3 -3
- package/dist/styles/separator.css +2 -2
- package/dist/styles/sheet.css +2 -2
- package/dist/styles/sidebar.css +2 -2
- package/dist/styles/slider.css +1 -1
- package/dist/styles/state-panel.css +1 -10
- package/dist/styles/switch.css +2 -2
- package/dist/styles/table.css +10 -10
- package/dist/styles/tabs.css +4 -4
- package/dist/styles/textarea.css +3 -4
- package/dist/styles/themes/tulipfarm.css +89 -99
- package/dist/styles/toast.css +2 -2
- package/dist/styles/toggle-group.css +2 -2
- package/dist/styles/toggle.css +2 -2
- package/dist/styles/tool-activity.css +5 -5
- package/package.json +10 -1
|
@@ -168,7 +168,7 @@ function AttachmentCarousel({ className, children, onKeyDownCapture, ...props })
|
|
|
168
168
|
scroll(event.key === "ArrowLeft" ? -1 : 1);
|
|
169
169
|
}
|
|
170
170
|
};
|
|
171
|
-
return (_jsxs("div", { "data-slot": "attachment-group", "data-variant": "carousel", role: "region", "aria-roledescription": "carousel", className: cn("sprout-attachment-group", "sprout-attachment-group-carousel", className), onKeyDownCapture: handleKeyDown, ...props, children: [_jsx("div", { ref: viewport, className: "sprout-attachment-carousel-viewport", onScroll: measure, children: _jsx("div", { className: "sprout-attachment-carousel-track", children: Children.map(children, (child) => child == null || child === false ? null : (_jsx("div", { role: "group", "aria-roledescription": "slide", className: "sprout-attachment-carousel-item", children: child }))) }) }), edges.start && (_jsx("span", { className: "sprout-attachment-carousel-edge", "data-side": "start", children: _jsx(
|
|
171
|
+
return (_jsxs("div", { "data-slot": "attachment-group", "data-variant": "carousel", role: "region", "aria-roledescription": "carousel", className: cn("sprout-attachment-group", "sprout-attachment-group-carousel", className), onKeyDownCapture: handleKeyDown, ...props, children: [_jsx("div", { ref: viewport, className: "sprout-attachment-carousel-viewport", onScroll: measure, children: _jsx("div", { className: "sprout-attachment-carousel-track", children: Children.map(children, (child) => child == null || child === false ? null : (_jsx("div", { role: "group", "aria-roledescription": "slide", className: "sprout-attachment-carousel-item", children: child }))) }) }), edges.start && (_jsx("span", { className: "sprout-attachment-carousel-edge", "data-side": "start", children: _jsx(Button, { variant: "outline", size: "icon-xs", "aria-label": "Previous attachments", tabIndex: -1, className: "sprout-attachment-carousel-arrow", onClick: () => scroll(-1), children: _jsx(ChevronLeft, { "aria-hidden": "true" }) }) })), edges.end && (_jsx("span", { className: "sprout-attachment-carousel-edge", "data-side": "end", children: _jsx(Button, { variant: "outline", size: "icon-xs", "aria-label": "Next attachments", tabIndex: -1, className: "sprout-attachment-carousel-arrow", onClick: () => scroll(1), children: _jsx(ChevronRight, { "aria-hidden": "true" }) }) }))] }));
|
|
172
172
|
}
|
|
173
173
|
/** A paper glyph with a colored extension badge, such as PDF or XLSX. Decorative. */
|
|
174
174
|
export function FileTypeIcon({ name, mediaType, size = "sm", className, ...props }) {
|
|
@@ -231,7 +231,7 @@ export function FileAttachment({ file, variant = "list", onRemove, onRetry, onOp
|
|
|
231
231
|
const remove = (size) => onRemove && (_jsx(Button, { variant: "ghost", size: size, "aria-label": `Remove ${file.name}`, onClick: onRemove, children: _jsx(X, { size: 14, "aria-hidden": "true" }) }));
|
|
232
232
|
const viewer = inViewer && !shared && (_jsx(FileViewer, { file: target, open: open, onOpenChange: setOpen }));
|
|
233
233
|
if (variant === "chip") {
|
|
234
|
-
const inner = (_jsxs(_Fragment, { children: [image ? (_jsx("span", { className: "sprout-file-attachment-thumb", children:
|
|
234
|
+
const inner = (_jsxs(_Fragment, { children: [image && file.url ? (_jsx("span", { className: "sprout-file-attachment-thumb", children: _jsx(AttachmentImage, { src: file.url, name: file.name }, file.url) })) : (_jsx(FileTypeIcon, { name: file.name, mediaType: file.mediaType })), _jsx("span", { className: "sprout-file-attachment-name", children: file.name })] }));
|
|
235
235
|
return (_jsxs(_Fragment, { children: [_jsxs("div", { ref: root, "data-slot": "file-attachment", "data-variant": "chip", "data-state": file.state, "data-removable": onRemove ? "" : undefined, className: cn("sprout-file-attachment", className), title: openable ? undefined : `${file.name} · ${stateLabel}`, children: [withPreview(openable ? (_jsx("button", { ...openProps, "aria-label": openLabel, title: peek ? undefined : `${file.name} · ${stateLabel}`, className: "sprout-file-attachment-open", children: inner })) : (_jsx("span", { className: "sprout-file-attachment-open", children: inner }))), busy && _jsx(Spinner, { "aria-label": stateLabel }), retry, remove("icon-xs")] }), viewer] }));
|
|
236
236
|
}
|
|
237
237
|
const name = (_jsxs(_Fragment, { children: [_jsx("span", { className: "sprout-file-attachment-name", title: file.name, children: file.name }), variant !== "inline" && (_jsxs("span", { className: "sprout-file-attachment-status", children: [busy && _jsx(Spinner, { size: "sm", "aria-hidden": "true" }), stateLabel] }))] }));
|
|
@@ -11,6 +11,10 @@ export const brandNames = {
|
|
|
11
11
|
deepseek: "DeepSeek",
|
|
12
12
|
grok: "Grok",
|
|
13
13
|
kimi: "Kimi",
|
|
14
|
+
gemini: "Gemini",
|
|
15
|
+
mistral: "Mistral",
|
|
16
|
+
meta: "Meta",
|
|
17
|
+
qwen: "Qwen",
|
|
14
18
|
google: "Google Workspace",
|
|
15
19
|
gmail: "Gmail",
|
|
16
20
|
"google-calendar": "Google Calendar",
|
|
@@ -3,11 +3,11 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
3
3
|
import { createContext, useContext, useEffect, useId, useLayoutEffect, useRef, useState, useSyncExternalStore, } from "react";
|
|
4
4
|
import { createPortal } from "react-dom";
|
|
5
5
|
import { ArrowLeft } from "reicon-react/icons/ArrowLeft";
|
|
6
|
-
import { Loader } from "reicon-react/icons/Loader";
|
|
7
6
|
import { SidebarRight } from "reicon-react/icons/SidebarRight";
|
|
8
7
|
import { X } from "reicon-react/icons/X";
|
|
9
8
|
import { cn } from "../internal/classes.js";
|
|
10
9
|
import { Button } from "./button.js";
|
|
10
|
+
import { Spinner } from "./spinner.js";
|
|
11
11
|
import { ResizableHandle, ResizablePanel, ResizablePanelGroup, useResizableLayout, } from "./resizable.js";
|
|
12
12
|
import { Sheet, SheetContent, SheetDescription, SheetTitle } from "./sheet.js";
|
|
13
13
|
/** Below this split width (px), the canvas opens as a full-screen sheet. */
|
|
@@ -153,7 +153,7 @@ export function CanvasPanel({ tabs, activeId, onSelect, onCloseTab, onClose, chi
|
|
|
153
153
|
tabButtons.current.set(tab.id, node);
|
|
154
154
|
else
|
|
155
155
|
tabButtons.current.delete(tab.id);
|
|
156
|
-
}, "data-slot": "canvas-tab", "data-active": active ? "" : undefined, "data-busy": tab.busy ? "" : undefined, "data-hover": hovered === tab.id ? "" : undefined, "aria-label": tab.busy ? `${tab.title}, loading` : undefined, "aria-selected": active, "aria-controls": bodyId, tabIndex: active || (activeIndex === -1 && index === 0) ? 0 : -1, style: { order: index * 2 }, onClick: () => onSelect(tab.id), onKeyDown: (event) => onTabKeyDown(event, index), onPointerEnter: () => setHovered(tab.id), onPointerLeave: () => setHovered(null), className: "sprout-canvas-tab", children: [tab.busy ? (_jsx(
|
|
156
|
+
}, "data-slot": "canvas-tab", "data-active": active ? "" : undefined, "data-busy": tab.busy ? "" : undefined, "data-hover": hovered === tab.id ? "" : undefined, "aria-label": tab.busy ? `${tab.title}, loading` : undefined, "aria-selected": active, "aria-controls": bodyId, tabIndex: active || (activeIndex === -1 && index === 0) ? 0 : -1, style: { order: index * 2 }, onClick: () => onSelect(tab.id), onKeyDown: (event) => onTabKeyDown(event, index), onPointerEnter: () => setHovered(tab.id), onPointerLeave: () => setHovered(null), className: "sprout-canvas-tab", children: [tab.busy ? (_jsx(Spinner, { size: "sm", "aria-hidden": "true" })) : (Icon && (_jsx("span", { "aria-hidden": "true", className: "sprout-canvas-tab-icon", children: _jsx(Icon, { size: 14 }) }))), _jsx("span", { className: "sprout-canvas-tab-title", children: tab.title })] }, tab.id));
|
|
157
157
|
}) }), tabs.map((tab, index) => {
|
|
158
158
|
const active = index === activeIndex;
|
|
159
159
|
return (_jsx("button", { type: "button", "aria-label": `Close ${tab.title}`, "data-active": active ? "" : undefined, "data-hover": hovered === tab.id ? "" : undefined, tabIndex: active ? 0 : -1, style: { order: index * 2 + 1 }, onClick: () => closeTab(index), onPointerEnter: () => setHovered(tab.id), onPointerLeave: () => setHovered(null), className: "sprout-canvas-tab-close", children: _jsx(X, { size: 12, "aria-hidden": "true" }) }, tab.id));
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { type AttachmentFile } from "./attachment.js";
|
|
3
|
+
import { type Brand } from "./brand-icon.js";
|
|
4
|
+
import { type RichTextValue } from "./rich-text-editor.js";
|
|
5
|
+
export declare const chatInputReferenceKindNames: readonly ["agent", "skill", "file"];
|
|
6
|
+
export declare const chatInputStatusNames: readonly ["idle", "streaming"];
|
|
7
|
+
export type ChatInputReferenceKind = (typeof chatInputReferenceKindNames)[number];
|
|
8
|
+
export type ChatInputStatus = (typeof chatInputStatusNames)[number];
|
|
9
|
+
/** One row in the `@`, `/`, or `#` panel. Picking it drops a chip into the message. */
|
|
10
|
+
export interface ChatInputSuggestion {
|
|
11
|
+
id: string;
|
|
12
|
+
label: string;
|
|
13
|
+
description?: string | undefined;
|
|
14
|
+
/** A 16px icon, file icon, brand glyph, or small avatar. */
|
|
15
|
+
media?: ReactNode;
|
|
16
|
+
}
|
|
17
|
+
/** Returns the rows that match what follows the trigger. It may be async. */
|
|
18
|
+
export type ChatInputSource = (query: string) => ChatInputSuggestion[] | Promise<ChatInputSuggestion[]>;
|
|
19
|
+
export interface ChatInputAction {
|
|
20
|
+
id: string;
|
|
21
|
+
label: string;
|
|
22
|
+
description?: string | undefined;
|
|
23
|
+
icon?: ReactNode;
|
|
24
|
+
onSelect: () => void;
|
|
25
|
+
}
|
|
26
|
+
export interface ChatInputOption {
|
|
27
|
+
id: string;
|
|
28
|
+
label: string;
|
|
29
|
+
description?: string | undefined;
|
|
30
|
+
icon?: ReactNode;
|
|
31
|
+
/** Modes only. Any CSS color; tints the box border, chip, and menu icon while on. */
|
|
32
|
+
color?: string | undefined;
|
|
33
|
+
}
|
|
34
|
+
export interface ChatInputModel {
|
|
35
|
+
id: string;
|
|
36
|
+
label: string;
|
|
37
|
+
description?: string | undefined;
|
|
38
|
+
/** Draws the vendor logo. Use `icon` for a mark that isn't a `Brand`. */
|
|
39
|
+
brand?: Brand | undefined;
|
|
40
|
+
icon?: ReactNode;
|
|
41
|
+
disabled?: boolean | undefined;
|
|
42
|
+
}
|
|
43
|
+
export interface ChatInputPrompt {
|
|
44
|
+
id: string;
|
|
45
|
+
label: string;
|
|
46
|
+
icon?: ReactNode;
|
|
47
|
+
/** The text put in the message. Defaults to `label`. */
|
|
48
|
+
prompt?: string | undefined;
|
|
49
|
+
}
|
|
50
|
+
export interface ChatInputProps {
|
|
51
|
+
value: RichTextValue;
|
|
52
|
+
onValueChange: (value: RichTextValue) => void;
|
|
53
|
+
/** Runs on Enter, Cmd or Ctrl+Enter, and the send button, only when the message has content. */
|
|
54
|
+
onSend: () => void;
|
|
55
|
+
/** Shown in place of send while `status` is `streaming`. */
|
|
56
|
+
onStop?: (() => void) | undefined;
|
|
57
|
+
/** `streaming` swaps send for stop and shows `streamingPlaceholder`. */
|
|
58
|
+
status?: ChatInputStatus | undefined;
|
|
59
|
+
placeholder?: string | undefined;
|
|
60
|
+
streamingPlaceholder?: string | undefined;
|
|
61
|
+
disabled?: boolean | undefined;
|
|
62
|
+
/** Accessible name of the form. Defaults to `Message`. */
|
|
63
|
+
label?: string | undefined;
|
|
64
|
+
/** `@` opens these. Leave unset to turn the trigger off. */
|
|
65
|
+
agents?: ChatInputSource | undefined;
|
|
66
|
+
/** `/` opens these. */
|
|
67
|
+
skills?: ChatInputSource | undefined;
|
|
68
|
+
/** `#` opens these. */
|
|
69
|
+
files?: ChatInputSource | undefined;
|
|
70
|
+
attachments?: AttachmentFile[] | undefined;
|
|
71
|
+
onRemoveAttachment?: ((id: string) => void) | undefined;
|
|
72
|
+
/** The `+` menu, such as upload from computer, Google Drive, or a URL. */
|
|
73
|
+
attachActions?: ChatInputAction[] | undefined;
|
|
74
|
+
/** The modes menu, such as Plan, Deep research, or Learning. One mode at a time. */
|
|
75
|
+
modes?: ChatInputOption[] | undefined;
|
|
76
|
+
mode?: string | null | undefined;
|
|
77
|
+
onModeChange?: ((mode: string | null) => void) | undefined;
|
|
78
|
+
projects?: ChatInputOption[] | undefined;
|
|
79
|
+
project?: string | undefined;
|
|
80
|
+
onProjectChange?: ((project: string) => void) | undefined;
|
|
81
|
+
models?: ChatInputModel[] | undefined;
|
|
82
|
+
model?: string | undefined;
|
|
83
|
+
onModelChange?: ((model: string) => void) | undefined;
|
|
84
|
+
/** Starter chips under an empty message. A click fills the message. */
|
|
85
|
+
prompts?: ChatInputPrompt[] | undefined;
|
|
86
|
+
onPromptSelect?: ((prompt: ChatInputPrompt) => void) | undefined;
|
|
87
|
+
className?: string | undefined;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* A chat message box on `RichTextEditor`. `@` tags agents, `/` adds skills,
|
|
91
|
+
* and `#` adds files. The bar holds attach and mode menus, a project picker,
|
|
92
|
+
* a model picker, and send or stop. Starter prompts sit under an empty box.
|
|
93
|
+
*/
|
|
94
|
+
export declare function ChatInput({ value, onValueChange, onSend, onStop, status, placeholder, streamingPlaceholder, disabled, label, agents, skills, files, attachments, onRemoveAttachment, attachActions, modes, mode, onModeChange, projects, project, onProjectChange, models, model, onModelChange, prompts, onPromptSelect, className, }: ChatInputProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useId, useLayoutEffect, useMemo, useRef, useState, } from "react";
|
|
4
|
+
import { Extension, Node, mergeAttributes, } from "@tiptap/core";
|
|
5
|
+
import { PluginKey } from "@tiptap/pm/state";
|
|
6
|
+
import Suggestion, { exitSuggestion, } from "@tiptap/suggestion";
|
|
7
|
+
import { ArrowUp } from "reicon-react/icons/ArrowUp";
|
|
8
|
+
import { ChevronDown } from "reicon-react/icons/ChevronDown";
|
|
9
|
+
import { Folder } from "reicon-react/icons/Folder";
|
|
10
|
+
import { Plus } from "reicon-react/icons/Plus";
|
|
11
|
+
import { Sliders } from "reicon-react/icons/Sliders";
|
|
12
|
+
import { X } from "reicon-react/icons/X";
|
|
13
|
+
import { cn } from "../internal/classes.js";
|
|
14
|
+
import { FileAttachment } from "./attachment.js";
|
|
15
|
+
import { BrandIcon } from "./brand-icon.js";
|
|
16
|
+
import { Button } from "./button.js";
|
|
17
|
+
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from "./command.js";
|
|
18
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuTrigger, } from "./dropdown-menu.js";
|
|
19
|
+
import { InputGroup, InputGroupAddon, InputGroupButton, } from "./input-group.js";
|
|
20
|
+
import { MentionList, MentionOption, MentionStatus } from "./mention-list.js";
|
|
21
|
+
import { Popover, PopoverContent, PopoverTrigger } from "./popover.js";
|
|
22
|
+
import { RichTextEditor, } from "./rich-text-editor.js";
|
|
23
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip.js";
|
|
24
|
+
export const chatInputReferenceKindNames = ["agent", "skill", "file"];
|
|
25
|
+
export const chatInputStatusNames = ["idle", "streaming"];
|
|
26
|
+
const triggers = {
|
|
27
|
+
agent: { char: "@", label: "Agents", empty: "No agents match." },
|
|
28
|
+
skill: { char: "/", label: "Skills", empty: "No skills match." },
|
|
29
|
+
file: { char: "#", label: "Files", empty: "No files match." },
|
|
30
|
+
};
|
|
31
|
+
const referenceNode = Node.create({
|
|
32
|
+
name: "chatReference",
|
|
33
|
+
inline: true,
|
|
34
|
+
group: "inline",
|
|
35
|
+
atom: true,
|
|
36
|
+
selectable: true,
|
|
37
|
+
addAttributes() {
|
|
38
|
+
return {
|
|
39
|
+
kind: {
|
|
40
|
+
default: "file",
|
|
41
|
+
rendered: false,
|
|
42
|
+
parseHTML: (el) => el.getAttribute("data-chat-reference"),
|
|
43
|
+
},
|
|
44
|
+
id: {
|
|
45
|
+
default: "",
|
|
46
|
+
rendered: false,
|
|
47
|
+
parseHTML: (el) => el.getAttribute("data-id"),
|
|
48
|
+
},
|
|
49
|
+
label: {
|
|
50
|
+
default: "",
|
|
51
|
+
rendered: false,
|
|
52
|
+
parseHTML: (el) => el.getAttribute("data-label"),
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
parseHTML() {
|
|
57
|
+
return [{ tag: "span[data-chat-reference]" }];
|
|
58
|
+
},
|
|
59
|
+
renderHTML({ node, HTMLAttributes }) {
|
|
60
|
+
const kind = node.attrs.kind;
|
|
61
|
+
return [
|
|
62
|
+
"span",
|
|
63
|
+
mergeAttributes(HTMLAttributes, {
|
|
64
|
+
"data-chat-reference": kind,
|
|
65
|
+
"data-id": node.attrs.id,
|
|
66
|
+
"data-label": node.attrs.label,
|
|
67
|
+
class: "sprout-chat-input-reference",
|
|
68
|
+
}),
|
|
69
|
+
`${triggers[kind]?.char ?? ""}${node.attrs.label}`,
|
|
70
|
+
];
|
|
71
|
+
},
|
|
72
|
+
renderText({ node }) {
|
|
73
|
+
const kind = node.attrs.kind;
|
|
74
|
+
return `${triggers[kind]?.char ?? ""}${node.attrs.label}`;
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
const linkShortcut = Extension.create({
|
|
78
|
+
name: "chatInputLinkShortcut",
|
|
79
|
+
addKeyboardShortcuts() {
|
|
80
|
+
return {
|
|
81
|
+
"Mod-k": ({ editor }) => {
|
|
82
|
+
if (editor.isActive("link"))
|
|
83
|
+
return editor.chain().focus().unsetLink().run();
|
|
84
|
+
const { from, to } = editor.state.selection;
|
|
85
|
+
const text = editor.state.doc.textBetween(from, to).trim();
|
|
86
|
+
const href = /^\S+@\S+\.\S+$/.test(text)
|
|
87
|
+
? `mailto:${text}`
|
|
88
|
+
: /^(https?:\/\/|www\.)\S+$/i.test(text)
|
|
89
|
+
? text.startsWith("www.")
|
|
90
|
+
? `https://${text}`
|
|
91
|
+
: text
|
|
92
|
+
: null;
|
|
93
|
+
if (!href)
|
|
94
|
+
return false;
|
|
95
|
+
return editor.chain().focus().setLink({ href }).run();
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
function triggerExtension(kind, source, onState) {
|
|
101
|
+
const key = new PluginKey(`chatInput-${kind}`);
|
|
102
|
+
return Extension.create({
|
|
103
|
+
name: `chatInput-${kind}`,
|
|
104
|
+
addProseMirrorPlugins() {
|
|
105
|
+
return [
|
|
106
|
+
Suggestion({
|
|
107
|
+
editor: this.editor,
|
|
108
|
+
pluginKey: key,
|
|
109
|
+
char: triggers[kind].char,
|
|
110
|
+
allowSpaces: false,
|
|
111
|
+
items: async ({ query }) => (await source.current?.(query)) ?? [],
|
|
112
|
+
command: ({ editor, range, props }) => editor
|
|
113
|
+
.chain()
|
|
114
|
+
.focus()
|
|
115
|
+
.insertContentAt(range, [
|
|
116
|
+
{
|
|
117
|
+
type: "chatReference",
|
|
118
|
+
attrs: { kind, id: props.id, label: props.label },
|
|
119
|
+
},
|
|
120
|
+
{ type: "text", text: " " },
|
|
121
|
+
])
|
|
122
|
+
.run(),
|
|
123
|
+
render: () => {
|
|
124
|
+
let current = null;
|
|
125
|
+
let active = 0;
|
|
126
|
+
const publish = () => {
|
|
127
|
+
if (!current)
|
|
128
|
+
return;
|
|
129
|
+
const items = current.items;
|
|
130
|
+
onState({
|
|
131
|
+
kind,
|
|
132
|
+
items,
|
|
133
|
+
active: Math.min(active, Math.max(0, items.length - 1)),
|
|
134
|
+
select: (index) => {
|
|
135
|
+
const item = items[index];
|
|
136
|
+
if (item)
|
|
137
|
+
current?.command(item);
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
};
|
|
141
|
+
return {
|
|
142
|
+
onStart: (props) => {
|
|
143
|
+
current = props;
|
|
144
|
+
active = 0;
|
|
145
|
+
publish();
|
|
146
|
+
},
|
|
147
|
+
onUpdate: (props) => {
|
|
148
|
+
current = props;
|
|
149
|
+
active = 0;
|
|
150
|
+
publish();
|
|
151
|
+
},
|
|
152
|
+
onKeyDown: ({ event, view }) => {
|
|
153
|
+
if (event.key === "Escape") {
|
|
154
|
+
exitSuggestion(view, key);
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
const count = current?.items.length ?? 0;
|
|
158
|
+
if (!count)
|
|
159
|
+
return false;
|
|
160
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
161
|
+
active =
|
|
162
|
+
(active + (event.key === "ArrowDown" ? 1 : count - 1)) %
|
|
163
|
+
count;
|
|
164
|
+
publish();
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
if ((event.key === "Enter" && !event.shiftKey) ||
|
|
168
|
+
event.key === "Tab") {
|
|
169
|
+
current.command(current.items[active]);
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
return false;
|
|
173
|
+
},
|
|
174
|
+
onExit: () => {
|
|
175
|
+
current = null;
|
|
176
|
+
onState(null);
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
},
|
|
180
|
+
}),
|
|
181
|
+
];
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
function hasContent(node) {
|
|
186
|
+
if (node.type === "chatReference")
|
|
187
|
+
return true;
|
|
188
|
+
if (node.text?.trim())
|
|
189
|
+
return true;
|
|
190
|
+
return node.content?.some(hasContent) ?? false;
|
|
191
|
+
}
|
|
192
|
+
function textDoc(text) {
|
|
193
|
+
return {
|
|
194
|
+
type: "doc",
|
|
195
|
+
content: [{ type: "paragraph", content: [{ type: "text", text }] }],
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function ModelMark({ model }) {
|
|
199
|
+
if (model.brand)
|
|
200
|
+
return _jsx(BrandIcon, { brand: model.brand, size: "inline" });
|
|
201
|
+
return model.icon ?? null;
|
|
202
|
+
}
|
|
203
|
+
function OptionPicker({ label, search, options, value, onChange, disabled, icon, align, }) {
|
|
204
|
+
const [open, setOpen] = useState(false);
|
|
205
|
+
const selected = options.find((option) => option.id === value);
|
|
206
|
+
return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsxs(PopoverTrigger, { render: _jsx(InputGroupButton, { size: "sm", "aria-label": `${label}: ${selected?.label ?? "None"}`, disabled: disabled, className: "sprout-chat-input-picker" }), children: [icon(selected), _jsx("span", { className: "sprout-chat-input-picker-label", children: selected?.label ?? label }), _jsx(ChevronDown, { size: 12, "aria-hidden": "true" })] }), _jsx(PopoverContent, { size: "picker", side: "top", align: align, sideOffset: 8, className: "sprout-chat-input-menu", children: _jsxs(Command, { children: [_jsx(CommandInput, { "aria-label": search, placeholder: `${search}…` }), _jsxs(CommandList, { children: [_jsx(CommandEmpty, { children: "No matches." }), _jsx(CommandGroup, { heading: label, children: options.map((option) => (_jsxs(CommandItem, { value: `${option.label} ${option.description ?? ""}`, "data-checked": option.id === value, disabled: "disabled" in option && !!option.disabled, onSelect: () => {
|
|
207
|
+
onChange(option.id);
|
|
208
|
+
setOpen(false);
|
|
209
|
+
}, children: [icon(option), _jsxs("span", { className: "sprout-chat-input-option-text", children: [_jsx("span", { children: option.label }), option.description && (_jsx("span", { className: "sprout-chat-input-option-description", children: option.description }))] })] }, option.id))) })] })] }) })] }));
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* A chat message box on `RichTextEditor`. `@` tags agents, `/` adds skills,
|
|
213
|
+
* and `#` adds files. The bar holds attach and mode menus, a project picker,
|
|
214
|
+
* a model picker, and send or stop. Starter prompts sit under an empty box.
|
|
215
|
+
*/
|
|
216
|
+
export function ChatInput({ value, onValueChange, onSend, onStop, status = "idle", placeholder = "Ask anything, @agents, /skills, #files…", streamingPlaceholder = "Working on your request…", disabled = false, label = "Message", agents, skills, files, attachments = [], onRemoveAttachment, attachActions, modes, mode = null, onModeChange, projects, project, onProjectChange, models, model, onModelChange, prompts, onPromptSelect, className, }) {
|
|
217
|
+
const editor = useRef(null);
|
|
218
|
+
const form = useRef(null);
|
|
219
|
+
const listId = useId();
|
|
220
|
+
const [panel, setPanel] = useState(null);
|
|
221
|
+
const [panelBelow, setPanelBelow] = useState(false);
|
|
222
|
+
const sources = {
|
|
223
|
+
agent: useRef(agents),
|
|
224
|
+
skill: useRef(skills),
|
|
225
|
+
file: useRef(files),
|
|
226
|
+
};
|
|
227
|
+
sources.agent.current = agents;
|
|
228
|
+
sources.skill.current = skills;
|
|
229
|
+
sources.file.current = files;
|
|
230
|
+
const enabled = `${!!agents}${!!skills}${!!files}`;
|
|
231
|
+
const extensions = useMemo(() => [
|
|
232
|
+
referenceNode,
|
|
233
|
+
linkShortcut,
|
|
234
|
+
...chatInputReferenceKindNames
|
|
235
|
+
.filter((kind) => sources[kind].current)
|
|
236
|
+
.map((kind) => triggerExtension(kind, sources[kind], setPanel)),
|
|
237
|
+
],
|
|
238
|
+
// Sources are refs, so only adding or removing a trigger rebuilds the editor.
|
|
239
|
+
[enabled]);
|
|
240
|
+
const streaming = status === "streaming";
|
|
241
|
+
const filled = hasContent(value) || attachments.length > 0;
|
|
242
|
+
const canSend = filled && !disabled && !streaming;
|
|
243
|
+
const activeMode = modes?.find((option) => option.id === mode);
|
|
244
|
+
const optionId = (index) => `${listId}-option-${index}`;
|
|
245
|
+
const panelOpen = !!panel;
|
|
246
|
+
useLayoutEffect(() => {
|
|
247
|
+
const bounds = form.current?.getBoundingClientRect();
|
|
248
|
+
// Near the top of a short screen there's no room above, so the panel opens below.
|
|
249
|
+
if (panel && bounds)
|
|
250
|
+
setPanelBelow(bounds.top < 320 && window.innerHeight - bounds.bottom > bounds.top);
|
|
251
|
+
}, [panelOpen]);
|
|
252
|
+
const send = () => {
|
|
253
|
+
if (canSend)
|
|
254
|
+
onSend();
|
|
255
|
+
};
|
|
256
|
+
const fill = (prompt) => {
|
|
257
|
+
editor.current?.setContent(textDoc(prompt.prompt ?? prompt.label));
|
|
258
|
+
editor.current?.editor()?.commands.focus("end");
|
|
259
|
+
onPromptSelect?.(prompt);
|
|
260
|
+
};
|
|
261
|
+
return (_jsxs("div", { "data-slot": "chat-input", "data-status": status, "data-mode-color": activeMode?.color ? "" : undefined, style: activeMode?.color
|
|
262
|
+
? {
|
|
263
|
+
"--sprout-chat-input-mode": activeMode.color,
|
|
264
|
+
}
|
|
265
|
+
: undefined, className: cn("sprout-chat-input", className), children: [_jsxs("form", { ref: form, "aria-label": label, className: "sprout-chat-input-form", onSubmit: (event) => {
|
|
266
|
+
event.preventDefault();
|
|
267
|
+
send();
|
|
268
|
+
}, children: [_jsxs(InputGroup, { variant: "composer", className: "sprout-chat-input-box", children: [attachments.length > 0 && (_jsx(InputGroupAddon, { align: "block-start", className: "sprout-chat-input-attachments", children: attachments.map((file) => (_jsx(FileAttachment, { file: file, variant: "chip", viewable: true, preview: true, onRemove: onRemoveAttachment && !disabled
|
|
269
|
+
? () => onRemoveAttachment(file.id)
|
|
270
|
+
: undefined }, file.id))) })), _jsx(RichTextEditor, { ref: editor, value: value, onChange: onValueChange, onSend: send, extensions: extensions, placeholder: streaming ? streamingPlaceholder : placeholder, disabled: disabled, ariaLabel: label, ariaExpanded: !!panel, ariaControls: panel ? listId : undefined, ariaActiveDescendant: panel?.items.length ? optionId(panel.active) : undefined, className: "sprout-chat-input-editor", toolbarAddon: _jsxs("div", { className: "sprout-chat-input-bar", children: [_jsxs("div", { className: "sprout-chat-input-bar-start", children: [attachActions && attachActions.length > 0 && (_jsxs(DropdownMenu, { children: [_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { render: _jsx(DropdownMenuTrigger, { render: _jsx(InputGroupButton, { size: "icon-sm", "aria-label": "Add files and more", disabled: disabled }) }), children: _jsx(Plus, { size: 16, "aria-hidden": "true" }) }), _jsx(TooltipContent, { children: "Add files and more" })] }), _jsx(DropdownMenuContent, { size: "menu", side: "top", align: "start", className: "sprout-chat-input-menu", children: _jsxs(DropdownMenuGroup, { children: [_jsx(DropdownMenuLabel, { children: "Add" }), attachActions.map((action) => (_jsxs(DropdownMenuItem, { onClick: action.onSelect, children: [action.icon, _jsxs("span", { className: "sprout-chat-input-option-text", children: [_jsx("span", { children: action.label }), action.description && (_jsx("span", { className: "sprout-chat-input-option-description", children: action.description }))] })] }, action.id)))] }) })] })), modes && modes.length > 0 && (_jsxs(DropdownMenu, { children: [_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { render: _jsx(DropdownMenuTrigger, { render: _jsx(InputGroupButton, { size: "icon-sm", "aria-label": `Mode: ${activeMode?.label ?? "Default"}`, disabled: disabled }) }), children: _jsx(Sliders, { size: 16, "aria-hidden": "true" }) }), _jsx(TooltipContent, { children: "Modes" })] }), _jsx(DropdownMenuContent, { size: "menu", side: "top", align: "start", className: "sprout-chat-input-menu", children: _jsxs(DropdownMenuGroup, { children: [_jsx(DropdownMenuLabel, { children: "Mode" }), _jsx(DropdownMenuRadioGroup, { value: mode ?? "", onValueChange: (next) => onModeChange?.(next === mode || !next ? null : String(next)), children: modes.map((option) => (_jsxs(DropdownMenuRadioItem, { value: option.id, closeOnClick: true, style: option.color
|
|
271
|
+
? {
|
|
272
|
+
"--sprout-chat-input-mode": option.color,
|
|
273
|
+
}
|
|
274
|
+
: undefined, className: option.color
|
|
275
|
+
? "sprout-chat-input-mode-option"
|
|
276
|
+
: undefined, children: [option.icon, _jsxs("span", { className: "sprout-chat-input-option-text", children: [_jsx("span", { children: option.label }), option.description && (_jsx("span", { className: "sprout-chat-input-option-description", children: option.description }))] })] }, option.id))) })] }) })] })), activeMode && (_jsxs(InputGroupButton, { size: "sm", variant: "secondary", className: "sprout-chat-input-mode", "aria-label": `Turn off ${activeMode.label}`, disabled: disabled, onClick: () => onModeChange?.(null), children: [activeMode.icon, _jsx("span", { className: "sprout-chat-input-picker-label", children: activeMode.label }), _jsx(X, { size: 12, "aria-hidden": "true" })] })), projects && projects.length > 0 && (_jsxs(_Fragment, { children: [_jsx("span", { className: "sprout-chat-input-divider", "aria-hidden": "true" }), _jsx(OptionPicker, { label: "Project", search: "Search projects", options: projects, value: project, onChange: (id) => onProjectChange?.(id), disabled: disabled, icon: (option) => option?.icon ?? (_jsx(Folder, { size: 16, "aria-hidden": "true" })), align: "start" })] }))] }), _jsxs("div", { className: "sprout-chat-input-bar-end", children: [models && models.length > 0 && (_jsx(OptionPicker, { label: "Model", search: "Search models", options: models, value: model, onChange: (id) => onModelChange?.(id), disabled: disabled, icon: (option) => option && _jsx(ModelMark, { model: option }), align: "end" })), streaming ? (_jsx(InputGroupButton, { size: "icon-sm", variant: "default", className: "sprout-chat-input-send", "aria-label": "Stop", onClick: onStop, children: _jsx("span", { className: "sprout-chat-input-stop", "aria-hidden": "true" }) })) : (_jsx(InputGroupButton, { type: "submit", size: "icon-sm", variant: canSend ? "default" : "secondary", className: "sprout-chat-input-send", "aria-label": "Send message", disabled: !canSend, children: _jsx(ArrowUp, { size: 16, "aria-hidden": "true" }) }))] })] }) })] }), panel && (_jsx("div", { className: "sprout-chat-input-panel", "data-side": panelBelow ? "bottom" : "top", children: _jsx(MentionList, { id: listId, label: triggers[panel.kind].label, announcement: panel.items.length
|
|
277
|
+
? `${panel.items.length} ${triggers[panel.kind].label.toLowerCase()}. Use Up and Down to choose, Enter to add, Escape to close.`
|
|
278
|
+
: triggers[panel.kind].empty, status: panel.items.length ? undefined : (_jsx(MentionStatus, { tone: "empty", children: triggers[panel.kind].empty })), children: panel.items.map((item, index) => (_jsx(MentionOption, { id: optionId(index), active: index === panel.active, onSelect: () => panel.select(index), media: item.media, title: item.label, description: item.description }, item.id))) }) }))] }), prompts && prompts.length > 0 && !filled && !streaming && (_jsx("div", { role: "group", "aria-label": "Suggested prompts", className: "sprout-chat-input-prompts", children: prompts.map((prompt) => (_jsxs(Button, { size: "sm", variant: "outline", disabled: disabled, className: "sprout-chat-input-prompt", onClick: () => fill(prompt), children: [prompt.icon, prompt.label] }, prompt.id))) }))] }));
|
|
279
|
+
}
|
|
@@ -5,7 +5,8 @@ import { Check } from "reicon-react/icons/Check";
|
|
|
5
5
|
import { Copy } from "reicon-react/icons/Copy";
|
|
6
6
|
import { cn } from "../internal/classes.js";
|
|
7
7
|
import { codeLanguageIds, codeLanguageNames, } from "../internal/languages.js";
|
|
8
|
-
import {
|
|
8
|
+
import { tokenClass, useCodeTokens } from "../internal/code-tokens.js";
|
|
9
|
+
import { languageForFilename, loadCodeLanguage, resolveCodeLanguage, } from "../internal/syntax.js";
|
|
9
10
|
import { Button } from "./button.js";
|
|
10
11
|
export { languageForFilename };
|
|
11
12
|
export const codeBlockVariantNames = ["inline", "file", "preview"];
|
|
@@ -30,38 +31,6 @@ export async function preloadCodeLanguages(...languages) {
|
|
|
30
31
|
return resolved ? [loadCodeLanguage(resolved)] : [];
|
|
31
32
|
}));
|
|
32
33
|
}
|
|
33
|
-
function useCodeTokens(code, language) {
|
|
34
|
-
const [loaded, setLoaded] = useState(0);
|
|
35
|
-
const highlightable = language !== null && code.length <= maxHighlightedLength;
|
|
36
|
-
const needed = useMemo(() => highlightable ? [language, ...embeddedLanguages(code, language)] : [], [code, language, highlightable]);
|
|
37
|
-
const missing = needed.filter((name) => !isLanguageReady(name));
|
|
38
|
-
const key = missing.join(" ");
|
|
39
|
-
useEffect(() => {
|
|
40
|
-
if (!key)
|
|
41
|
-
return;
|
|
42
|
-
let live = true;
|
|
43
|
-
void Promise.all(key.split(" ").map((name) => loadCodeLanguage(name))).then(() => live && setLoaded((value) => value + 1), () => undefined);
|
|
44
|
-
return () => {
|
|
45
|
-
live = false;
|
|
46
|
-
};
|
|
47
|
-
}, [key]);
|
|
48
|
-
return useMemo(() => (highlightable ? codeTokens(code, language) : null),
|
|
49
|
-
// `loaded` and `key` re-run this once the grammars arrive.
|
|
50
|
-
[code, language, highlightable, loaded, key]);
|
|
51
|
-
}
|
|
52
|
-
const fontStyleClasses = [
|
|
53
|
-
[1, "sprout-syntax-italic"],
|
|
54
|
-
[2, "sprout-syntax-bold"],
|
|
55
|
-
[4, "sprout-syntax-underline"],
|
|
56
|
-
];
|
|
57
|
-
function tokenClass(token) {
|
|
58
|
-
const role = /--sprout-syntax-(?:token-)?([a-z-]+)/.exec(token.color ?? "")?.[1];
|
|
59
|
-
const classes = role && role !== "foreground" ? [`sprout-syntax-${role}`] : [];
|
|
60
|
-
for (const [flag, name] of fontStyleClasses)
|
|
61
|
-
if ((token.fontStyle ?? 0) & flag)
|
|
62
|
-
classes.push(name);
|
|
63
|
-
return classes.length ? classes.join(" ") : undefined;
|
|
64
|
-
}
|
|
65
34
|
export function CodeBlock({ code, language, filename, lineNumbers = true, wrap = false, variant = "inline", className, ...props }) {
|
|
66
35
|
const [copyState, setCopyState] = useState("idle");
|
|
67
36
|
const timer = useRef(undefined);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type HTMLAttributes } from "react";
|
|
2
|
+
export interface FileExplorerTextFile {
|
|
3
|
+
/** A slash-separated path, such as `agents/scout/persona.md`. Folders come from the paths. */
|
|
4
|
+
path: string;
|
|
5
|
+
content: string;
|
|
6
|
+
}
|
|
7
|
+
export interface FileExplorerUnavailableFile {
|
|
8
|
+
path: string;
|
|
9
|
+
/** Why the file has no text to show, such as “Binary files cannot be displayed.” */
|
|
10
|
+
unavailable: string;
|
|
11
|
+
}
|
|
12
|
+
export type FileExplorerFile = FileExplorerTextFile | FileExplorerUnavailableFile;
|
|
13
|
+
export interface FileExplorerProps extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
|
|
14
|
+
files: FileExplorerFile[];
|
|
15
|
+
/** Heads the sidebar and names the tree, such as “Soul”. */
|
|
16
|
+
label?: string | undefined;
|
|
17
|
+
/** The file open at first. Its folders start expanded. */
|
|
18
|
+
defaultPath?: string | undefined;
|
|
19
|
+
}
|
|
20
|
+
/** Search stops after this many matching lines, so one common word cannot flood the list. */
|
|
21
|
+
export declare const fileExplorerMaxMatches = 500;
|
|
22
|
+
/**
|
|
23
|
+
* A read-only code explorer: a folder tree, open tabs, and a syntax-colored
|
|
24
|
+
* file pane. Search filters file names and lists matching lines across every
|
|
25
|
+
* file; picking a line opens the file there.
|
|
26
|
+
*/
|
|
27
|
+
export declare function FileExplorer({ files, label, defaultPath, className, ...props }: FileExplorerProps): import("react").JSX.Element;
|