@kitnai/chat 0.7.0 → 0.8.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/README.md +9 -9
- package/dist/custom-elements.json +1626 -883
- package/dist/kitn-chat.es.js +36 -36
- package/dist/llms/llms-full.txt +303 -142
- package/dist/llms/llms.txt +18 -18
- package/dist/schemas/card-envelope.schema.json +14 -0
- package/dist/schemas/card-event.schema.json +12 -0
- package/dist/schemas/confirm.schema.json +65 -0
- package/dist/schemas/embed.schema.json +65 -0
- package/dist/schemas/form.result.schema.json +7 -0
- package/dist/schemas/form.schema.json +33 -0
- package/dist/schemas/link.schema.json +56 -0
- package/dist/schemas/task-list.result.schema.json +16 -0
- package/dist/schemas/task-list.schema.json +78 -0
- package/dist/theme.tokens.css +65 -65
- package/dist/tsx-B8rCNbgL.js +1 -0
- package/dist/typescript-RycA9KXf.js +1 -0
- package/frameworks/react/index.tsx +356 -189
- package/frameworks/react/runtime.tsx +2 -2
- package/llms-full.txt +303 -142
- package/llms.txt +18 -18
- package/package.json +5 -2
- package/src/components/artifact.stories.tsx +138 -0
- package/src/components/artifact.tsx +581 -0
- package/src/components/attachments.stories.tsx +7 -8
- package/src/components/attachments.tsx +2 -2
- package/src/components/card.tsx +110 -0
- package/src/components/chain-of-thought.stories.tsx +7 -8
- package/src/components/chat-container.stories.tsx +7 -8
- package/src/components/chat-container.tsx +4 -0
- package/src/components/checkpoint.stories.tsx +7 -8
- package/src/components/code-block.stories.tsx +8 -9
- package/src/components/component-meta.json +3411 -0
- package/src/components/confirm-card.stories.tsx +74 -0
- package/src/components/confirm-card.tsx +299 -0
- package/src/components/context.stories.tsx +7 -8
- package/src/components/conversation-item.stories.tsx +7 -8
- package/src/components/conversation-item.tsx +2 -2
- package/src/components/conversation-list.stories.tsx +7 -8
- package/src/components/conversation-list.tsx +1 -1
- package/src/components/embed.tsx +196 -0
- package/src/components/empty.stories.tsx +8 -9
- package/src/components/feedback-bar.stories.tsx +7 -8
- package/src/components/file-tree.stories.tsx +73 -0
- package/src/components/file-tree.tsx +383 -0
- package/src/components/file-upload.stories.tsx +7 -8
- package/src/components/form-widgets.tsx +461 -0
- package/src/components/form.tsx +796 -0
- package/src/components/image.stories.tsx +7 -8
- package/src/components/link-card.tsx +194 -0
- package/src/components/loader.stories.tsx +7 -8
- package/src/components/markdown.stories.tsx +7 -8
- package/src/components/message-narrow.stories.tsx +12 -13
- package/src/components/message-skills.stories.tsx +16 -17
- package/src/components/message.stories.tsx +17 -18
- package/src/components/model-switcher.stories.tsx +7 -8
- package/src/components/prompt-input.stories.tsx +8 -9
- package/src/components/prompt-suggestion.stories.tsx +7 -8
- package/src/components/prompt-suggestion.tsx +3 -3
- package/src/components/reasoning.stories.tsx +7 -8
- package/src/components/scroll-button.stories.tsx +7 -8
- package/src/components/slash-command.stories.tsx +8 -9
- package/src/components/slash-command.tsx +2 -2
- package/src/components/source.stories.tsx +7 -8
- package/src/components/source.tsx +1 -1
- package/src/components/task-list-card.stories.tsx +78 -0
- package/src/components/task-list-card.tsx +388 -0
- package/src/components/text-shimmer.stories.tsx +7 -8
- package/src/components/thinking-bar.stories.tsx +7 -8
- package/src/components/tool.stories.tsx +7 -8
- package/src/components/tool.tsx +2 -2
- package/src/components/voice-input.stories.tsx +7 -8
- package/src/elements/artifact.stories.tsx +291 -0
- package/src/elements/artifact.tsx +72 -0
- package/src/elements/{kitn-attachments.stories.tsx → attachments.stories.tsx} +11 -20
- package/src/elements/attachments.tsx +4 -4
- package/src/elements/card.stories.tsx +118 -0
- package/src/elements/card.tsx +40 -0
- package/src/elements/catalog.stories.tsx +491 -0
- package/src/elements/{kitn-chain-of-thought.stories.tsx → chain-of-thought.stories.tsx} +13 -22
- package/src/elements/chain-of-thought.tsx +3 -3
- package/src/elements/{kitn-chat-scope-picker.stories.tsx → chat-scope-picker.stories.tsx} +10 -19
- package/src/elements/chat-scope-picker.tsx +4 -4
- package/src/elements/{kitn-chat-workspace.stories.tsx → chat-workspace.stories.tsx} +15 -23
- package/src/elements/chat-workspace.tsx +2 -2
- package/src/elements/{kitn-chat.stories.tsx → chat.stories.tsx} +12 -20
- package/src/elements/chat.tsx +2 -2
- package/src/elements/{kitn-checkpoint.stories.tsx → checkpoint.stories.tsx} +11 -20
- package/src/elements/checkpoint.tsx +4 -4
- package/src/elements/{kitn-code-block.stories.tsx → code-block.stories.tsx} +10 -19
- package/src/elements/code-block.tsx +3 -3
- package/src/elements/compiled.css +1 -1
- package/src/elements/composed-shell.stories.tsx +316 -0
- package/src/elements/confirm-card.stories.tsx +186 -0
- package/src/elements/confirm-card.tsx +45 -0
- package/src/elements/{kitn-context-meter.stories.tsx → context-meter.stories.tsx} +10 -19
- package/src/elements/context-meter.tsx +3 -3
- package/src/elements/{kitn-conversation-list.stories.tsx → conversation-list.stories.tsx} +12 -20
- package/src/elements/conversation-list.tsx +2 -2
- package/src/elements/css.ts +1 -1
- package/src/elements/define.tsx +10 -10
- package/src/elements/element-meta.json +1379 -733
- package/src/elements/element-types.d.ts +251 -125
- package/src/elements/embed.stories.tsx +197 -0
- package/src/elements/embed.tsx +35 -0
- package/src/elements/{kitn-empty.stories.tsx → empty.stories.tsx} +12 -21
- package/src/elements/empty.tsx +3 -3
- package/src/elements/{kitn-feedback-bar.stories.tsx → feedback-bar.stories.tsx} +11 -20
- package/src/elements/feedback-bar.tsx +4 -4
- package/src/elements/file-tree.stories.tsx +133 -0
- package/src/elements/file-tree.tsx +52 -0
- package/src/elements/{kitn-file-upload.stories.tsx → file-upload.stories.tsx} +12 -21
- package/src/elements/file-upload.tsx +4 -4
- package/src/elements/form.stories.tsx +204 -0
- package/src/elements/form.tsx +37 -0
- package/src/elements/{kitn-image.stories.tsx → image.stories.tsx} +10 -19
- package/src/elements/image.tsx +3 -3
- package/src/elements/link-card.stories.tsx +193 -0
- package/src/elements/link-card.tsx +34 -0
- package/src/elements/{kitn-loader.stories.tsx → loader.stories.tsx} +11 -20
- package/src/elements/loader.tsx +3 -3
- package/src/elements/{kitn-markdown.stories.tsx → markdown.stories.tsx} +10 -19
- package/src/elements/markdown.tsx +3 -3
- package/src/elements/{kitn-message-skills.stories.tsx → message-skills.stories.tsx} +10 -19
- package/src/elements/message-skills.tsx +3 -3
- package/src/elements/{kitn-message.stories.tsx → message.stories.tsx} +12 -21
- package/src/elements/message.tsx +5 -5
- package/src/elements/{kitn-model-switcher.stories.tsx → model-switcher.stories.tsx} +10 -19
- package/src/elements/model-switcher.tsx +5 -5
- package/src/elements/{kitn-prompt-input.stories.tsx → prompt-input.stories.tsx} +14 -22
- package/src/elements/prompt-input.tsx +3 -3
- package/src/elements/{kitn-prompt-suggestions.stories.tsx → prompt-suggestions.stories.tsx} +13 -22
- package/src/elements/prompt-suggestions.tsx +4 -4
- package/src/elements/{kitn-reasoning.stories.tsx → reasoning.stories.tsx} +10 -19
- package/src/elements/reasoning.tsx +4 -4
- package/src/elements/register.ts +11 -1
- package/src/elements/resizable.stories.tsx +200 -0
- package/src/elements/resizable.tsx +264 -0
- package/src/elements/{kitn-response-stream.stories.tsx → response-stream.stories.tsx} +10 -19
- package/src/elements/response-stream.tsx +4 -4
- package/src/elements/{kitn-source-list.stories.tsx → source-list.stories.tsx} +11 -20
- package/src/elements/{kitn-source.stories.tsx → source.stories.tsx} +12 -21
- package/src/elements/source.tsx +5 -5
- package/src/elements/styles.css +140 -1
- package/src/elements/task-list-card.stories.tsx +194 -0
- package/src/elements/task-list-card.tsx +40 -0
- package/src/elements/{kitn-text-shimmer.stories.tsx → text-shimmer.stories.tsx} +10 -19
- package/src/elements/text-shimmer.tsx +3 -3
- package/src/elements/{kitn-thinking-bar.stories.tsx → thinking-bar.stories.tsx} +11 -20
- package/src/elements/thinking-bar.tsx +5 -5
- package/src/elements/{kitn-tool.stories.tsx → tool.stories.tsx} +10 -19
- package/src/elements/tool.tsx +3 -3
- package/src/elements/{kitn-voice-input.stories.tsx → voice-input.stories.tsx} +10 -19
- package/src/elements/voice-input.tsx +4 -4
- package/src/index.ts +94 -2
- package/src/primitives/card-contract.ts +60 -0
- package/src/primitives/card-host.tsx +35 -0
- package/src/primitives/card-routing.ts +79 -0
- package/src/primitives/card-schemas/card-envelope.schema.json +14 -0
- package/src/primitives/card-schemas/card-event.schema.json +12 -0
- package/src/primitives/card-schemas/confirm.schema.json +65 -0
- package/src/primitives/card-schemas/embed.schema.json +65 -0
- package/src/primitives/card-schemas/form.result.schema.json +7 -0
- package/src/primitives/card-schemas/form.schema.json +33 -0
- package/src/primitives/card-schemas/link.schema.json +56 -0
- package/src/primitives/card-schemas/task-list.result.schema.json +16 -0
- package/src/primitives/card-schemas/task-list.schema.json +78 -0
- package/src/primitives/card-validate.ts +95 -0
- package/src/primitives/embed-providers.ts +254 -0
- package/src/primitives/highlighter.ts +4 -0
- package/src/primitives/link-preview.ts +87 -0
- package/src/primitives/pdf-preview.ts +121 -0
- package/src/stories/chat-panel-layout.stories.tsx +2 -1
- package/src/stories/chat-scene.tsx +22 -21
- package/src/stories/checkpoint-restore.stories.tsx +10 -10
- package/src/stories/conversation-with-reasoning.stories.tsx +4 -4
- package/src/stories/conversation-with-sources.stories.tsx +7 -7
- package/src/stories/docs/Accessibility.mdx +2 -2
- package/src/stories/docs/ForAIAgents.mdx +3 -3
- package/src/stories/docs/GettingStarted.mdx +2 -2
- package/src/stories/docs/Installation.mdx +2 -2
- package/src/stories/docs/Integrations.mdx +29 -29
- package/src/stories/docs/Introduction.mdx +3 -3
- package/src/stories/docs/Theming.mdx +2 -2
- package/src/stories/docs/element-controls.ts +32 -0
- package/src/stories/docs/theme-editor/theme-editor.tsx +1 -0
- package/src/stories/examples/ChoosingComponents.mdx +94 -0
- package/src/stories/examples/sample-data.ts +79 -0
- package/src/stories/message-actions.stories.tsx +13 -13
- package/src/stories/pattern-centered-conversation.stories.tsx +3 -3
- package/src/stories/pattern-docked-widget.stories.tsx +1 -1
- package/src/stories/pattern-empty-state.stories.tsx +3 -3
- package/src/stories/prompt-input-variants.stories.tsx +13 -13
- package/src/stories/streaming-response.stories.tsx +3 -3
- package/src/stories/typography.stories.tsx +4 -4
- package/src/ui/avatar.stories.tsx +7 -8
- package/src/ui/badge.stories.tsx +7 -8
- package/src/ui/button.stories.tsx +8 -9
- package/src/ui/button.tsx +1 -0
- package/src/ui/collapsible.stories.tsx +6 -7
- package/src/ui/dropdown.stories.tsx +6 -7
- package/src/ui/hover-card.stories.tsx +6 -7
- package/src/ui/resizable.stories.tsx +74 -9
- package/src/ui/resizable.tsx +351 -71
- package/src/ui/scroll-area.stories.tsx +6 -7
- package/src/ui/scroll-area.tsx +3 -1
- package/src/ui/separator.stories.tsx +7 -8
- package/src/ui/skeleton.stories.tsx +7 -8
- package/src/ui/textarea.stories.tsx +6 -7
- package/src/ui/tooltip.stories.tsx +8 -9
- package/theme.css +65 -65
- package/src/stories/docs/element-spec.tsx +0 -86
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type JSX,
|
|
3
|
+
splitProps,
|
|
4
|
+
mergeProps,
|
|
5
|
+
createSignal,
|
|
6
|
+
createEffect,
|
|
7
|
+
createMemo,
|
|
8
|
+
on,
|
|
9
|
+
onMount,
|
|
10
|
+
onCleanup,
|
|
11
|
+
Show,
|
|
12
|
+
} from 'solid-js';
|
|
13
|
+
import { cn } from '../utils/cn';
|
|
14
|
+
import { Button } from '../ui/button';
|
|
15
|
+
import { CodeBlock, CodeBlockCode } from './code-block';
|
|
16
|
+
import { FileTree, type FileTreeFile } from './file-tree';
|
|
17
|
+
import { Loader } from './loader';
|
|
18
|
+
import { isPdfPreviewEnabled, renderPdfInto } from '../primitives/pdf-preview';
|
|
19
|
+
import {
|
|
20
|
+
ArrowLeft,
|
|
21
|
+
ArrowRight,
|
|
22
|
+
RotateCw,
|
|
23
|
+
House,
|
|
24
|
+
Eye,
|
|
25
|
+
Code as CodeIcon,
|
|
26
|
+
FileText,
|
|
27
|
+
ExternalLink,
|
|
28
|
+
Download,
|
|
29
|
+
} from 'lucide-solid';
|
|
30
|
+
|
|
31
|
+
export type ArtifactTab = 'preview' | 'code';
|
|
32
|
+
|
|
33
|
+
/** A file the artifact can preview + show source for. */
|
|
34
|
+
export type ArtifactFile = FileTreeFile;
|
|
35
|
+
|
|
36
|
+
export interface ArtifactProps extends Omit<JSX.HTMLAttributes<HTMLDivElement>, 'onSelect'> {
|
|
37
|
+
/** URL the preview iframe frames. */
|
|
38
|
+
src?: string;
|
|
39
|
+
/** Files for the Code tab's tree (+ each file's preview `url`). */
|
|
40
|
+
files?: ArtifactFile[];
|
|
41
|
+
/** Active tab. Default `preview`. */
|
|
42
|
+
tab?: ArtifactTab;
|
|
43
|
+
/** Selected file path (syncs tree highlight + Code source + preview). */
|
|
44
|
+
activeFile?: string;
|
|
45
|
+
/** iframe `sandbox` override. Default `allow-scripts allow-forms`. */
|
|
46
|
+
sandbox?: string;
|
|
47
|
+
/** Accessible iframe title. */
|
|
48
|
+
iframeTitle?: string;
|
|
49
|
+
/** Fired when the preview navigates (back/forward/reload/path-edit/file-click). */
|
|
50
|
+
onNavigate?: (url: string) => void;
|
|
51
|
+
/** Fired when the Preview|Code tab changes. */
|
|
52
|
+
onTabChange?: (tab: ArtifactTab) => void;
|
|
53
|
+
/** Fired when a file is selected in the tree. */
|
|
54
|
+
onFileSelect?: (path: string) => void;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const DEFAULT_SANDBOX = 'allow-scripts allow-forms';
|
|
58
|
+
|
|
59
|
+
/** Resolve a file's preview URL: explicit `url`, else `<src-origin> + /path`. */
|
|
60
|
+
function resolveFileUrl(file: ArtifactFile, src: string | undefined): string {
|
|
61
|
+
if (file.url) return file.url;
|
|
62
|
+
if (!src) return file.path;
|
|
63
|
+
try {
|
|
64
|
+
return new URL(file.path, src).href;
|
|
65
|
+
} catch {
|
|
66
|
+
return file.path;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** True when `url` should render as a PDF: a matching `files` entry is typed
|
|
71
|
+
* `'pdf'`, or the URL path (query/hash stripped) ends in `.pdf`. */
|
|
72
|
+
export function isPdfUrl(url: string, files: ArtifactFile[]): boolean {
|
|
73
|
+
if (!url) return false;
|
|
74
|
+
const match = files.find((f) => f.url === url || f.path === url);
|
|
75
|
+
if (match?.type === 'pdf') return true;
|
|
76
|
+
const path = url.split(/[?#]/)[0];
|
|
77
|
+
return /\.pdf$/i.test(path);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* `Artifact` — a framed, switchable generated-artifact viewer. A functional nav
|
|
82
|
+
* toolbar (back · forward · reload · home + editable path field + Preview|Code
|
|
83
|
+
* toggle) over a sandboxed `<iframe>` (Preview) or a file-tree + `<kc-code-block>`
|
|
84
|
+
* (Code). The component self-navigates the iframe and emits `navigate` /
|
|
85
|
+
* `tabchange` / `fileselect` so a consumer can observe/sync.
|
|
86
|
+
*/
|
|
87
|
+
export function Artifact(props: ArtifactProps): JSX.Element {
|
|
88
|
+
const merged = mergeProps(
|
|
89
|
+
{ files: [] as ArtifactFile[], tab: 'preview' as ArtifactTab, sandbox: DEFAULT_SANDBOX },
|
|
90
|
+
props,
|
|
91
|
+
);
|
|
92
|
+
const [local, rest] = splitProps(merged, [
|
|
93
|
+
'src',
|
|
94
|
+
'files',
|
|
95
|
+
'tab',
|
|
96
|
+
'activeFile',
|
|
97
|
+
'sandbox',
|
|
98
|
+
'iframeTitle',
|
|
99
|
+
'onNavigate',
|
|
100
|
+
'onTabChange',
|
|
101
|
+
'onFileSelect',
|
|
102
|
+
'class',
|
|
103
|
+
]);
|
|
104
|
+
|
|
105
|
+
// A sandboxed iframe WITHOUT `allow-same-origin` makes the framed document's
|
|
106
|
+
// `contentWindow.history`/`location` opaque (a deliberate security property —
|
|
107
|
+
// see the spec). So the component keeps its OWN history stack for the
|
|
108
|
+
// navigations it initiates (path-edit, file-click, home, src changes) and
|
|
109
|
+
// back/forward/reload operate on that. (In-frame relative-link clicks navigate
|
|
110
|
+
// the iframe itself but can't be observed cross-origin — a known, accepted
|
|
111
|
+
// sandbox limitation; opt into `allow-same-origin` if the consumer trusts the
|
|
112
|
+
// artifact and wants those tracked.)
|
|
113
|
+
const [history, setHistory] = createSignal<string[]>(local.src ? [local.src] : []);
|
|
114
|
+
const [cursor, setCursor] = createSignal(local.src ? 0 : -1);
|
|
115
|
+
const currentUrl = () => history()[cursor()] ?? '';
|
|
116
|
+
const canBack = () => cursor() > 0;
|
|
117
|
+
const canForward = () => cursor() < history().length - 1;
|
|
118
|
+
|
|
119
|
+
const [tab, setTab] = createSignal<ArtifactTab>(local.tab);
|
|
120
|
+
const [activeFile, setActiveFile] = createSignal<string | undefined>(local.activeFile);
|
|
121
|
+
const [reloadKey, setReloadKey] = createSignal(0);
|
|
122
|
+
|
|
123
|
+
let iframeEl: HTMLIFrameElement | undefined;
|
|
124
|
+
|
|
125
|
+
// Controlled syncing: when the consumer changes the props, follow them.
|
|
126
|
+
createEffect(() => setTab(local.tab));
|
|
127
|
+
createEffect(() => setActiveFile(local.activeFile));
|
|
128
|
+
// `src` change → navigate. Use `on(local.src, …)` so the effect tracks ONLY
|
|
129
|
+
// the `src` prop — NOT `currentUrl()` — otherwise navigating away (file click,
|
|
130
|
+
// path edit) would re-trigger it and snap the iframe back to `src`. Skip the
|
|
131
|
+
// initial run: history is already seeded with `src` and the iframe binds it.
|
|
132
|
+
createEffect(
|
|
133
|
+
on(
|
|
134
|
+
() => local.src,
|
|
135
|
+
(next) => {
|
|
136
|
+
if (next && next !== currentUrl()) navigate(next);
|
|
137
|
+
},
|
|
138
|
+
{ defer: true },
|
|
139
|
+
),
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const fileFor = (path: string | undefined): ArtifactFile | undefined =>
|
|
143
|
+
path === undefined ? undefined : local.files.find((f) => f.path === path);
|
|
144
|
+
|
|
145
|
+
const activeFileObj = createMemo(() => fileFor(activeFile()));
|
|
146
|
+
// Code applies only to text-ish files that carry source.
|
|
147
|
+
const hasSource = createMemo(() => {
|
|
148
|
+
const f = activeFileObj();
|
|
149
|
+
return !!f && f.type !== 'image' && f.type !== 'pdf' && typeof f.code === 'string';
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
/** Push a new entry (truncating any forward history) and load it. */
|
|
153
|
+
function navigate(url: string) {
|
|
154
|
+
if (url === currentUrl()) {
|
|
155
|
+
reload();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
setHistory((h) => [...h.slice(0, cursor() + 1), url]);
|
|
159
|
+
setCursor((c) => c + 1);
|
|
160
|
+
loadCurrent();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Point the iframe at the current cursor entry + emit `navigate`. */
|
|
164
|
+
function loadCurrent() {
|
|
165
|
+
const url = currentUrl();
|
|
166
|
+
if (iframeEl) iframeEl.src = url || 'about:blank';
|
|
167
|
+
local.onNavigate?.(url);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function selectTab(next: ArtifactTab) {
|
|
171
|
+
if (next === tab()) return;
|
|
172
|
+
setTab(next);
|
|
173
|
+
local.onTabChange?.(next);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function selectFile(path: string, file: ArtifactFile) {
|
|
177
|
+
setActiveFile(path);
|
|
178
|
+
local.onFileSelect?.(path);
|
|
179
|
+
navigate(resolveFileUrl(file, local.src));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Toolbar actions — operate on the component's own history stack (see above).
|
|
183
|
+
const goBack = () => {
|
|
184
|
+
if (!canBack()) return;
|
|
185
|
+
setCursor((c) => c - 1);
|
|
186
|
+
loadCurrent();
|
|
187
|
+
};
|
|
188
|
+
const goForward = () => {
|
|
189
|
+
if (!canForward()) return;
|
|
190
|
+
setCursor((c) => c + 1);
|
|
191
|
+
loadCurrent();
|
|
192
|
+
};
|
|
193
|
+
function reload() {
|
|
194
|
+
const url = currentUrl();
|
|
195
|
+
if (iframeEl) {
|
|
196
|
+
// Force a real reload even when the src is unchanged.
|
|
197
|
+
iframeEl.src = 'about:blank';
|
|
198
|
+
iframeEl.src = url || 'about:blank';
|
|
199
|
+
}
|
|
200
|
+
setReloadKey((k) => k + 1); // re-render the inline PDF viewer too
|
|
201
|
+
local.onNavigate?.(url);
|
|
202
|
+
}
|
|
203
|
+
const goHome = () => {
|
|
204
|
+
if (local.src) navigate(local.src);
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// Best-effort: if the consumer opted into `allow-same-origin`, keep the path
|
|
208
|
+
// field truthful as the framed doc navigates itself. Cross-origin (the secure
|
|
209
|
+
// default) throws and we keep our own stack.
|
|
210
|
+
const onIframeLoad = () => {
|
|
211
|
+
try {
|
|
212
|
+
const href = iframeEl?.contentWindow?.location.href;
|
|
213
|
+
if (href && href !== 'about:blank' && href !== currentUrl()) {
|
|
214
|
+
setHistory((h) => [...h.slice(0, cursor() + 1), href]);
|
|
215
|
+
setCursor((c) => c + 1);
|
|
216
|
+
local.onNavigate?.(href);
|
|
217
|
+
}
|
|
218
|
+
} catch {
|
|
219
|
+
/* cross-origin (sandboxed without allow-same-origin): keep our own url */
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const submitPath = (e: Event) => {
|
|
224
|
+
e.preventDefault();
|
|
225
|
+
const input = (e.currentTarget as HTMLFormElement).elements.namedItem(
|
|
226
|
+
'kc-artifact-path',
|
|
227
|
+
) as HTMLInputElement | null;
|
|
228
|
+
if (input && input.value) navigate(input.value);
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
return (
|
|
232
|
+
<div
|
|
233
|
+
class={cn(
|
|
234
|
+
'flex h-full w-full flex-col overflow-hidden rounded-xl border border-border bg-card text-card-foreground',
|
|
235
|
+
local.class,
|
|
236
|
+
)}
|
|
237
|
+
{...rest}
|
|
238
|
+
>
|
|
239
|
+
<ArtifactToolbar
|
|
240
|
+
url={currentUrl}
|
|
241
|
+
tab={tab}
|
|
242
|
+
canBack={canBack}
|
|
243
|
+
canForward={canForward}
|
|
244
|
+
canHome={() => !!local.src}
|
|
245
|
+
onBack={goBack}
|
|
246
|
+
onForward={goForward}
|
|
247
|
+
onReload={reload}
|
|
248
|
+
onHome={goHome}
|
|
249
|
+
onSubmitPath={submitPath}
|
|
250
|
+
onTab={selectTab}
|
|
251
|
+
/>
|
|
252
|
+
<div class="relative min-h-0 flex-1">
|
|
253
|
+
<Show
|
|
254
|
+
when={tab() === 'preview'}
|
|
255
|
+
fallback={
|
|
256
|
+
<ArtifactCode
|
|
257
|
+
files={local.files}
|
|
258
|
+
activeFile={activeFile}
|
|
259
|
+
activeFileObj={activeFileObj}
|
|
260
|
+
hasSource={hasSource}
|
|
261
|
+
onSelect={selectFile}
|
|
262
|
+
/>
|
|
263
|
+
}
|
|
264
|
+
>
|
|
265
|
+
<Show
|
|
266
|
+
when={isPdfUrl(currentUrl(), local.files)}
|
|
267
|
+
fallback={
|
|
268
|
+
<ArtifactPreview
|
|
269
|
+
ref={(el) => (iframeEl = el)}
|
|
270
|
+
src={currentUrl}
|
|
271
|
+
sandbox={local.sandbox}
|
|
272
|
+
title={local.iframeTitle ?? 'Artifact preview'}
|
|
273
|
+
onLoad={onIframeLoad}
|
|
274
|
+
/>
|
|
275
|
+
}
|
|
276
|
+
>
|
|
277
|
+
<ArtifactPdfPreview url={currentUrl()} reloadKey={reloadKey()} />
|
|
278
|
+
</Show>
|
|
279
|
+
</Show>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// --- ArtifactToolbar (internal) -------------------------------------------
|
|
286
|
+
|
|
287
|
+
interface ToolbarProps {
|
|
288
|
+
url: () => string;
|
|
289
|
+
tab: () => ArtifactTab;
|
|
290
|
+
canBack: () => boolean;
|
|
291
|
+
canForward: () => boolean;
|
|
292
|
+
canHome: () => boolean;
|
|
293
|
+
onBack: () => void;
|
|
294
|
+
onForward: () => void;
|
|
295
|
+
onReload: () => void;
|
|
296
|
+
onHome: () => void;
|
|
297
|
+
onSubmitPath: (e: Event) => void;
|
|
298
|
+
onTab: (tab: ArtifactTab) => void;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function ArtifactToolbar(props: ToolbarProps): JSX.Element {
|
|
302
|
+
return (
|
|
303
|
+
<div class="flex shrink-0 items-center gap-1.5 border-b border-border bg-muted/40 px-2 py-1.5">
|
|
304
|
+
<Button
|
|
305
|
+
variant="ghost"
|
|
306
|
+
size="icon-sm"
|
|
307
|
+
aria-label="Back"
|
|
308
|
+
disabled={!props.canBack()}
|
|
309
|
+
onClick={() => props.onBack()}
|
|
310
|
+
>
|
|
311
|
+
<ArrowLeft size={16} aria-hidden="true" />
|
|
312
|
+
</Button>
|
|
313
|
+
<Button
|
|
314
|
+
variant="ghost"
|
|
315
|
+
size="icon-sm"
|
|
316
|
+
aria-label="Forward"
|
|
317
|
+
disabled={!props.canForward()}
|
|
318
|
+
onClick={() => props.onForward()}
|
|
319
|
+
>
|
|
320
|
+
<ArrowRight size={16} aria-hidden="true" />
|
|
321
|
+
</Button>
|
|
322
|
+
<Button variant="ghost" size="icon-sm" aria-label="Reload" onClick={() => props.onReload()}>
|
|
323
|
+
<RotateCw size={15} aria-hidden="true" />
|
|
324
|
+
</Button>
|
|
325
|
+
<Button
|
|
326
|
+
variant="ghost"
|
|
327
|
+
size="icon-sm"
|
|
328
|
+
aria-label="Home"
|
|
329
|
+
disabled={!props.canHome()}
|
|
330
|
+
onClick={() => props.onHome()}
|
|
331
|
+
>
|
|
332
|
+
<House size={15} aria-hidden="true" />
|
|
333
|
+
</Button>
|
|
334
|
+
<form class="min-w-0 flex-1" onSubmit={(e) => props.onSubmitPath(e)}>
|
|
335
|
+
<label class="sr-only" for="kc-artifact-path">
|
|
336
|
+
Address
|
|
337
|
+
</label>
|
|
338
|
+
<input
|
|
339
|
+
id="kc-artifact-path"
|
|
340
|
+
name="kc-artifact-path"
|
|
341
|
+
type="text"
|
|
342
|
+
spellcheck={false}
|
|
343
|
+
autocomplete="off"
|
|
344
|
+
value={props.url()}
|
|
345
|
+
class={cn(
|
|
346
|
+
'h-7 w-full rounded-md border border-border bg-background px-2.5 text-xs text-foreground',
|
|
347
|
+
'font-mono outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
|
348
|
+
)}
|
|
349
|
+
placeholder="Enter a path or URL…"
|
|
350
|
+
/>
|
|
351
|
+
</form>
|
|
352
|
+
<div
|
|
353
|
+
role="tablist"
|
|
354
|
+
aria-label="View"
|
|
355
|
+
class="flex shrink-0 items-center gap-0.5 rounded-md bg-muted p-0.5"
|
|
356
|
+
>
|
|
357
|
+
<SegmentButton
|
|
358
|
+
label="Preview"
|
|
359
|
+
icon={<Eye size={14} aria-hidden="true" />}
|
|
360
|
+
selected={props.tab() === 'preview'}
|
|
361
|
+
onClick={() => props.onTab('preview')}
|
|
362
|
+
/>
|
|
363
|
+
<SegmentButton
|
|
364
|
+
label="Code"
|
|
365
|
+
icon={<CodeIcon size={14} aria-hidden="true" />}
|
|
366
|
+
selected={props.tab() === 'code'}
|
|
367
|
+
onClick={() => props.onTab('code')}
|
|
368
|
+
/>
|
|
369
|
+
</div>
|
|
370
|
+
</div>
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function SegmentButton(props: {
|
|
375
|
+
label: string;
|
|
376
|
+
icon: JSX.Element;
|
|
377
|
+
selected: boolean;
|
|
378
|
+
onClick: () => void;
|
|
379
|
+
}): JSX.Element {
|
|
380
|
+
return (
|
|
381
|
+
<button
|
|
382
|
+
type="button"
|
|
383
|
+
role="tab"
|
|
384
|
+
aria-selected={props.selected}
|
|
385
|
+
class={cn(
|
|
386
|
+
'inline-flex h-6 items-center gap-1.5 rounded px-2 text-xs font-medium transition-colors outline-none',
|
|
387
|
+
'focus-visible:ring-2 focus-visible:ring-ring',
|
|
388
|
+
props.selected
|
|
389
|
+
? 'bg-background text-foreground shadow-sm'
|
|
390
|
+
: 'text-muted-foreground hover:text-foreground',
|
|
391
|
+
)}
|
|
392
|
+
onClick={() => props.onClick()}
|
|
393
|
+
>
|
|
394
|
+
{props.icon}
|
|
395
|
+
{props.label}
|
|
396
|
+
</button>
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// --- ArtifactPreview (internal) -------------------------------------------
|
|
401
|
+
|
|
402
|
+
interface PreviewProps {
|
|
403
|
+
ref: (el: HTMLIFrameElement) => void;
|
|
404
|
+
src: () => string;
|
|
405
|
+
sandbox: string;
|
|
406
|
+
title: string;
|
|
407
|
+
onLoad: () => void;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function ArtifactPreview(props: PreviewProps): JSX.Element {
|
|
411
|
+
return (
|
|
412
|
+
<iframe
|
|
413
|
+
ref={props.ref}
|
|
414
|
+
src={props.src() || 'about:blank'}
|
|
415
|
+
sandbox={props.sandbox}
|
|
416
|
+
title={props.title}
|
|
417
|
+
class="absolute inset-0 h-full w-full border-0 bg-white"
|
|
418
|
+
onLoad={() => props.onLoad()}
|
|
419
|
+
/>
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// --- ArtifactCode (internal) ----------------------------------------------
|
|
424
|
+
|
|
425
|
+
interface CodeProps {
|
|
426
|
+
files: ArtifactFile[];
|
|
427
|
+
activeFile: () => string | undefined;
|
|
428
|
+
activeFileObj: () => ArtifactFile | undefined;
|
|
429
|
+
hasSource: () => boolean;
|
|
430
|
+
onSelect: (path: string, file: ArtifactFile) => void;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function ArtifactCode(props: CodeProps): JSX.Element {
|
|
434
|
+
return (
|
|
435
|
+
<div class="absolute inset-0 flex">
|
|
436
|
+
<div class="w-56 shrink-0 overflow-auto border-r border-border bg-muted/20 py-1.5 scrollbar-thin">
|
|
437
|
+
<FileTree
|
|
438
|
+
files={props.files}
|
|
439
|
+
activeFile={props.activeFile()}
|
|
440
|
+
onSelect={(path, file) => props.onSelect(path, file)}
|
|
441
|
+
/>
|
|
442
|
+
</div>
|
|
443
|
+
<div class="min-w-0 flex-1 overflow-auto scrollbar-thin">
|
|
444
|
+
<Show
|
|
445
|
+
when={props.activeFileObj()}
|
|
446
|
+
fallback={
|
|
447
|
+
<div class="flex h-full items-center justify-center p-6 text-sm text-muted-foreground">
|
|
448
|
+
Select a file to view its source.
|
|
449
|
+
</div>
|
|
450
|
+
}
|
|
451
|
+
>
|
|
452
|
+
<Show
|
|
453
|
+
when={props.hasSource()}
|
|
454
|
+
fallback={
|
|
455
|
+
<div class="flex h-full items-center justify-center p-6 text-sm text-muted-foreground">
|
|
456
|
+
No source — this file ({props.activeFileObj()!.type ?? 'binary'}) has no code view.
|
|
457
|
+
</div>
|
|
458
|
+
}
|
|
459
|
+
>
|
|
460
|
+
<CodeBlock class="h-full rounded-none border-0">
|
|
461
|
+
<CodeBlockCode
|
|
462
|
+
code={props.activeFileObj()!.code ?? ''}
|
|
463
|
+
language={props.activeFileObj()!.language}
|
|
464
|
+
/>
|
|
465
|
+
</CodeBlock>
|
|
466
|
+
</Show>
|
|
467
|
+
</Show>
|
|
468
|
+
</div>
|
|
469
|
+
</div>
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// --- ArtifactPdfFallback (internal) ---------------------------------------
|
|
474
|
+
|
|
475
|
+
/** Shown when inline PDF rendering is disabled or fails (CORS / load / parse). */
|
|
476
|
+
function ArtifactPdfFallback(props: { url: string }): JSX.Element {
|
|
477
|
+
const name = () => {
|
|
478
|
+
const path = props.url.split(/[?#]/)[0];
|
|
479
|
+
return path.slice(path.lastIndexOf('/') + 1) || 'document.pdf';
|
|
480
|
+
};
|
|
481
|
+
const linkClass =
|
|
482
|
+
'inline-flex h-8 items-center gap-1.5 rounded-md border border-border bg-background px-3 text-xs font-medium text-foreground transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring';
|
|
483
|
+
return (
|
|
484
|
+
<div
|
|
485
|
+
role="region"
|
|
486
|
+
aria-label="PDF preview unavailable"
|
|
487
|
+
class="absolute inset-0 flex flex-col items-center justify-center gap-3 bg-card p-6 text-center"
|
|
488
|
+
>
|
|
489
|
+
<FileText size={40} class="text-muted-foreground" aria-hidden="true" />
|
|
490
|
+
<div class="text-sm font-medium text-foreground">{name()}</div>
|
|
491
|
+
<div class="text-xs text-muted-foreground">Can't preview this PDF inline.</div>
|
|
492
|
+
<div class="flex flex-wrap items-center justify-center gap-2">
|
|
493
|
+
<a class={linkClass} href={props.url} target="_blank" rel="noopener noreferrer">
|
|
494
|
+
Open in new tab
|
|
495
|
+
<ExternalLink size={13} aria-hidden="true" />
|
|
496
|
+
</a>
|
|
497
|
+
<a class={linkClass} href={props.url} download="">
|
|
498
|
+
<Download size={13} aria-hidden="true" />
|
|
499
|
+
Download
|
|
500
|
+
</a>
|
|
501
|
+
</div>
|
|
502
|
+
</div>
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// --- ArtifactPdfPreview (internal) ----------------------------------------
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Renders a PDF inline via pdf.js (loaded on demand). Four states:
|
|
510
|
+
* disabled → fallback (no network); loading → spinner; success → stacked
|
|
511
|
+
* fit-width canvases; error (load/CORS/parse) → fallback card. Re-renders when
|
|
512
|
+
* the url or `reloadKey` changes, and (debounced) when the panel resizes.
|
|
513
|
+
*/
|
|
514
|
+
function ArtifactPdfPreview(props: { url: string; reloadKey: number }): JSX.Element {
|
|
515
|
+
const [state, setState] = createSignal<'loading' | 'success' | 'error'>('loading');
|
|
516
|
+
let container: HTMLDivElement | undefined;
|
|
517
|
+
let token = 0;
|
|
518
|
+
let resizeTimer: ReturnType<typeof setTimeout> | undefined;
|
|
519
|
+
|
|
520
|
+
const renderNow = async () => {
|
|
521
|
+
if (!isPdfPreviewEnabled() || !container) {
|
|
522
|
+
setState('error');
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
const mine = ++token;
|
|
526
|
+
setState('loading');
|
|
527
|
+
try {
|
|
528
|
+
const width = container.clientWidth || 600;
|
|
529
|
+
await renderPdfInto(props.url, container, width);
|
|
530
|
+
if (mine === token) setState('success');
|
|
531
|
+
} catch {
|
|
532
|
+
if (mine === token) setState('error');
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
createEffect(
|
|
537
|
+
on(
|
|
538
|
+
() => [props.url, props.reloadKey] as const,
|
|
539
|
+
() => {
|
|
540
|
+
void renderNow();
|
|
541
|
+
},
|
|
542
|
+
),
|
|
543
|
+
);
|
|
544
|
+
|
|
545
|
+
onMount(() => {
|
|
546
|
+
if (!container || typeof ResizeObserver === 'undefined') return;
|
|
547
|
+
const ro = new ResizeObserver(() => {
|
|
548
|
+
if (state() !== 'success') return;
|
|
549
|
+
clearTimeout(resizeTimer);
|
|
550
|
+
resizeTimer = setTimeout(() => void renderNow(), 200);
|
|
551
|
+
});
|
|
552
|
+
ro.observe(container);
|
|
553
|
+
onCleanup(() => {
|
|
554
|
+
ro.disconnect();
|
|
555
|
+
clearTimeout(resizeTimer);
|
|
556
|
+
token++; // cancel any in-flight render
|
|
557
|
+
});
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
return (
|
|
561
|
+
<div class="absolute inset-0">
|
|
562
|
+
{/* Always present so clientWidth is the real panel width. */}
|
|
563
|
+
<div
|
|
564
|
+
ref={(el) => (container = el)}
|
|
565
|
+
role="region"
|
|
566
|
+
aria-label="PDF preview"
|
|
567
|
+
aria-busy={state() === 'loading'}
|
|
568
|
+
tabindex="0"
|
|
569
|
+
class="absolute inset-0 flex flex-col items-center gap-3 overflow-auto bg-muted/20 p-3 scrollbar-thin focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset"
|
|
570
|
+
/>
|
|
571
|
+
<Show when={isPdfPreviewEnabled() && state() === 'loading'}>
|
|
572
|
+
<div class="pointer-events-none absolute inset-0 flex items-center justify-center">
|
|
573
|
+
<Loader variant="circular" size="md" />
|
|
574
|
+
</div>
|
|
575
|
+
</Show>
|
|
576
|
+
<Show when={!isPdfPreviewEnabled() || state() === 'error'}>
|
|
577
|
+
<ArtifactPdfFallback url={props.url} />
|
|
578
|
+
</Show>
|
|
579
|
+
</div>
|
|
580
|
+
);
|
|
581
|
+
}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
AttachmentEmpty,
|
|
13
13
|
} from './attachments';
|
|
14
14
|
import type { AttachmentData } from './attachments';
|
|
15
|
+
import { componentDescription } from '../stories/docs/element-controls';
|
|
15
16
|
|
|
16
17
|
const sampleAttachments: AttachmentData[] = [
|
|
17
18
|
{
|
|
@@ -62,14 +63,12 @@ const meta = {
|
|
|
62
63
|
parameters: {
|
|
63
64
|
layout: 'padded',
|
|
64
65
|
docs: {
|
|
65
|
-
description:
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
].join('\n\n'),
|
|
72
|
-
},
|
|
66
|
+
description: componentDescription([
|
|
67
|
+
'A composable container for displaying file and source-document attachments as thumbnails, inline chips, or a vertical list. Built from `Attachments` + per-item `Attachment` with `AttachmentPreview`, `AttachmentInfo`, and `AttachmentRemove` parts.',
|
|
68
|
+
'**When to use:** to show files a user attached to a prompt, or documents/sources referenced by a message. Choose `grid` for thumbnails, `inline` for compact chips, `list` for a detailed rows view.',
|
|
69
|
+
'**How to use:** set `variant` on `Attachments`, then map your data to `Attachment` (passing each item via `data` and an `onRemove` handler) and compose the preview/info/remove parts inside.',
|
|
70
|
+
'**Placement:** prompt input area (pending uploads), message bodies (attached or cited files), and document panels.',
|
|
71
|
+
]),
|
|
73
72
|
controls: { exclude: ['use:eventListener'] },
|
|
74
73
|
},
|
|
75
74
|
},
|
|
@@ -157,12 +157,12 @@ function Attachment(props: AttachmentProps) {
|
|
|
157
157
|
variant === 'grid' && 'size-24 overflow-hidden rounded-lg',
|
|
158
158
|
variant === 'inline' && [
|
|
159
159
|
'flex h-8 cursor-pointer select-none items-center gap-1.5',
|
|
160
|
-
'rounded-md bg-muted/50 px-1.5',
|
|
160
|
+
'rounded-md bg-muted/50 px-1.5 text-foreground',
|
|
161
161
|
'font-medium text-sm transition-all',
|
|
162
162
|
'hover:bg-muted',
|
|
163
163
|
],
|
|
164
164
|
variant === 'list' && [
|
|
165
|
-
'flex w-full items-center gap-3 rounded-lg bg-muted/30 p-3',
|
|
165
|
+
'flex w-full items-center gap-3 rounded-lg bg-muted/30 p-3 text-foreground',
|
|
166
166
|
'hover:bg-muted/50',
|
|
167
167
|
],
|
|
168
168
|
local.class,
|