@elabs-ai/components-ai 4.0.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 +133 -0
- package/dist/_audio-player-media-chrome-KA5DY54G.js +81 -0
- package/dist/_audio-player-media-chrome-KA5DY54G.js.map +1 -0
- package/dist/_flow-boundary-D63PJ65S.js +186 -0
- package/dist/_flow-boundary-D63PJ65S.js.map +1 -0
- package/dist/_interactive-terminal-xterm-ATJ5EW3G.js +11 -0
- package/dist/_interactive-terminal-xterm-ATJ5EW3G.js.map +1 -0
- package/dist/_persona-rive-RFR2EUWP.js +164 -0
- package/dist/_persona-rive-RFR2EUWP.js.map +1 -0
- package/dist/index.d.ts +4079 -0
- package/dist/index.js +11770 -0
- package/dist/index.js.map +1 -0
- package/package.json +87 -0
- package/src/_audio-player-media-chrome.tsx +146 -0
- package/src/_chat-shell-rail.tsx +18 -0
- package/src/_code-block-theme.test.ts +134 -0
- package/src/_code-block-theme.ts +184 -0
- package/src/_flow-boundary.tsx +237 -0
- package/src/_flow-lazy.ts +34 -0
- package/src/_interactive-terminal-xterm.ts +32 -0
- package/src/_lazy-mermaid.test.ts +101 -0
- package/src/_lazy-mermaid.ts +117 -0
- package/src/_persona-rive.tsx +252 -0
- package/src/_streamdown-i18n.ts +119 -0
- package/src/agent-timeline.stories.tsx +66 -0
- package/src/agent-timeline.test.tsx +74 -0
- package/src/agent-timeline.tsx +106 -0
- package/src/agent.tsx +113 -0
- package/src/agentic-workspace.stories.tsx +1099 -0
- package/src/artifact.stories.tsx +62 -0
- package/src/artifact.test.tsx +112 -0
- package/src/artifact.tsx +167 -0
- package/src/asset-preview.stories.tsx +100 -0
- package/src/asset-preview.test.tsx +126 -0
- package/src/asset-preview.tsx +218 -0
- package/src/attachments.tsx +393 -0
- package/src/audio-player.stories.tsx +99 -0
- package/src/audio-player.tsx +170 -0
- package/src/blocks-ai-composer.stories.tsx +83 -0
- package/src/canvas.stories.tsx +142 -0
- package/src/canvas.tsx +29 -0
- package/src/chain-of-thought.tsx +193 -0
- package/src/chat-greeting.stories.tsx +96 -0
- package/src/chat-greeting.test.tsx +91 -0
- package/src/chat-greeting.tsx +91 -0
- package/src/chat-shell.stories.tsx +123 -0
- package/src/chat-shell.test.tsx +113 -0
- package/src/chat-shell.tsx +97 -0
- package/src/chat.stories.tsx +99 -0
- package/src/checkpoint.tsx +54 -0
- package/src/code-block.stories.tsx +81 -0
- package/src/code-block.test.tsx +191 -0
- package/src/code-block.tsx +673 -0
- package/src/commit.tsx +343 -0
- package/src/composer.stories.tsx +188 -0
- package/src/composer.test.tsx +182 -0
- package/src/composer.tsx +202 -0
- package/src/confirmation.stories.tsx +61 -0
- package/src/confirmation.test.tsx +136 -0
- package/src/confirmation.tsx +273 -0
- package/src/connection.tsx +23 -0
- package/src/context-panel.stories.tsx +137 -0
- package/src/context-panel.test.tsx +198 -0
- package/src/context-panel.tsx +609 -0
- package/src/context.stories.tsx +16 -0
- package/src/context.tsx +363 -0
- package/src/controls.tsx +20 -0
- package/src/conversation.stories.tsx +26 -0
- package/src/conversation.test.tsx +123 -0
- package/src/conversation.tsx +143 -0
- package/src/dark-theme-variant.test.ts +47 -0
- package/src/edge.tsx +33 -0
- package/src/environment-variables.tsx +304 -0
- package/src/file-tree.stories.tsx +55 -0
- package/src/file-tree.test.tsx +101 -0
- package/src/file-tree.tsx +390 -0
- package/src/gallery.stories.tsx +182 -0
- package/src/gallery.test.tsx +150 -0
- package/src/gallery.tsx +737 -0
- package/src/grouped-parts.stories.tsx +104 -0
- package/src/grouped-parts.test.tsx +167 -0
- package/src/grouped-parts.tsx +247 -0
- package/src/image.stories.tsx +67 -0
- package/src/image.test.tsx +105 -0
- package/src/image.tsx +115 -0
- package/src/index.ts +93 -0
- package/src/inline-citation.stories.tsx +91 -0
- package/src/inline-citation.test.tsx +96 -0
- package/src/inline-citation.tsx +314 -0
- package/src/interactive-terminal.stories.tsx +165 -0
- package/src/interactive-terminal.test.tsx +448 -0
- package/src/interactive-terminal.tsx +444 -0
- package/src/jsx-preview.stories.tsx +265 -0
- package/src/jsx-preview.test.tsx +75 -0
- package/src/jsx-preview.tsx +409 -0
- package/src/markdown-view.stories.tsx +57 -0
- package/src/markdown-view.test.tsx +36 -0
- package/src/markdown-view.tsx +113 -0
- package/src/message-edit.stories.tsx +150 -0
- package/src/message-edit.test.tsx +115 -0
- package/src/message-edit.tsx +336 -0
- package/src/message-feedback.stories.tsx +53 -0
- package/src/message-feedback.test.tsx +62 -0
- package/src/message-feedback.tsx +117 -0
- package/src/message-form-spec.ts +349 -0
- package/src/message-form.stories.tsx +182 -0
- package/src/message-form.test.tsx +227 -0
- package/src/message-form.tsx +845 -0
- package/src/message-table-spec.ts +281 -0
- package/src/message-table.stories.tsx +176 -0
- package/src/message-table.test.tsx +144 -0
- package/src/message-table.tsx +366 -0
- package/src/message.stories.tsx +262 -0
- package/src/message.test.tsx +508 -0
- package/src/message.tsx +619 -0
- package/src/mic-selector.tsx +339 -0
- package/src/microcopy.test.tsx +123 -0
- package/src/model-selector.stories.tsx +73 -0
- package/src/model-selector.test.tsx +55 -0
- package/src/model-selector.tsx +225 -0
- package/src/motion-config.tsx +49 -0
- package/src/node.tsx +71 -0
- package/src/open-in-chat.tsx +340 -0
- package/src/package-info.tsx +205 -0
- package/src/panel.tsx +20 -0
- package/src/part-groups.ts +258 -0
- package/src/persona-sources.ts +59 -0
- package/src/persona.stories.tsx +111 -0
- package/src/persona.test.tsx +67 -0
- package/src/persona.tsx +148 -0
- package/src/plan.tsx +135 -0
- package/src/prompt-input.stories.tsx +261 -0
- package/src/prompt-input.test.tsx +392 -0
- package/src/prompt-input.tsx +1668 -0
- package/src/queue.tsx +237 -0
- package/src/reasoning.stories.tsx +81 -0
- package/src/reasoning.test.tsx +50 -0
- package/src/reasoning.tsx +229 -0
- package/src/sandbox.stories.tsx +62 -0
- package/src/sandbox.test.tsx +42 -0
- package/src/sandbox.tsx +149 -0
- package/src/schema-display.tsx +412 -0
- package/src/selection-toolbar.stories.tsx +86 -0
- package/src/selection-toolbar.test.tsx +89 -0
- package/src/selection-toolbar.tsx +192 -0
- package/src/shimmer.stories.tsx +10 -0
- package/src/shimmer.tsx +81 -0
- package/src/snippet.stories.tsx +19 -0
- package/src/snippet.test.tsx +116 -0
- package/src/snippet.tsx +138 -0
- package/src/sources.stories.tsx +34 -0
- package/src/sources.test.tsx +129 -0
- package/src/sources.tsx +93 -0
- package/src/speech-input.tsx +312 -0
- package/src/stack-trace.tsx +479 -0
- package/src/streamdown-i18n.test.tsx +163 -0
- package/src/suggestion.stories.tsx +75 -0
- package/src/suggestion.test.tsx +72 -0
- package/src/suggestion.tsx +120 -0
- package/src/task.stories.tsx +22 -0
- package/src/task.test.tsx +36 -0
- package/src/task.tsx +89 -0
- package/src/templates-ai-assistant.stories.tsx +142 -0
- package/src/terminal.tsx +244 -0
- package/src/test-results.stories.tsx +177 -0
- package/src/test-results.test.tsx +241 -0
- package/src/test-results.tsx +408 -0
- package/src/tool-result-card.stories.tsx +143 -0
- package/src/tool-result-card.test.tsx +72 -0
- package/src/tool-result-card.tsx +86 -0
- package/src/tool.stories.tsx +60 -0
- package/src/tool.test.tsx +63 -0
- package/src/tool.tsx +235 -0
- package/src/toolbar.tsx +44 -0
- package/src/transcription.tsx +118 -0
- package/src/voice-selector.tsx +469 -0
- package/src/web-preview.stories.tsx +67 -0
- package/src/web-preview.test.tsx +117 -0
- package/src/web-preview.tsx +283 -0
package/src/image.tsx
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Skeleton, useLocale } from "@elabs-ai/components-ui";
|
|
4
|
+
import { cn } from "@elabs-ai/components-ui/lib/cn";
|
|
5
|
+
import type { Experimental_GeneratedImage } from "ai";
|
|
6
|
+
import { ImageOff } from "lucide-react";
|
|
7
|
+
import type { ImgHTMLAttributes } from "react";
|
|
8
|
+
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react";
|
|
9
|
+
|
|
10
|
+
export type ImageProps = Experimental_GeneratedImage &
|
|
11
|
+
Omit<ImgHTMLAttributes<HTMLImageElement>, "src"> & {
|
|
12
|
+
/**
|
|
13
|
+
* Show a Skeleton placeholder (loading-states.md) until the img element
|
|
14
|
+
* finishes decoding, so the base64 payload does not pop in. Requires
|
|
15
|
+
* explicit `width`+`height` so the reserved box matches the final size
|
|
16
|
+
* (interaction-guidelines.md Images section — prevents CLS); without
|
|
17
|
+
* both there is no box to reserve, so the default is `false` in that
|
|
18
|
+
* case rather than mounting a skeleton that collapses to nothing.
|
|
19
|
+
* @default Boolean(width && height)
|
|
20
|
+
*/
|
|
21
|
+
showSkeleton?: boolean;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** `<img>` wrapper for an AI-SDK generated image, with a decode-aware skeleton. */
|
|
25
|
+
export const Image = forwardRef<HTMLImageElement, ImageProps>(function Image(
|
|
26
|
+
{
|
|
27
|
+
base64,
|
|
28
|
+
uint8Array: _uint8Array,
|
|
29
|
+
mediaType,
|
|
30
|
+
className,
|
|
31
|
+
showSkeleton,
|
|
32
|
+
onLoad,
|
|
33
|
+
onError,
|
|
34
|
+
width,
|
|
35
|
+
height,
|
|
36
|
+
alt,
|
|
37
|
+
...props
|
|
38
|
+
},
|
|
39
|
+
forwardedRef,
|
|
40
|
+
) {
|
|
41
|
+
const { t } = useLocale();
|
|
42
|
+
const shouldSkeleton = showSkeleton ?? Boolean(width && height);
|
|
43
|
+
const [loaded, setLoaded] = useState(false);
|
|
44
|
+
const [failed, setFailed] = useState(false);
|
|
45
|
+
const imgRef = useRef<HTMLImageElement>(null);
|
|
46
|
+
useImperativeHandle(forwardedRef, () => imgRef.current as HTMLImageElement);
|
|
47
|
+
|
|
48
|
+
const pending = shouldSkeleton && !loaded && !failed;
|
|
49
|
+
|
|
50
|
+
// The live region must exist BEFORE its text appears (ARIA22), so it is
|
|
51
|
+
// always mounted (while a skeleton can render at all) and its label is set
|
|
52
|
+
// one tick later via this effect.
|
|
53
|
+
const [announce, setAnnounce] = useState(false);
|
|
54
|
+
useEffect(() => setAnnounce(pending), [pending]);
|
|
55
|
+
|
|
56
|
+
// A cached / instant data-URI decode can finish before React attaches
|
|
57
|
+
// `onLoad` — read `complete` once mounted (gallery.tsx's GalleryImg has the
|
|
58
|
+
// same guard).
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (imgRef.current?.complete) setLoaded(true);
|
|
61
|
+
}, []);
|
|
62
|
+
|
|
63
|
+
if (failed) {
|
|
64
|
+
return (
|
|
65
|
+
<span
|
|
66
|
+
className={cn(
|
|
67
|
+
"flex flex-col items-center justify-center gap-1 rounded-md bg-muted text-muted-foreground",
|
|
68
|
+
className,
|
|
69
|
+
)}
|
|
70
|
+
style={{ height, width }}
|
|
71
|
+
{...(alt ? { role: "img", "aria-label": alt } : { "aria-hidden": true })}
|
|
72
|
+
>
|
|
73
|
+
<ImageOff className="size-6" aria-hidden="true" />
|
|
74
|
+
</span>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const img = (
|
|
79
|
+
<img
|
|
80
|
+
{...props}
|
|
81
|
+
ref={imgRef}
|
|
82
|
+
alt={alt ?? ""}
|
|
83
|
+
height={height}
|
|
84
|
+
width={width}
|
|
85
|
+
className={cn("h-auto max-w-full overflow-hidden rounded-md", className)}
|
|
86
|
+
src={`data:${mediaType};base64,${base64}`}
|
|
87
|
+
onLoad={(event) => {
|
|
88
|
+
setLoaded(true);
|
|
89
|
+
onLoad?.(event);
|
|
90
|
+
}}
|
|
91
|
+
onError={(event) => {
|
|
92
|
+
setFailed(true);
|
|
93
|
+
onError?.(event);
|
|
94
|
+
}}
|
|
95
|
+
/>
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
// No skeleton to reserve a box for — render the bare <img> exactly as
|
|
99
|
+
// before this feature landed (a `className` like "w-full" keeps resolving
|
|
100
|
+
// against the real parent, not a shrink-to-fit wrapper), and mount no
|
|
101
|
+
// (permanently empty) live region.
|
|
102
|
+
if (!shouldSkeleton) return img;
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<span className="relative inline-block max-w-full" style={{ height, width }}>
|
|
106
|
+
<span className="sr-only" role="status" aria-live="polite">
|
|
107
|
+
{announce ? t("loading") : ""}
|
|
108
|
+
</span>
|
|
109
|
+
{pending ? <Skeleton className="absolute inset-0 size-full rounded-md" /> : null}
|
|
110
|
+
{img}
|
|
111
|
+
</span>
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
Image.displayName = "Image";
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @elabs-ai/components-ai — AI Elements ported onto the brand-ui design system.
|
|
3
|
+
*
|
|
4
|
+
* Components are presentational and render the Vercel AI SDK's `UIMessage`
|
|
5
|
+
* data model; the consuming app still owns model calls (e.g. `useChat`).
|
|
6
|
+
*
|
|
7
|
+
* ATTRIBUTION. Vendored from AI Elements (https://github.com/vercel/ai-elements,
|
|
8
|
+
* https://elements.ai-sdk.dev):
|
|
9
|
+
*
|
|
10
|
+
* Copyright 2023 Vercel, Inc.
|
|
11
|
+
* Licensed under the Apache License, Version 2.0.
|
|
12
|
+
* https://www.apache.org/licenses/LICENSE-2.0
|
|
13
|
+
*
|
|
14
|
+
* MODIFICATIONS: these files have been changed from the originals — rewired to
|
|
15
|
+
* import primitives from `@elabs-ai/components-ui` and tokens from
|
|
16
|
+
* `@elabs-ai/components-tokens`, re-tokenized, and extended with surfaces that do
|
|
17
|
+
* not exist upstream. Stating that is an Apache-2.0 §4(b) obligation, not a
|
|
18
|
+
* courtesy — this is the notice that discharges it for the whole package.
|
|
19
|
+
*
|
|
20
|
+
* See ATTRIBUTION.md and .claude/rules/attribution.md.
|
|
21
|
+
*/
|
|
22
|
+
export * from "./agent";
|
|
23
|
+
export * from "./agent-timeline";
|
|
24
|
+
export * from "./artifact";
|
|
25
|
+
export * from "./asset-preview";
|
|
26
|
+
export * from "./attachments";
|
|
27
|
+
export * from "./audio-player";
|
|
28
|
+
export * from "./canvas";
|
|
29
|
+
export * from "./chain-of-thought";
|
|
30
|
+
export * from "./chat-greeting";
|
|
31
|
+
export * from "./checkpoint";
|
|
32
|
+
export * from "./code-block";
|
|
33
|
+
export * from "./commit";
|
|
34
|
+
export * from "./composer";
|
|
35
|
+
export * from "./confirmation";
|
|
36
|
+
export * from "./connection";
|
|
37
|
+
export * from "./context";
|
|
38
|
+
export * from "./context-panel";
|
|
39
|
+
export * from "./controls";
|
|
40
|
+
export * from "./conversation";
|
|
41
|
+
export * from "./edge";
|
|
42
|
+
export * from "./environment-variables";
|
|
43
|
+
export * from "./file-tree";
|
|
44
|
+
export * from "./gallery";
|
|
45
|
+
export * from "./grouped-parts";
|
|
46
|
+
export * from "./image";
|
|
47
|
+
export * from "./inline-citation";
|
|
48
|
+
export * from "./interactive-terminal";
|
|
49
|
+
export * from "./jsx-preview";
|
|
50
|
+
// Mermaid loads on first diagram render, not in the entry chunk. `preloadMermaid`
|
|
51
|
+
// warms it early when a surface is known to render diagrams.
|
|
52
|
+
export { createLazyMermaidPlugin, lazyMermaid, preloadMermaid } from "./_lazy-mermaid";
|
|
53
|
+
export * from "./markdown-view";
|
|
54
|
+
export * from "./message";
|
|
55
|
+
export * from "./message-edit";
|
|
56
|
+
export * from "./message-feedback";
|
|
57
|
+
export * from "./message-form";
|
|
58
|
+
export * from "./message-form-spec";
|
|
59
|
+
export * from "./message-table";
|
|
60
|
+
export * from "./message-table-spec";
|
|
61
|
+
export * from "./mic-selector";
|
|
62
|
+
export * from "./model-selector";
|
|
63
|
+
export * from "./motion-config";
|
|
64
|
+
export * from "./node";
|
|
65
|
+
export * from "./open-in-chat";
|
|
66
|
+
export * from "./package-info";
|
|
67
|
+
export * from "./panel";
|
|
68
|
+
export * from "./part-groups";
|
|
69
|
+
export * from "./persona";
|
|
70
|
+
export * from "./plan";
|
|
71
|
+
export * from "./prompt-input";
|
|
72
|
+
export * from "./queue";
|
|
73
|
+
export * from "./reasoning";
|
|
74
|
+
export * from "./sandbox";
|
|
75
|
+
export * from "./schema-display";
|
|
76
|
+
export * from "./selection-toolbar";
|
|
77
|
+
export * from "./shimmer";
|
|
78
|
+
export * from "./snippet";
|
|
79
|
+
export * from "./sources";
|
|
80
|
+
export * from "./speech-input";
|
|
81
|
+
export * from "./stack-trace";
|
|
82
|
+
export * from "./suggestion";
|
|
83
|
+
export * from "./task";
|
|
84
|
+
export * from "./terminal";
|
|
85
|
+
export * from "./test-results";
|
|
86
|
+
export * from "./tool";
|
|
87
|
+
export * from "./tool-result-card";
|
|
88
|
+
export * from "./toolbar";
|
|
89
|
+
export * from "./transcription";
|
|
90
|
+
export * from "./voice-selector";
|
|
91
|
+
export * from "./web-preview";
|
|
92
|
+
|
|
93
|
+
export * from "./chat-shell";
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import {
|
|
3
|
+
EvidenceChip,
|
|
4
|
+
InlineCitation,
|
|
5
|
+
InlineCitationCard,
|
|
6
|
+
InlineCitationCardBody,
|
|
7
|
+
InlineCitationCardTrigger,
|
|
8
|
+
InlineCitationText,
|
|
9
|
+
} from "./inline-citation";
|
|
10
|
+
const meta = {
|
|
11
|
+
title: "AI/InlineCitation",
|
|
12
|
+
component: InlineCitation,
|
|
13
|
+
parameters: { layout: "padded" },
|
|
14
|
+
} satisfies Meta<typeof InlineCitation>;
|
|
15
|
+
export default meta;
|
|
16
|
+
type Story = StoryObj<typeof meta>;
|
|
17
|
+
export const Default: Story = {
|
|
18
|
+
render: () => (
|
|
19
|
+
<p className="max-w-prose text-body">
|
|
20
|
+
The nightly deploy succeeded
|
|
21
|
+
<InlineCitation>
|
|
22
|
+
<InlineCitationText> on the main branch</InlineCitationText>
|
|
23
|
+
<InlineCitationCard>
|
|
24
|
+
<InlineCitationCardTrigger sources={["https://ci.acme.com/run/42"]} />
|
|
25
|
+
<InlineCitationCardBody>
|
|
26
|
+
<div className="p-3 text-meta text-muted-foreground">
|
|
27
|
+
CI run #42 — all checks passed.
|
|
28
|
+
</div>
|
|
29
|
+
</InlineCitationCardBody>
|
|
30
|
+
</InlineCitationCard>
|
|
31
|
+
</InlineCitation>
|
|
32
|
+
.
|
|
33
|
+
</p>
|
|
34
|
+
),
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Sources that are not URLs — the enterprise case.
|
|
38
|
+
*
|
|
39
|
+
* A warehouse table, a document id or a filing has no hostname to show, so
|
|
40
|
+
* `sources` accepts an opaque string or a `{ id, label, url }` object. A bare
|
|
41
|
+
* non-URL string renders verbatim; `label` wins over a hostname. Nothing here
|
|
42
|
+
* has to synthesize a fake URL to satisfy the chip.
|
|
43
|
+
*/
|
|
44
|
+
export const NonUrlSources: Story = {
|
|
45
|
+
render: () => (
|
|
46
|
+
<p className="max-w-prose text-body">
|
|
47
|
+
Q3 bookings finished 4% ahead of plan
|
|
48
|
+
<InlineCitation>
|
|
49
|
+
<InlineCitationText> in the EMEA segment</InlineCitationText>
|
|
50
|
+
<InlineCitationCard>
|
|
51
|
+
<EvidenceChip
|
|
52
|
+
sources={[
|
|
53
|
+
{ id: "warehouse.public.bookings_q3", label: "Bookings (Q3 snapshot)" },
|
|
54
|
+
"Q3 FY25 Board Deck.pdf",
|
|
55
|
+
{ id: "doc-4821" },
|
|
56
|
+
]}
|
|
57
|
+
/>
|
|
58
|
+
<InlineCitationCardBody>
|
|
59
|
+
<div className="p-3 text-meta text-muted-foreground">
|
|
60
|
+
Internal warehouse table + the Q3 board deck. Neither has a URL.
|
|
61
|
+
</div>
|
|
62
|
+
</InlineCitationCardBody>
|
|
63
|
+
</InlineCitationCard>
|
|
64
|
+
</InlineCitation>
|
|
65
|
+
.
|
|
66
|
+
</p>
|
|
67
|
+
),
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// EvidenceChip — the named green "grounded" front door (#191, research 11 §B.4).
|
|
71
|
+
export const Evidence: Story = {
|
|
72
|
+
render: () => (
|
|
73
|
+
<p className="max-w-prose text-body">
|
|
74
|
+
Churn improved 0.4pp quarter-over-quarter
|
|
75
|
+
<InlineCitation>
|
|
76
|
+
<InlineCitationText> across enterprise accounts</InlineCitationText>
|
|
77
|
+
<InlineCitationCard>
|
|
78
|
+
<EvidenceChip
|
|
79
|
+
sources={["https://warehouse.acme.com/q3-retention", "https://crm.acme.com/cohorts"]}
|
|
80
|
+
/>
|
|
81
|
+
<InlineCitationCardBody>
|
|
82
|
+
<div className="p-3 text-meta text-muted-foreground">
|
|
83
|
+
Q3 retention cohort, refreshed nightly.
|
|
84
|
+
</div>
|
|
85
|
+
</InlineCitationCardBody>
|
|
86
|
+
</InlineCitationCard>
|
|
87
|
+
</InlineCitation>
|
|
88
|
+
.
|
|
89
|
+
</p>
|
|
90
|
+
),
|
|
91
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
EvidenceChip,
|
|
6
|
+
InlineCitationCard,
|
|
7
|
+
InlineCitationCardTrigger,
|
|
8
|
+
inlineCitationSourceLabel,
|
|
9
|
+
type InlineCitationSourceRef,
|
|
10
|
+
} from "./inline-citation";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* `InlineCitationCardTrigger` renders a `HoverCardTrigger`, which Radix throws
|
|
14
|
+
* on outside a `HoverCard` — every case must be wrapped.
|
|
15
|
+
*/
|
|
16
|
+
const renderTrigger = (sources: InlineCitationSourceRef[]) =>
|
|
17
|
+
render(
|
|
18
|
+
<InlineCitationCard>
|
|
19
|
+
<InlineCitationCardTrigger sources={sources} />
|
|
20
|
+
</InlineCitationCard>,
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
describe("InlineCitationCardTrigger — source labels", () => {
|
|
24
|
+
it("shows the hostname for a URL source", () => {
|
|
25
|
+
renderTrigger(["https://ci.acme.com/run/42"]);
|
|
26
|
+
expect(screen.getByText(/ci\.acme\.com/)).toBeInTheDocument();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("renders a NON-URL source verbatim instead of throwing", () => {
|
|
30
|
+
// The P0: `new URL("Q3 filing, FY25")` throws TypeError, and with no error
|
|
31
|
+
// boundary in @elabs-ai/components-ai that took down the entire message subtree. An agent
|
|
32
|
+
// citing a document id or a table name must not have to fake a URL.
|
|
33
|
+
expect(() => renderTrigger(["Q3 filing, FY25"])).not.toThrow();
|
|
34
|
+
expect(screen.getByText(/Q3 filing, FY25/)).toBeInTheDocument();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it.each([
|
|
38
|
+
["a bare document id", "doc-4821"],
|
|
39
|
+
["a scheme-less host", "ci.acme.com/run/42"],
|
|
40
|
+
["a root-relative path", "/reports/q3.pdf"],
|
|
41
|
+
["a human title with punctuation", "Internal Wiki › Pricing"],
|
|
42
|
+
["a filename", "Q3 Revenue Report.pdf"],
|
|
43
|
+
])("does not throw on %s", (_label, source) => {
|
|
44
|
+
expect(() => renderTrigger([source])).not.toThrow();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("prefers an explicit label over the URL hostname", () => {
|
|
48
|
+
renderTrigger([{ label: "Q3 Revenue Report", url: "https://docs.acme.com/q3" }]);
|
|
49
|
+
expect(screen.getByText(/Q3 Revenue Report/)).toBeInTheDocument();
|
|
50
|
+
expect(screen.queryByText(/docs\.acme\.com/)).not.toBeInTheDocument();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("falls back to the hostname when an object has only a url", () => {
|
|
54
|
+
renderTrigger([{ url: "https://docs.acme.com/q3" }]);
|
|
55
|
+
expect(screen.getByText(/docs\.acme\.com/)).toBeInTheDocument();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("falls back to the opaque id when an object has neither label nor url", () => {
|
|
59
|
+
renderTrigger([{ id: "warehouse.public.orders" }]);
|
|
60
|
+
expect(screen.getByText(/warehouse\.public\.orders/)).toBeInTheDocument();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("counts the remaining sources", () => {
|
|
64
|
+
renderTrigger(["https://a.example.com", "doc-2", { id: "doc-3" }]);
|
|
65
|
+
expect(screen.getByText(/\+2/)).toBeInTheDocument();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("renders 'unknown' for an empty source list", () => {
|
|
69
|
+
renderTrigger([]);
|
|
70
|
+
expect(screen.getByText("unknown")).toBeInTheDocument();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("EvidenceChip inherits the same tolerance (it is an alias)", () => {
|
|
74
|
+
expect(() =>
|
|
75
|
+
render(
|
|
76
|
+
<InlineCitationCard>
|
|
77
|
+
<EvidenceChip sources={["internal-doc-4821"]} />
|
|
78
|
+
</InlineCitationCard>,
|
|
79
|
+
),
|
|
80
|
+
).not.toThrow();
|
|
81
|
+
expect(screen.getByText(/internal-doc-4821/)).toBeInTheDocument();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("inlineCitationSourceLabel", () => {
|
|
86
|
+
it.each<[InlineCitationSourceRef, string]>([
|
|
87
|
+
["https://ci.acme.com/run/42", "ci.acme.com"],
|
|
88
|
+
["doc-4821", "doc-4821"],
|
|
89
|
+
[{ label: "Pricing wiki", url: "https://w.acme.com" }, "Pricing wiki"],
|
|
90
|
+
[{ url: "https://w.acme.com/x" }, "w.acme.com"],
|
|
91
|
+
[{ id: "orders" }, "orders"],
|
|
92
|
+
[{}, "unknown"],
|
|
93
|
+
])("%o → %s", (source, expected) => {
|
|
94
|
+
expect(inlineCitationSourceLabel(source)).toBe(expected);
|
|
95
|
+
});
|
|
96
|
+
});
|