@intentface/chat 0.1.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/LICENSE +21 -0
- package/README.md +108 -0
- package/dist/ask-user/ask-user.d.ts +104 -0
- package/dist/ask-user/ask-user.js +226 -0
- package/dist/ask-user/index.d.ts +3 -0
- package/dist/ask-user/index.js +3 -0
- package/dist/ask-user/index.parts.d.ts +1 -0
- package/dist/ask-user/index.parts.js +5 -0
- package/dist/attachments/attachments.d.ts +41 -0
- package/dist/attachments/attachments.js +91 -0
- package/dist/attachments/index.d.ts +3 -0
- package/dist/attachments/index.js +3 -0
- package/dist/attachments/index.parts.d.ts +1 -0
- package/dist/attachments/index.parts.js +5 -0
- package/dist/chip/chip.d.ts +25 -0
- package/dist/chip/chip.js +25 -0
- package/dist/chip/index.d.ts +2 -0
- package/dist/chip/index.js +2 -0
- package/dist/chip/index.parts.d.ts +1 -0
- package/dist/chip/index.parts.js +5 -0
- package/dist/chip-markdown.d.ts +25 -0
- package/dist/chip-markdown.js +52 -0
- package/dist/composer/actions.d.ts +35 -0
- package/dist/composer/actions.js +75 -0
- package/dist/composer/ask-user-machine.d.ts +52 -0
- package/dist/composer/ask-user-machine.js +164 -0
- package/dist/composer/attachments-machine.d.ts +34 -0
- package/dist/composer/attachments-machine.js +91 -0
- package/dist/composer/attachments.d.ts +16 -0
- package/dist/composer/attachments.js +32 -0
- package/dist/composer/command-list.d.ts +49 -0
- package/dist/composer/command-list.js +357 -0
- package/dist/composer/commands.d.ts +28 -0
- package/dist/composer/commands.js +56 -0
- package/dist/composer/container.d.ts +3 -0
- package/dist/composer/container.js +32 -0
- package/dist/composer/controller.d.ts +3 -0
- package/dist/composer/controller.js +6 -0
- package/dist/composer/editor-dom.d.ts +55 -0
- package/dist/composer/editor-dom.js +431 -0
- package/dist/composer/fuzzy.d.ts +4 -0
- package/dist/composer/fuzzy.js +49 -0
- package/dist/composer/index.d.ts +25 -0
- package/dist/composer/index.js +13 -0
- package/dist/composer/index.parts.d.ts +11 -0
- package/dist/composer/index.parts.js +15 -0
- package/dist/composer/internals.d.ts +29 -0
- package/dist/composer/internals.js +102 -0
- package/dist/composer/keyboard.d.ts +61 -0
- package/dist/composer/keyboard.js +75 -0
- package/dist/composer/panel.d.ts +28 -0
- package/dist/composer/panel.js +80 -0
- package/dist/composer/placeholder.d.ts +10 -0
- package/dist/composer/placeholder.js +8 -0
- package/dist/composer/popover.d.ts +12 -0
- package/dist/composer/popover.js +63 -0
- package/dist/composer/prefix-detection.d.ts +23 -0
- package/dist/composer/prefix-detection.js +58 -0
- package/dist/composer/root.d.ts +21 -0
- package/dist/composer/root.js +107 -0
- package/dist/composer/segments.d.ts +108 -0
- package/dist/composer/segments.js +366 -0
- package/dist/composer/store.d.ts +97 -0
- package/dist/composer/store.js +451 -0
- package/dist/composer/textarea.d.ts +39 -0
- package/dist/composer/textarea.js +64 -0
- package/dist/composer/trigger-tracker.d.ts +12 -0
- package/dist/composer/trigger-tracker.js +90 -0
- package/dist/composer/types.d.ts +126 -0
- package/dist/composer/types.js +4 -0
- package/dist/composer/use-composer-editor.d.ts +43 -0
- package/dist/composer/use-composer-editor.js +649 -0
- package/dist/internal/collapsible.d.ts +22 -0
- package/dist/internal/collapsible.js +90 -0
- package/dist/internal/primitive-props.d.ts +4 -0
- package/dist/internal/primitive-props.js +4 -0
- package/dist/internal/render/baseTypes.d.ts +18 -0
- package/dist/internal/render/baseTypes.js +4 -0
- package/dist/internal/render/empty.d.ts +3 -0
- package/dist/internal/render/empty.js +5 -0
- package/dist/internal/render/getReactElementRef.d.ts +5 -0
- package/dist/internal/render/getReactElementRef.js +15 -0
- package/dist/internal/render/getStateAttributesProps.d.ts +4 -0
- package/dist/internal/render/getStateAttributesProps.js +23 -0
- package/dist/internal/render/mergeObjects.d.ts +1 -0
- package/dist/internal/render/mergeObjects.js +14 -0
- package/dist/internal/render/mergeProps.d.ts +55 -0
- package/dist/internal/render/mergeProps.js +183 -0
- package/dist/internal/render/positioning.d.ts +22 -0
- package/dist/internal/render/positioning.js +78 -0
- package/dist/internal/render/reactVersion.d.ts +3 -0
- package/dist/internal/render/reactVersion.js +7 -0
- package/dist/internal/render/resolveClassName.d.ts +8 -0
- package/dist/internal/render/resolveClassName.js +12 -0
- package/dist/internal/render/resolveStyle.d.ts +8 -0
- package/dist/internal/render/resolveStyle.js +12 -0
- package/dist/internal/render/transition.d.ts +48 -0
- package/dist/internal/render/transition.js +203 -0
- package/dist/internal/render/types.d.ts +74 -0
- package/dist/internal/render/types.js +3 -0
- package/dist/internal/render/useMergedRefs.d.ts +21 -0
- package/dist/internal/render/useMergedRefs.js +102 -0
- package/dist/internal/render/useRefWithInit.d.ts +10 -0
- package/dist/internal/render/useRefWithInit.js +12 -0
- package/dist/internal/render/useRender.d.ts +71 -0
- package/dist/internal/render/useRender.js +12 -0
- package/dist/internal/render/useRenderElement.d.ts +62 -0
- package/dist/internal/render/useRenderElement.js +157 -0
- package/dist/internal/render/warn.d.ts +1 -0
- package/dist/internal/render/warn.js +15 -0
- package/dist/internal/state-mappings.d.ts +8 -0
- package/dist/internal/state-mappings.js +17 -0
- package/dist/message/index.d.ts +3 -0
- package/dist/message/index.js +3 -0
- package/dist/message/index.parts.d.ts +1 -0
- package/dist/message/index.parts.js +5 -0
- package/dist/message/message.d.ts +43 -0
- package/dist/message/message.js +78 -0
- package/dist/message-utils.d.ts +55 -0
- package/dist/message-utils.js +79 -0
- package/dist/reasoning/index.d.ts +3 -0
- package/dist/reasoning/index.js +3 -0
- package/dist/reasoning/index.parts.d.ts +1 -0
- package/dist/reasoning/index.parts.js +5 -0
- package/dist/reasoning/reasoning.d.ts +20 -0
- package/dist/reasoning/reasoning.js +49 -0
- package/dist/steps/index.d.ts +2 -0
- package/dist/steps/index.js +2 -0
- package/dist/steps/index.parts.d.ts +1 -0
- package/dist/steps/index.parts.js +5 -0
- package/dist/steps/steps.d.ts +53 -0
- package/dist/steps/steps.js +79 -0
- package/dist/thread/geometry.d.ts +18 -0
- package/dist/thread/geometry.js +59 -0
- package/dist/thread/index.d.ts +4 -0
- package/dist/thread/index.js +3 -0
- package/dist/thread/index.parts.d.ts +1 -0
- package/dist/thread/index.parts.js +5 -0
- package/dist/thread/stores.d.ts +21 -0
- package/dist/thread/stores.js +63 -0
- package/dist/thread/thread.d.ts +66 -0
- package/dist/thread/thread.js +639 -0
- package/dist/types.d.ts +58 -0
- package/dist/types.js +17 -0
- package/package.json +106 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type ReactElement, type ReactNode } from "react";
|
|
2
|
+
import type { PrimitiveProps } from "../internal/primitive-props.js";
|
|
3
|
+
type ChipPreviewProps = {
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
};
|
|
6
|
+
export declare const ChipPreview: (_props: ChipPreviewProps) => ReactNode;
|
|
7
|
+
export type ChipState = {
|
|
8
|
+
/** Opaque styling hook, surfaced as data-variant. */
|
|
9
|
+
variant: string | undefined;
|
|
10
|
+
};
|
|
11
|
+
export type ChipRootProps = PrimitiveProps<"span", ChipState> & {
|
|
12
|
+
variant?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Called when a Chip.Preview child is present. Receives the rendered badge
|
|
15
|
+
* element and the preview content; compose your popup of choice around
|
|
16
|
+
* them. Without it, the preview content is ignored.
|
|
17
|
+
*/
|
|
18
|
+
renderWithPreview?: (badge: ReactElement, preview: ReactNode) => ReactNode;
|
|
19
|
+
};
|
|
20
|
+
export declare const ChipRoot: ({ variant, renderWithPreview, className, render, style, children, ...elementProps }: ChipRootProps) => string | number | bigint | boolean | ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | null | undefined;
|
|
21
|
+
export type ChipIconProps = PrimitiveProps<"span">;
|
|
22
|
+
export declare const ChipIcon: ({ className, render, style, ...elementProps }: ChipIconProps) => ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
23
|
+
export type ChipLabelProps = PrimitiveProps<"span">;
|
|
24
|
+
export declare const ChipLabel: ({ className, render, style, ...elementProps }: ChipLabelProps) => ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
// Headless inline chip. Renders an unstyled <span> flow-surface with
|
|
3
|
+
// data-variant for styling hooks. Popups are not owned by this layer: a
|
|
4
|
+
// Chip.Preview child is captured and handed to `renderWithPreview` so the
|
|
5
|
+
// styled layer can compose its own hover-card/popover around the badge.
|
|
6
|
+
// Every part supports the Base UI render prop.
|
|
7
|
+
import { Children, isValidElement } from "react";
|
|
8
|
+
import { useRenderElement } from "../internal/render/useRenderElement.js";
|
|
9
|
+
// Marker component — never renders directly. ChipRoot inspects its children
|
|
10
|
+
// and routes ChipPreview's content into `renderWithPreview`.
|
|
11
|
+
export const ChipPreview = (_props) => null;
|
|
12
|
+
export const ChipRoot = ({ variant, renderWithPreview, className, render, style, children, ...elementProps }) => {
|
|
13
|
+
const childArray = Children.toArray(children);
|
|
14
|
+
const previewChild = childArray.find((child) => isValidElement(child) && child.type === ChipPreview);
|
|
15
|
+
const inlineChildren = childArray.filter((child) => !isValidElement(child) || child.type !== ChipPreview);
|
|
16
|
+
const badge = useRenderElement("span", { className, render, style }, {
|
|
17
|
+
state: { variant },
|
|
18
|
+
props: [{ "data-chip": "", children: inlineChildren }, elementProps],
|
|
19
|
+
});
|
|
20
|
+
if (!previewChild || !renderWithPreview)
|
|
21
|
+
return badge;
|
|
22
|
+
return renderWithPreview(badge, previewChild.props.children);
|
|
23
|
+
};
|
|
24
|
+
export const ChipIcon = ({ className, render, style, ...elementProps }) => useRenderElement("span", { className, render, style }, { props: [{ "aria-hidden": true, "data-chip-icon": "" }, elementProps] });
|
|
25
|
+
export const ChipLabel = ({ className, render, style, ...elementProps }) => useRenderElement("span", { className, render, style }, { props: [{ "data-chip-label": "" }, elementProps] });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ChipIcon as Icon, ChipLabel as Label, ChipPreview as Preview, ChipRoot as Root, } from "./chip.js";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Part names for the `Chip` namespace. Deliberately carries no "use client":
|
|
2
|
+
// this module and index.ts must stay server-resolvable so a React Server
|
|
3
|
+
// Component can reach Chip.Root / Chip.Label through them. The directive lives
|
|
4
|
+
// one level down, on chip.tsx.
|
|
5
|
+
export { ChipIcon as Icon, ChipLabel as Label, ChipPreview as Preview, ChipRoot as Root, } from "./chip.js";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opaque wire string. The styled layer owns the concrete union and narrows
|
|
3
|
+
* at its own boundary.
|
|
4
|
+
*/
|
|
5
|
+
export type ChipIconKey = string;
|
|
6
|
+
export declare const CHIP_REF_PATTERN: RegExp;
|
|
7
|
+
export type ChipData = {
|
|
8
|
+
prefix: string;
|
|
9
|
+
value: string;
|
|
10
|
+
label: string;
|
|
11
|
+
icon?: ChipIconKey;
|
|
12
|
+
};
|
|
13
|
+
export type ChipSegment = {
|
|
14
|
+
type: "text";
|
|
15
|
+
text: string;
|
|
16
|
+
} | {
|
|
17
|
+
type: "chip";
|
|
18
|
+
label: string;
|
|
19
|
+
prefix: string;
|
|
20
|
+
value: string;
|
|
21
|
+
icon?: ChipIconKey;
|
|
22
|
+
};
|
|
23
|
+
export declare const escapeMarkdownLink: (input: string) => string;
|
|
24
|
+
export declare const encodeChipMarkdown: (chip: ChipData) => string;
|
|
25
|
+
export declare const parseChipSegments: (text: string) => ChipSegment[];
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Wire format for inline chip references inside a message text part.
|
|
2
|
+
// Shape: [Label](chip:prefix:value?icon=…). The icon rides along in the
|
|
3
|
+
// token, so a rendered message reconstructs the chip from its own text
|
|
4
|
+
// alone — no sidecar metadata array. Presentation that's derivable at render
|
|
5
|
+
// time (e.g. a per-prefix variant) is the renderer's job, not wire data.
|
|
6
|
+
export const CHIP_REF_PATTERN = /\[([^\]]+)\]\(chip:([^:)]+):([^)?]+)(?:\?([^)]*))?\)/g;
|
|
7
|
+
export const escapeMarkdownLink = (input) => input.replace(/[[\]()\\]/g, (match) => `\\${match}`);
|
|
8
|
+
// Tolerate legacy/un-encoded values — a stray "%" would otherwise throw and
|
|
9
|
+
// take the whole message render down with it.
|
|
10
|
+
const safeDecode = (value) => {
|
|
11
|
+
try {
|
|
12
|
+
return decodeURIComponent(value);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
export const encodeChipMarkdown = (chip) => {
|
|
19
|
+
const params = new URLSearchParams();
|
|
20
|
+
if (chip.icon)
|
|
21
|
+
params.set("icon", chip.icon);
|
|
22
|
+
const query = params.toString();
|
|
23
|
+
const value = encodeURIComponent(chip.value);
|
|
24
|
+
return `[${escapeMarkdownLink(chip.label)}](chip:${chip.prefix}:${value}${query ? `?${query}` : ""})`;
|
|
25
|
+
};
|
|
26
|
+
export const parseChipSegments = (text) => {
|
|
27
|
+
const segments = [];
|
|
28
|
+
let lastIndex = 0;
|
|
29
|
+
for (const match of text.matchAll(CHIP_REF_PATTERN)) {
|
|
30
|
+
const start = match.index;
|
|
31
|
+
if (start > lastIndex) {
|
|
32
|
+
segments.push({ type: "text", text: text.slice(lastIndex, start) });
|
|
33
|
+
}
|
|
34
|
+
const [token, label = "", prefix = "", rawValue = "", rawQuery] = match;
|
|
35
|
+
const params = new URLSearchParams(rawQuery ?? "");
|
|
36
|
+
const icon = params.get("icon");
|
|
37
|
+
segments.push({
|
|
38
|
+
type: "chip",
|
|
39
|
+
label,
|
|
40
|
+
prefix,
|
|
41
|
+
value: safeDecode(rawValue),
|
|
42
|
+
...(icon ? { icon } : {}),
|
|
43
|
+
});
|
|
44
|
+
lastIndex = start + token.length;
|
|
45
|
+
}
|
|
46
|
+
if (lastIndex < text.length) {
|
|
47
|
+
segments.push({ type: "text", text: text.slice(lastIndex) });
|
|
48
|
+
}
|
|
49
|
+
return segments;
|
|
50
|
+
};
|
|
51
|
+
// Building editor nodes from these segments lives with the document model in
|
|
52
|
+
// composer/segments.ts — this module is only the neutral wire format.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { PrimitiveProps } from "../internal/primitive-props.js";
|
|
2
|
+
export type ComposerContextWindowState = {
|
|
3
|
+
/** Open (data-open) while the strip has content and no panel is active. */
|
|
4
|
+
open: boolean;
|
|
5
|
+
};
|
|
6
|
+
export type ComposerContextWindowProps = PrimitiveProps<"div", ComposerContextWindowState>;
|
|
7
|
+
export declare const ComposerContextWindow: ({ children, className, render, style, ...elementProps }: ComposerContextWindowProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
8
|
+
export type ComposerActionsProps = PrimitiveProps<"div">;
|
|
9
|
+
export declare const ComposerActions: ({ className, render, style, ...elementProps }: ComposerActionsProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
10
|
+
export type UseComposerSubmitOptions = {
|
|
11
|
+
isGenerating?: boolean;
|
|
12
|
+
onStop?: () => void;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
};
|
|
15
|
+
export type ComposerSubmitState = {
|
|
16
|
+
/** "button" while generating (click stops), "submit" otherwise. */
|
|
17
|
+
type: "button" | "submit";
|
|
18
|
+
disabled: boolean;
|
|
19
|
+
isGenerating: boolean;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Send↔stop behavior: auto-disables while empty or submitting, flips the
|
|
23
|
+
* button type while generating, and aborts on Escape (unless something closer
|
|
24
|
+
* — the command list — already handled it).
|
|
25
|
+
*/
|
|
26
|
+
export declare const useComposerSubmit: ({ isGenerating, onStop, disabled, }: UseComposerSubmitOptions) => ComposerSubmitState;
|
|
27
|
+
export type ComposerSubmitButtonState = {
|
|
28
|
+
/** Present as data-generating while the stop affordance is active. */
|
|
29
|
+
generating: boolean;
|
|
30
|
+
};
|
|
31
|
+
export type ComposerSubmitProps = PrimitiveProps<"button", ComposerSubmitButtonState> & {
|
|
32
|
+
isGenerating?: boolean;
|
|
33
|
+
onStop?: () => void;
|
|
34
|
+
};
|
|
35
|
+
export declare const ComposerSubmit: ({ disabled, isGenerating, onStop, onClick, className, render, style, ...elementProps }: ComposerSubmitProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
// Composer.ContextWindow / Actions / Submit. ContextWindow is content-driven
|
|
3
|
+
// like the panels — open (data-open / data-closed) while it has content and no
|
|
4
|
+
// panel is active, yielding to an active panel. Submit owns the send↔stop
|
|
5
|
+
// behavior via useComposerSubmit and renders a plain button — the styled layer
|
|
6
|
+
// applies the same hook to its own button component.
|
|
7
|
+
import { Children, useEffect } from "react";
|
|
8
|
+
import { useRenderElement } from "../internal/render/useRenderElement.js";
|
|
9
|
+
import { openStateMapping } from "../internal/state-mappings.js";
|
|
10
|
+
import { useAsRef } from "./internals.js";
|
|
11
|
+
import { useComposer } from "./store.js";
|
|
12
|
+
export const ComposerContextWindow = ({ children, className, render, style, ...elementProps }) => {
|
|
13
|
+
// Yield while any native panel is active — the strip slides back out once they
|
|
14
|
+
// close. (Union of the native slices; no central registry.)
|
|
15
|
+
const anyPanelActive = useComposer((composer) => composer.commands.active || composer.askUser.active);
|
|
16
|
+
const hasContent = Children.toArray(children).length > 0;
|
|
17
|
+
const open = hasContent && !anyPanelActive;
|
|
18
|
+
return useRenderElement("div", { className, render, style }, {
|
|
19
|
+
state: { open },
|
|
20
|
+
stateAttributesMapping: openStateMapping,
|
|
21
|
+
props: [{ "data-composer-context-window": "", children }, elementProps],
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
export const ComposerActions = ({ className, render, style, ...elementProps }) => useRenderElement("div", { className, render, style }, { props: [{ "data-composer-actions": "" }, elementProps] });
|
|
25
|
+
/**
|
|
26
|
+
* Send↔stop behavior: auto-disables while empty or submitting, flips the
|
|
27
|
+
* button type while generating, and aborts on Escape (unless something closer
|
|
28
|
+
* — the command list — already handled it).
|
|
29
|
+
*/
|
|
30
|
+
export const useComposerSubmit = ({ isGenerating = false, onStop, disabled, }) => {
|
|
31
|
+
const hasContent = useComposer((composer) => composer.textarea.hasContent);
|
|
32
|
+
const isSubmitting = useComposer((composer) => composer.isSubmitting);
|
|
33
|
+
const attachments = useComposer((composer) => composer.attachments);
|
|
34
|
+
// Esc aborts while generating — unless something already handled it (the
|
|
35
|
+
// command-list closes on Esc and preventDefaults first, so it wins).
|
|
36
|
+
const onStopRef = useAsRef(onStop);
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (!isGenerating)
|
|
39
|
+
return;
|
|
40
|
+
const handleKeyDown = (event) => {
|
|
41
|
+
if (event.key !== "Escape" || event.defaultPrevented)
|
|
42
|
+
return;
|
|
43
|
+
event.preventDefault();
|
|
44
|
+
onStopRef.current?.();
|
|
45
|
+
};
|
|
46
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
47
|
+
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
48
|
+
}, [isGenerating]);
|
|
49
|
+
const autoDisabled = disabled ?? ((!hasContent && attachments.items.length === 0) || isSubmitting);
|
|
50
|
+
return {
|
|
51
|
+
type: isGenerating ? "button" : "submit",
|
|
52
|
+
disabled: isGenerating ? false : autoDisabled,
|
|
53
|
+
isGenerating,
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
export const ComposerSubmit = ({ disabled, isGenerating = false, onStop, onClick, className, render, style, ...elementProps }) => {
|
|
57
|
+
const submit = useComposerSubmit({ isGenerating, onStop, disabled });
|
|
58
|
+
return useRenderElement("button", { className, render, style }, {
|
|
59
|
+
state: { generating: isGenerating },
|
|
60
|
+
props: [
|
|
61
|
+
{
|
|
62
|
+
type: submit.type,
|
|
63
|
+
"data-composer-submit": "",
|
|
64
|
+
// Default overridable names for the (typically icon-only) morphing
|
|
65
|
+
// control — both states announce, not just the stop affordance.
|
|
66
|
+
"aria-label": isGenerating ? "Stop generating" : "Send message",
|
|
67
|
+
disabled: submit.disabled,
|
|
68
|
+
// Replaces (not chains) the consumer's onClick while generating —
|
|
69
|
+
// a stop click must never fall through to submit handlers.
|
|
70
|
+
onClick: isGenerating ? () => onStop?.() : onClick,
|
|
71
|
+
},
|
|
72
|
+
elementProps,
|
|
73
|
+
],
|
|
74
|
+
});
|
|
75
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { AskUserQuestion, ComposerAnswerEntry } from "./types.js";
|
|
2
|
+
export type AnswerEntry = {
|
|
3
|
+
selected: Set<string>;
|
|
4
|
+
freeText: string;
|
|
5
|
+
};
|
|
6
|
+
export type AskUserState = {
|
|
7
|
+
step: number;
|
|
8
|
+
answers: Map<number, AnswerEntry>;
|
|
9
|
+
};
|
|
10
|
+
export declare const INITIAL_ASK_USER_STATE: AskUserState;
|
|
11
|
+
export type AskUserEffect = {
|
|
12
|
+
type: "clear-input";
|
|
13
|
+
} | {
|
|
14
|
+
type: "set-input-text";
|
|
15
|
+
text: string;
|
|
16
|
+
} | {
|
|
17
|
+
type: "focus-input";
|
|
18
|
+
} | {
|
|
19
|
+
type: "focus-options";
|
|
20
|
+
} | {
|
|
21
|
+
type: "reset-highlight";
|
|
22
|
+
} | {
|
|
23
|
+
type: "submit-answers";
|
|
24
|
+
answers: ComposerAnswerEntry[];
|
|
25
|
+
};
|
|
26
|
+
export type AskUserAction = {
|
|
27
|
+
type: "toggle-option";
|
|
28
|
+
label: string;
|
|
29
|
+
} | {
|
|
30
|
+
type: "select-option";
|
|
31
|
+
label: string;
|
|
32
|
+
} | {
|
|
33
|
+
type: "clear-selections";
|
|
34
|
+
} | {
|
|
35
|
+
type: "continue-step";
|
|
36
|
+
freeText: string;
|
|
37
|
+
} | {
|
|
38
|
+
type: "dismiss-step";
|
|
39
|
+
} | {
|
|
40
|
+
type: "step-back";
|
|
41
|
+
currentText: string;
|
|
42
|
+
} | {
|
|
43
|
+
type: "step-forward";
|
|
44
|
+
currentText: string;
|
|
45
|
+
};
|
|
46
|
+
export type AskUserTransition = {
|
|
47
|
+
next: AskUserState;
|
|
48
|
+
effects: AskUserEffect[];
|
|
49
|
+
};
|
|
50
|
+
export declare const compileAnswers: (state: AskUserState, questions: AskUserQuestion[]) => ComposerAnswerEntry[];
|
|
51
|
+
export declare const isLastStep: (state: AskUserState, questions: AskUserQuestion[]) => boolean;
|
|
52
|
+
export declare const transitionAskUser: (state: AskUserState, questions: AskUserQuestion[], action: AskUserAction) => AskUserTransition;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// Ask-user — pure state machine for the questionnaire flow. State is a current
|
|
2
|
+
// step plus a map of per-step answers; every transition is an immutable map
|
|
3
|
+
// operation, so the store only owns the state cell and the effect execution.
|
|
4
|
+
export const INITIAL_ASK_USER_STATE = {
|
|
5
|
+
step: 0,
|
|
6
|
+
answers: new Map(),
|
|
7
|
+
};
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Answer helpers — immutable operations on the per-step answer map, plus the
|
|
10
|
+
// projection/query the store reads.
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
const emptyEntry = () => ({
|
|
13
|
+
selected: new Set(),
|
|
14
|
+
freeText: "",
|
|
15
|
+
});
|
|
16
|
+
const cloneAnswers = (source) => new Map(source);
|
|
17
|
+
// Record `text` as the step's free-text answer. Single-select treats text and
|
|
18
|
+
// a chosen option as mutually exclusive, so any selection is dropped;
|
|
19
|
+
// multi-select keeps existing selections alongside the text. Empty text is a
|
|
20
|
+
// no-op (returns an untouched clone).
|
|
21
|
+
const writeFreeText = (answers, step, text, multiSelect) => {
|
|
22
|
+
const next = cloneAnswers(answers);
|
|
23
|
+
if (text.length === 0)
|
|
24
|
+
return next;
|
|
25
|
+
const previous = next.get(step) ?? emptyEntry();
|
|
26
|
+
next.set(step, {
|
|
27
|
+
selected: multiSelect ? previous.selected : new Set(),
|
|
28
|
+
freeText: text,
|
|
29
|
+
});
|
|
30
|
+
return next;
|
|
31
|
+
};
|
|
32
|
+
// Remove the step's entry entirely (skip / dismiss).
|
|
33
|
+
const skipStep = (answers, step) => {
|
|
34
|
+
const next = cloneAnswers(answers);
|
|
35
|
+
next.delete(step);
|
|
36
|
+
return next;
|
|
37
|
+
};
|
|
38
|
+
// Toggle an option on the step's entry. Multi-select toggles membership and
|
|
39
|
+
// keeps free text; single-select replaces both (option and text are mutually
|
|
40
|
+
// exclusive).
|
|
41
|
+
const toggleAnswer = (answers, step, label, multiSelect) => {
|
|
42
|
+
const next = cloneAnswers(answers);
|
|
43
|
+
const previous = next.get(step) ?? emptyEntry();
|
|
44
|
+
const selected = new Set(previous.selected);
|
|
45
|
+
if (multiSelect) {
|
|
46
|
+
if (selected.has(label))
|
|
47
|
+
selected.delete(label);
|
|
48
|
+
else
|
|
49
|
+
selected.add(label);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
selected.clear();
|
|
53
|
+
selected.add(label);
|
|
54
|
+
}
|
|
55
|
+
next.set(step, { selected, freeText: multiSelect ? previous.freeText : "" });
|
|
56
|
+
return next;
|
|
57
|
+
};
|
|
58
|
+
// Project the collected answers onto the public ComposerAnswerEntry union, one
|
|
59
|
+
// entry per question in order. The chosen branch encodes the invariant:
|
|
60
|
+
// single-select carries `option` *or* `text`; multi-select carries both;
|
|
61
|
+
// an unanswered question is bare `{ question }`.
|
|
62
|
+
export const compileAnswers = (state, questions) => questions.map(({ question, multiSelect }, index) => {
|
|
63
|
+
const entry = state.answers.get(index);
|
|
64
|
+
if (!entry)
|
|
65
|
+
return { question };
|
|
66
|
+
const selected = [...entry.selected];
|
|
67
|
+
const text = entry.freeText.trim();
|
|
68
|
+
if (multiSelect) {
|
|
69
|
+
if (selected.length === 0 && text === "")
|
|
70
|
+
return { question };
|
|
71
|
+
return { question, options: selected, text };
|
|
72
|
+
}
|
|
73
|
+
const firstSelected = selected[0];
|
|
74
|
+
if (firstSelected !== undefined)
|
|
75
|
+
return { question, option: firstSelected };
|
|
76
|
+
if (text !== "")
|
|
77
|
+
return { question, text };
|
|
78
|
+
return { question };
|
|
79
|
+
});
|
|
80
|
+
export const isLastStep = (state, questions) => state.step >= questions.length - 1;
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// Transitions — the decide half of the flow: map an action onto the next state
|
|
83
|
+
// plus the effects to run. transitionAskUser is the single entry point.
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Commit `nextAnswers`, advance one step, and reset the input — then either
|
|
86
|
+
// arm the next question (blurred, highlight reset) or compile and submit on
|
|
87
|
+
// the last step (input focused for the follow-up message).
|
|
88
|
+
const advanceStep = (state, questions, nextAnswers) => {
|
|
89
|
+
const next = { step: state.step + 1, answers: nextAnswers };
|
|
90
|
+
if (state.step < questions.length - 1) {
|
|
91
|
+
return {
|
|
92
|
+
next,
|
|
93
|
+
effects: [{ type: "clear-input" }, { type: "reset-highlight" }, { type: "focus-options" }],
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
next,
|
|
98
|
+
effects: [
|
|
99
|
+
{ type: "clear-input" },
|
|
100
|
+
{ type: "submit-answers", answers: compileAnswers(next, questions) },
|
|
101
|
+
{ type: "focus-input" },
|
|
102
|
+
],
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
// Navigate to `targetStep`, preserving any in-progress free text on the step
|
|
106
|
+
// being left and restoring the target step's saved text into the input.
|
|
107
|
+
const transitionToStep = (state, targetStep, currentText) => {
|
|
108
|
+
if (targetStep === state.step)
|
|
109
|
+
return { next: state, effects: [] };
|
|
110
|
+
const trimmed = currentText.trim();
|
|
111
|
+
const answers = cloneAnswers(state.answers);
|
|
112
|
+
if (trimmed.length > 0) {
|
|
113
|
+
const previous = answers.get(state.step) ?? emptyEntry();
|
|
114
|
+
answers.set(state.step, { ...previous, freeText: trimmed });
|
|
115
|
+
}
|
|
116
|
+
const next = { step: targetStep, answers };
|
|
117
|
+
return {
|
|
118
|
+
next,
|
|
119
|
+
effects: [
|
|
120
|
+
{ type: "set-input-text", text: answers.get(targetStep)?.freeText ?? "" },
|
|
121
|
+
{ type: "reset-highlight" },
|
|
122
|
+
{ type: "focus-options" },
|
|
123
|
+
],
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
// The decide half of the ask-user flow, mirroring interpretAskUserKey: map an
|
|
127
|
+
// action onto the next state plus the effects to run. Composed actions
|
|
128
|
+
// (select-option = toggle + advance) resolve atomically here, so no caller
|
|
129
|
+
// ever chains transitions across a stale state snapshot.
|
|
130
|
+
export const transitionAskUser = (state, questions, action) => {
|
|
131
|
+
const multiSelect = !!questions[state.step]?.multiSelect;
|
|
132
|
+
switch (action.type) {
|
|
133
|
+
case "toggle-option":
|
|
134
|
+
return {
|
|
135
|
+
next: {
|
|
136
|
+
...state,
|
|
137
|
+
answers: toggleAnswer(state.answers, state.step, action.label, multiSelect),
|
|
138
|
+
},
|
|
139
|
+
effects: multiSelect ? [] : [{ type: "clear-input" }],
|
|
140
|
+
};
|
|
141
|
+
case "select-option": {
|
|
142
|
+
const toggled = toggleAnswer(state.answers, state.step, action.label, multiSelect);
|
|
143
|
+
if (multiSelect)
|
|
144
|
+
return { next: { ...state, answers: toggled }, effects: [] };
|
|
145
|
+
return advanceStep(state, questions, toggled);
|
|
146
|
+
}
|
|
147
|
+
case "clear-selections": {
|
|
148
|
+
const previous = state.answers.get(state.step);
|
|
149
|
+
if (!previous || previous.selected.size === 0)
|
|
150
|
+
return { next: state, effects: [] };
|
|
151
|
+
const answers = cloneAnswers(state.answers);
|
|
152
|
+
answers.set(state.step, { selected: new Set(), freeText: previous.freeText });
|
|
153
|
+
return { next: { ...state, answers }, effects: [] };
|
|
154
|
+
}
|
|
155
|
+
case "continue-step":
|
|
156
|
+
return advanceStep(state, questions, writeFreeText(state.answers, state.step, action.freeText.trim(), multiSelect));
|
|
157
|
+
case "dismiss-step":
|
|
158
|
+
return advanceStep(state, questions, skipStep(state.answers, state.step));
|
|
159
|
+
case "step-back":
|
|
160
|
+
return transitionToStep(state, Math.max(0, state.step - 1), action.currentText);
|
|
161
|
+
case "step-forward":
|
|
162
|
+
return transitionToStep(state, Math.min(questions.length - 1, state.step + 1), action.currentText);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type AttachmentErrorCode, type AttachmentItem } from "../attachments/index.js";
|
|
2
|
+
export type AttachmentStoreState = {
|
|
3
|
+
items: AttachmentItem[];
|
|
4
|
+
error: AttachmentErrorCode | null;
|
|
5
|
+
};
|
|
6
|
+
export type AttachmentStoreAction = {
|
|
7
|
+
type: "add";
|
|
8
|
+
files: File[] | FileList;
|
|
9
|
+
} | {
|
|
10
|
+
type: "remove";
|
|
11
|
+
id: string;
|
|
12
|
+
} | {
|
|
13
|
+
type: "reset";
|
|
14
|
+
};
|
|
15
|
+
export type AttachmentStoreConfig = {
|
|
16
|
+
accept: string;
|
|
17
|
+
maxFiles: number;
|
|
18
|
+
maxFileSize: number;
|
|
19
|
+
convert: (file: File) => AttachmentItem;
|
|
20
|
+
destroy: (item: AttachmentItem) => void;
|
|
21
|
+
};
|
|
22
|
+
export declare const INITIAL_ATTACHMENT_STATE: AttachmentStoreState;
|
|
23
|
+
export declare const attachmentReducer: (state: AttachmentStoreState, action: AttachmentStoreAction, config: AttachmentStoreConfig) => AttachmentStoreState;
|
|
24
|
+
export type DragHandlerCallbacks = {
|
|
25
|
+
isInScope: (event: DragEvent) => boolean;
|
|
26
|
+
onFiles: (files: FileList) => void;
|
|
27
|
+
setDragging: (active: boolean) => void;
|
|
28
|
+
};
|
|
29
|
+
export declare const createDragHandlers: (callbacks: DragHandlerCallbacks) => {
|
|
30
|
+
onDragOver: (event: Event) => void;
|
|
31
|
+
onDragEnter: (event: Event) => void;
|
|
32
|
+
onDragLeave: (event: Event) => void;
|
|
33
|
+
onDrop: (event: Event) => void;
|
|
34
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Attachments — pure store reducer that validates and accumulates files, plus
|
|
2
|
+
// the drag-and-drop handler factory. The store owns the state cell; this owns
|
|
3
|
+
// the rules.
|
|
4
|
+
import { matchesAccept } from "../attachments/index.js";
|
|
5
|
+
export const INITIAL_ATTACHMENT_STATE = {
|
|
6
|
+
items: [],
|
|
7
|
+
error: null,
|
|
8
|
+
};
|
|
9
|
+
export const attachmentReducer = (state, action, config) => {
|
|
10
|
+
switch (action.type) {
|
|
11
|
+
case "add": {
|
|
12
|
+
const incoming = [...action.files];
|
|
13
|
+
if (!incoming.length)
|
|
14
|
+
return state;
|
|
15
|
+
const accepted = incoming.filter((file) => matchesAccept(file, config.accept));
|
|
16
|
+
if (!accepted.length) {
|
|
17
|
+
return { ...state, error: "accept" };
|
|
18
|
+
}
|
|
19
|
+
const sized = accepted.filter((file) => file.size <= config.maxFileSize);
|
|
20
|
+
if (!sized.length) {
|
|
21
|
+
return { ...state, error: "max_file_size" };
|
|
22
|
+
}
|
|
23
|
+
const capacity = Math.max(0, config.maxFiles - state.items.length);
|
|
24
|
+
const capped = sized.slice(0, capacity);
|
|
25
|
+
const overCapacityError = sized.length > capacity ? "max_files" : null;
|
|
26
|
+
if (!capped.length) {
|
|
27
|
+
return { ...state, error: overCapacityError ?? state.error };
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
items: [...state.items, ...capped.map(config.convert)],
|
|
31
|
+
error: overCapacityError,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
case "remove": {
|
|
35
|
+
const found = state.items.find((item) => item.id === action.id);
|
|
36
|
+
if (found)
|
|
37
|
+
config.destroy(found);
|
|
38
|
+
return {
|
|
39
|
+
items: state.items.filter((item) => item.id !== action.id),
|
|
40
|
+
error: null,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
case "reset": {
|
|
44
|
+
for (const item of state.items)
|
|
45
|
+
config.destroy(item);
|
|
46
|
+
return INITIAL_ATTACHMENT_STATE;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
export const createDragHandlers = (callbacks) => {
|
|
51
|
+
const counter = { current: 0 };
|
|
52
|
+
const carriesFiles = (event) => event.dataTransfer?.types?.includes("Files") ?? false;
|
|
53
|
+
return {
|
|
54
|
+
onDragOver: (event) => {
|
|
55
|
+
const dragEvent = event;
|
|
56
|
+
if (!callbacks.isInScope(dragEvent))
|
|
57
|
+
return;
|
|
58
|
+
if (carriesFiles(dragEvent))
|
|
59
|
+
dragEvent.preventDefault();
|
|
60
|
+
},
|
|
61
|
+
onDragEnter: (event) => {
|
|
62
|
+
const dragEvent = event;
|
|
63
|
+
if (!callbacks.isInScope(dragEvent))
|
|
64
|
+
return;
|
|
65
|
+
if (carriesFiles(dragEvent)) {
|
|
66
|
+
counter.current++;
|
|
67
|
+
callbacks.setDragging(true);
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
onDragLeave: (event) => {
|
|
71
|
+
const dragEvent = event;
|
|
72
|
+
if (!callbacks.isInScope(dragEvent))
|
|
73
|
+
return;
|
|
74
|
+
counter.current--;
|
|
75
|
+
if (counter.current === 0)
|
|
76
|
+
callbacks.setDragging(false);
|
|
77
|
+
},
|
|
78
|
+
onDrop: (event) => {
|
|
79
|
+
const dragEvent = event;
|
|
80
|
+
if (!callbacks.isInScope(dragEvent))
|
|
81
|
+
return;
|
|
82
|
+
if (carriesFiles(dragEvent))
|
|
83
|
+
dragEvent.preventDefault();
|
|
84
|
+
counter.current = 0;
|
|
85
|
+
callbacks.setDragging(false);
|
|
86
|
+
const files = dragEvent.dataTransfer?.files;
|
|
87
|
+
if (files && files.length > 0)
|
|
88
|
+
callbacks.onFiles(files);
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { type AttachmentItem } from "../attachments/index.js";
|
|
3
|
+
import type { PrimitiveProps } from "../internal/primitive-props.js";
|
|
4
|
+
export type ComposerAttachmentsProps = {
|
|
5
|
+
convert?: (file: File) => AttachmentItem;
|
|
6
|
+
destroy?: (item: AttachmentItem) => void;
|
|
7
|
+
accept?: string;
|
|
8
|
+
maxFiles?: number;
|
|
9
|
+
maxFileSize?: number;
|
|
10
|
+
multiple?: boolean;
|
|
11
|
+
globalDrop?: boolean;
|
|
12
|
+
children?: ReactNode;
|
|
13
|
+
};
|
|
14
|
+
export declare const ComposerAttachments: ({ convert, destroy, accept, maxFiles, maxFileSize, multiple, globalDrop, children, }: ComposerAttachmentsProps) => import("react").JSX.Element;
|
|
15
|
+
export type ComposerAttachmentTriggerProps = PrimitiveProps<"button">;
|
|
16
|
+
export declare const ComposerAttachmentTrigger: ({ className, render, style, ...elementProps }: ComposerAttachmentTriggerProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { revokeAttachmentUrl, toAttachmentItem } from "../attachments/index.js";
|
|
4
|
+
import { useRenderElement } from "../internal/render/useRenderElement.js";
|
|
5
|
+
import { useComposer, useComposerContextStore } from "./store.js";
|
|
6
|
+
export const ComposerAttachments = ({ convert = toAttachmentItem, destroy = revokeAttachmentUrl, accept = "", maxFiles = Number.POSITIVE_INFINITY, maxFileSize = Number.POSITIVE_INFINITY, multiple = true, globalDrop = false, children, }) => {
|
|
7
|
+
const store = useComposerContextStore();
|
|
8
|
+
const attachments = useComposer((composer) => composer.attachments);
|
|
9
|
+
store.attachmentConfigRef.current = { accept, maxFiles, maxFileSize, convert, destroy };
|
|
10
|
+
attachments.globalDropRef.current = globalDrop;
|
|
11
|
+
const handleFileChange = (event) => {
|
|
12
|
+
if (event.currentTarget.files) {
|
|
13
|
+
attachments.add(event.currentTarget.files);
|
|
14
|
+
}
|
|
15
|
+
event.currentTarget.value = "";
|
|
16
|
+
};
|
|
17
|
+
return (_jsxs(_Fragment, { children: [_jsx("input", { accept: accept, style: { display: "none" }, multiple: multiple, onChange: handleFileChange, ref: attachments.fileInputRef, type: "file" }), children] }));
|
|
18
|
+
};
|
|
19
|
+
export const ComposerAttachmentTrigger = ({ className, render, style, ...elementProps }) => {
|
|
20
|
+
const attachments = useComposer((composer) => composer.attachments);
|
|
21
|
+
return useRenderElement("button", { className, render, style }, {
|
|
22
|
+
props: [
|
|
23
|
+
{
|
|
24
|
+
// Default overridable name — the trigger is typically icon-only.
|
|
25
|
+
"aria-label": "Add attachment",
|
|
26
|
+
"data-composer-attachment-trigger": "",
|
|
27
|
+
onClick: () => attachments.openFileDialog(),
|
|
28
|
+
},
|
|
29
|
+
elementProps,
|
|
30
|
+
],
|
|
31
|
+
});
|
|
32
|
+
};
|