@kitn.ai/ui 0.24.0 → 0.25.1
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 +11 -7
- package/dist/{create-tween-vA7upv0h.js → create-tween-4z1XYLkR.js} +1 -1
- package/dist/{create-tween-CTT5l1bo.js → create-tween-B5Y4Im8G.js} +1 -1
- package/dist/{create-tween-BmBnlg25.js → create-tween-CUzOlaMS.js} +1 -1
- package/dist/elements/artifact.js +1 -1
- package/dist/elements/cards.js +1 -1
- package/dist/elements/chat-workspace.js +1 -1
- package/dist/elements/chat.js +1 -1
- package/dist/elements/choice.js +1 -1
- package/dist/elements/chunks/artifact-BSuPYFHY.js +1 -0
- package/dist/elements/chunks/{card-renderer-aNA0WRFH.js → card-renderer-CSoTL_e1.js} +1 -1
- package/dist/elements/chunks/card-routing-BK09BdmL.js +1 -0
- package/dist/elements/chunks/{chat-thread-DN9evldr.js → chat-thread-DxoKaStk.js} +1 -1
- package/dist/elements/chunks/{choice-card-kNsjU2m8.js → choice-card-CGMc3kek.js} +1 -1
- package/dist/elements/chunks/{confirm-card-CEvFmVhz.js → confirm-card-BWjf2-xo.js} +1 -1
- package/dist/elements/chunks/{form-lUuJjz6s.js → form-Cp8Y00Wn.js} +1 -1
- package/dist/elements/chunks/link-preview-Cf5KOP-i.js +1 -0
- package/dist/elements/chunks/link-preview-DNILK391.js +1 -0
- package/dist/elements/chunks/{markdown-BpAHnBJz.js → markdown-DbozyYqa.js} +16 -16
- package/dist/elements/chunks/{message-CoqLuLkd.js → message-CE4k-qaj.js} +1 -1
- package/dist/elements/chunks/{message-_oYx5mk1.js → message-DtXKPqiR.js} +1 -1
- package/dist/elements/chunks/{reasoning-D26Bb4p6.js → reasoning-BrxQfpfI.js} +1 -1
- package/dist/elements/chunks/source-w9V0NnBn.js +1 -0
- package/dist/elements/chunks/{tasks-card-CfqrkoVO.js → tasks-card-lPxoodlA.js} +1 -1
- package/dist/elements/compare.js +1 -1
- package/dist/elements/confirm-card.js +1 -1
- package/dist/elements/embed.js +1 -1
- package/dist/elements/form.js +1 -1
- package/dist/elements/link-preview.js +1 -1
- package/dist/elements/markdown.js +1 -1
- package/dist/elements/message.js +1 -1
- package/dist/elements/reasoning.js +1 -1
- package/dist/elements/remote.js +1 -1
- package/dist/elements/source.js +1 -1
- package/dist/elements/tasks.js +1 -1
- package/dist/elements/thread.js +1 -1
- package/dist/index.js +4011 -3936
- package/dist/index.server.js +3302 -3228
- package/dist/kai.es.js +1 -1
- package/dist/primitives/card-routing.d.ts +33 -0
- package/dist/{register-impl-C96AnOJn.js → register-impl-DGmGzrEM.js} +15 -15
- package/dist/{solid-C76UXJuy.js → solid-BMuDmFo_.js} +4427 -4352
- package/dist/{solid-BuqWLn6x.js → solid-CGYNdLRR.js} +3738 -3664
- package/dist/solid.js +1 -1
- package/dist/solid.server.js +1 -1
- package/dist/{variant-aurora-CD35fdJF.js → variant-aurora-DwSOfraQ.js} +2 -2
- package/dist/{variant-aurora-Dau5Z4yR.js → variant-aurora-XEkPT0_z.js} +1 -1
- package/dist/{variant-aurora-Dc-2RVH9.js → variant-aurora-wKvAQ5wp.js} +2 -2
- package/dist/{variant-custom-CHr_jbMU.js → variant-custom-BhS9wIpU.js} +2 -2
- package/dist/{variant-custom-Df7QvXyZ.js → variant-custom-MkTNTycd.js} +1 -1
- package/dist/{variant-custom-Cyu8zx4A.js → variant-custom-eFIg2XWQ.js} +2 -2
- package/dist/{variant-wave-CXcaP_9W.js → variant-wave-BwzrHD1s.js} +1 -1
- package/dist/{variant-wave-BkVpxkVF.js → variant-wave-CUWGx9Bn.js} +2 -2
- package/dist/{variant-wave-DcGqjVnK.js → variant-wave-CbVsmgwT.js} +2 -2
- package/dist/wire.js +112 -105
- package/package.json +3 -1
- package/src/components/artifact.tsx +110 -24
- package/src/components/markdown.tsx +83 -5
- package/src/components/source.tsx +35 -3
- package/src/primitives/card-routing.ts +54 -2
- package/src/wire/formats/openai.ts +38 -2
- package/dist/elements/chunks/artifact-BhJIuh3D.js +0 -1
- package/dist/elements/chunks/card-routing-C986KuUA.js +0 -1
- package/dist/elements/chunks/link-preview-C9-jNJBf.js +0 -1
- package/dist/elements/chunks/source-eJurW_-L.js +0 -1
|
@@ -16,6 +16,7 @@ import { CodeBlock, CodeBlockCode } from './code-block';
|
|
|
16
16
|
import { FileTree, type FileTreeFile } from './file-tree';
|
|
17
17
|
import { Loader } from './loader';
|
|
18
18
|
import { isPdfPreviewEnabled, renderPdfInto } from '../primitives/pdf-preview';
|
|
19
|
+
import { isSafeUrl, isScriptUrl } from '../primitives/card-routing';
|
|
19
20
|
import {
|
|
20
21
|
ArrowLeft,
|
|
21
22
|
ArrowRight,
|
|
@@ -123,6 +124,18 @@ export interface ArtifactProps extends Omit<JSX.HTMLAttributes<HTMLDivElement>,
|
|
|
123
124
|
|
|
124
125
|
const DEFAULT_SANDBOX = 'allow-scripts allow-forms';
|
|
125
126
|
|
|
127
|
+
/** Refuse loudly. `src` and `files[].url` are MODEL-REACHABLE -- they arrive on
|
|
128
|
+
* an `artifact` card envelope produced by a tool call, and the card schema
|
|
129
|
+
* validates them only as `format: "uri"`, which constrains no scheme -- so
|
|
130
|
+
* every url this component puts in an href, hands to `window.open`, or frames
|
|
131
|
+
* is filtered. A refusal without a trace would be a decision made while hiding
|
|
132
|
+
* that it happened; the component has no error channel of its own, so it warns
|
|
133
|
+
* the way `card-routing` does. */
|
|
134
|
+
function warnBlockedUrl(sink: string, url: string): void {
|
|
135
|
+
// eslint-disable-next-line no-console
|
|
136
|
+
console.warn(`[kai-artifact] blocked unsafe url in ${sink}: ${url}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
126
139
|
/** Resolve a file's preview URL: explicit `url`, else `<src-origin> + /path`. */
|
|
127
140
|
function resolveFileUrl(file: ArtifactFile, src: string | undefined): string {
|
|
128
141
|
if (file.url) return file.url;
|
|
@@ -205,14 +218,47 @@ export function Artifact(props: ArtifactProps): JSX.Element {
|
|
|
205
218
|
// navigations it initiates (path-edit, file-click, home, src changes) and
|
|
206
219
|
// back/forward/reload operate on that. (In-frame relative-link clicks navigate
|
|
207
220
|
// the iframe itself but can't be observed cross-origin — a known, accepted
|
|
208
|
-
// sandbox limitation
|
|
209
|
-
//
|
|
221
|
+
// sandbox limitation.)
|
|
222
|
+
//
|
|
223
|
+
// DO NOT add `allow-same-origin` to get those clicks tracked. This comment
|
|
224
|
+
// used to suggest exactly that "if the consumer trusts the artifact", and it
|
|
225
|
+
// was the worst advice in the file: `allow-same-origin` gives the framed
|
|
226
|
+
// document the HOST's origin, so a hostile `src` runs with full access to the
|
|
227
|
+
// embedding page, zero clicks -- confirmed executing in Chromium. And "the
|
|
228
|
+
// artifact is trusted" is not a premise you get to hold when the url can come
|
|
229
|
+
// from tool output: on the card path `src` and `files[].url` are
|
|
230
|
+
// model-supplied. The scheme filter below is a backstop, not a licence --
|
|
231
|
+
// `allow-same-origin` still hands the framed document the host's cookies,
|
|
232
|
+
// storage and DOM for everything else it does. Losing in-frame click tracking
|
|
233
|
+
// is the correct trade.
|
|
210
234
|
const [history, setHistory] = createSignal<string[]>(local.src ? [local.src] : []);
|
|
211
235
|
const [cursor, setCursor] = createSignal(local.src ? 0 : -1);
|
|
212
236
|
const currentUrl = () => history()[cursor()] ?? '';
|
|
213
237
|
const canBack = () => cursor() > 0;
|
|
214
238
|
const canForward = () => cursor() < history().length - 1;
|
|
215
239
|
|
|
240
|
+
/** What the iframe is actually pointed at.
|
|
241
|
+
*
|
|
242
|
+
* Separate from `currentUrl()` on purpose: the history stack and the address
|
|
243
|
+
* field keep reporting what ARRIVED (a reader looking at a refused artifact
|
|
244
|
+
* should see the address that was refused, not a blank), while this is the
|
|
245
|
+
* only value that reaches the `src` sink.
|
|
246
|
+
*
|
|
247
|
+
* `isScriptUrl` and not `isSafeUrl`: a `data:`/`blob:` src is a legitimate
|
|
248
|
+
* artifact -- see the note on `displayUrl` -- and gets an opaque origin, so
|
|
249
|
+
* it cannot touch the host page. `javascript:`/`vbscript:` run in the
|
|
250
|
+
* EMBEDDER's origin, which the default sandbox already refuses but a
|
|
251
|
+
* consumer-set `allow-same-origin` re-enables (see the note on the history
|
|
252
|
+
* stack above for why nothing here recommends that setting). */
|
|
253
|
+
const framedUrl = createMemo(() => {
|
|
254
|
+
const u = currentUrl();
|
|
255
|
+
if (u && isScriptUrl(u)) {
|
|
256
|
+
warnBlockedUrl('preview', u);
|
|
257
|
+
return '';
|
|
258
|
+
}
|
|
259
|
+
return u;
|
|
260
|
+
});
|
|
261
|
+
|
|
216
262
|
// Seed the internal tab: a present `tab` (controlled) wins; otherwise the
|
|
217
263
|
// uncontrolled `defaultTab` (which merges to 'preview'). The user can then
|
|
218
264
|
// switch freely unless `tab` is set (the effect below re-asserts the controlled
|
|
@@ -230,14 +276,23 @@ export function Artifact(props: ArtifactProps): JSX.Element {
|
|
|
230
276
|
local.onMaximizeChange?.(next);
|
|
231
277
|
};
|
|
232
278
|
|
|
233
|
-
// Open-in-tab — only enabled when there's a concrete url to open.
|
|
279
|
+
// Open-in-tab — only enabled when there's a concrete SAFE url to open. The
|
|
280
|
+
// scheme filter is part of `canOpenInTab` rather than only of the handler so
|
|
281
|
+
// the button renders DISABLED for a url that would be refused: a control that
|
|
282
|
+
// looks live and does nothing is the worse failure.
|
|
234
283
|
const canOpenInTab = createMemo(() => {
|
|
235
284
|
const u = currentUrl();
|
|
236
|
-
return !!u && u !== 'about:blank';
|
|
285
|
+
return !!u && u !== 'about:blank' && isSafeUrl(u);
|
|
237
286
|
});
|
|
238
287
|
const openInNewTab = () => {
|
|
239
|
-
|
|
240
|
-
|
|
288
|
+
const u = currentUrl();
|
|
289
|
+
if (!canOpenInTab()) {
|
|
290
|
+
// `openExternal()` on the controller reaches this without any button, so
|
|
291
|
+
// the refusal has to be stated here and not only in the disabled state.
|
|
292
|
+
if (u && u !== 'about:blank') warnBlockedUrl('openExternal', u);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
window.open(u, '_blank', 'noopener,noreferrer');
|
|
241
296
|
};
|
|
242
297
|
|
|
243
298
|
// The expand button is suppressed in standalone (no enclosing resizable).
|
|
@@ -300,9 +355,11 @@ export function Artifact(props: ArtifactProps): JSX.Element {
|
|
|
300
355
|
|
|
301
356
|
/** Point the iframe at the current cursor entry + emit `navigate`. */
|
|
302
357
|
function loadCurrent() {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
358
|
+
if (iframeEl) iframeEl.src = framedUrl() || 'about:blank';
|
|
359
|
+
// Reports the REAL url, not the framed one: a consumer observing navigation
|
|
360
|
+
// (or a card patching `src` back into its envelope) must not be told the
|
|
361
|
+
// model sent something it did not.
|
|
362
|
+
local.onNavigate?.(currentUrl());
|
|
306
363
|
}
|
|
307
364
|
|
|
308
365
|
function selectTab(next: ArtifactTab) {
|
|
@@ -329,14 +386,13 @@ export function Artifact(props: ArtifactProps): JSX.Element {
|
|
|
329
386
|
loadCurrent();
|
|
330
387
|
};
|
|
331
388
|
function reload() {
|
|
332
|
-
const url = currentUrl();
|
|
333
389
|
if (iframeEl) {
|
|
334
390
|
// Force a real reload even when the src is unchanged.
|
|
335
391
|
iframeEl.src = 'about:blank';
|
|
336
|
-
iframeEl.src =
|
|
392
|
+
iframeEl.src = framedUrl() || 'about:blank';
|
|
337
393
|
}
|
|
338
394
|
setReloadKey((k) => k + 1); // re-render the inline PDF viewer too
|
|
339
|
-
local.onNavigate?.(
|
|
395
|
+
local.onNavigate?.(currentUrl());
|
|
340
396
|
}
|
|
341
397
|
const goHome = () => {
|
|
342
398
|
if (local.src) navigate(local.src);
|
|
@@ -458,7 +514,7 @@ export function Artifact(props: ArtifactProps): JSX.Element {
|
|
|
458
514
|
fallback={
|
|
459
515
|
<ArtifactPreview
|
|
460
516
|
ref={(el) => (iframeEl = el)}
|
|
461
|
-
src={
|
|
517
|
+
src={framedUrl}
|
|
462
518
|
sandbox={local.sandbox}
|
|
463
519
|
title={local.iframeTitle ?? 'Artifact preview'}
|
|
464
520
|
onLoad={onIframeLoad}
|
|
@@ -719,12 +775,30 @@ function ArtifactCode(props: CodeProps): JSX.Element {
|
|
|
719
775
|
|
|
720
776
|
// --- ArtifactPdfFallback (internal) ---------------------------------------
|
|
721
777
|
|
|
722
|
-
/** Shown when inline PDF rendering is disabled or fails (CORS / load / parse).
|
|
778
|
+
/** Shown when inline PDF rendering is disabled or fails (CORS / load / parse).
|
|
779
|
+
*
|
|
780
|
+
* THIS IS THE DEFAULT VIEW FOR A HOSTILE URL, not an edge case: pdf.js has to
|
|
781
|
+
* FETCH the document to render it inline, and it cannot fetch a `javascript:`
|
|
782
|
+
* or `data:` one, so every such url fails straight through to here -- where two
|
|
783
|
+
* anchors used to put it in an `href`, one click from executing in the host
|
|
784
|
+
* page's origin. The url is model-reachable (`artifact` card `src` /
|
|
785
|
+
* `files[].url`, schema-validated only as `format: "uri"`), so it is filtered
|
|
786
|
+
* with the same `isSafeUrl` the markdown renderer and the card `open` verb use.
|
|
787
|
+
* Relative paths pass, which matters here: `resolveFileUrl` returns a bare
|
|
788
|
+
* `path` when the artifact has no `src`. */
|
|
723
789
|
function ArtifactPdfFallback(props: { url: string }): JSX.Element {
|
|
724
790
|
const name = () => {
|
|
725
791
|
const path = props.url.split(/[?#]/)[0];
|
|
726
792
|
return path.slice(path.lastIndexOf('/') + 1) || 'document.pdf';
|
|
727
793
|
};
|
|
794
|
+
/** The url actually put in `href`, or `undefined` -- which OMITS the attribute.
|
|
795
|
+
* Never `''`: an empty href navigates to the current page. */
|
|
796
|
+
const href = createMemo(() => {
|
|
797
|
+
if (!props.url) return undefined;
|
|
798
|
+
if (isSafeUrl(props.url)) return props.url;
|
|
799
|
+
warnBlockedUrl('pdf fallback', props.url);
|
|
800
|
+
return undefined;
|
|
801
|
+
});
|
|
728
802
|
const linkClass =
|
|
729
803
|
'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';
|
|
730
804
|
return (
|
|
@@ -736,16 +810,28 @@ function ArtifactPdfFallback(props: { url: string }): JSX.Element {
|
|
|
736
810
|
<FileText size={40} class="text-muted-foreground" aria-hidden="true" />
|
|
737
811
|
<div class="text-sm font-medium text-foreground">{name()}</div>
|
|
738
812
|
<div class="text-xs text-muted-foreground">Can't preview this PDF inline.</div>
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
<
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
813
|
+
{/* No safe address, no controls. Two buttons that silently do nothing
|
|
814
|
+
would be worse than saying what happened -- and a bare `<a>` with the
|
|
815
|
+
href dropped is exactly that. */}
|
|
816
|
+
<Show
|
|
817
|
+
when={href()}
|
|
818
|
+
fallback={
|
|
819
|
+
<div class="text-xs text-muted-foreground">
|
|
820
|
+
Blocked: that address uses a scheme this viewer won't open.
|
|
821
|
+
</div>
|
|
822
|
+
}
|
|
823
|
+
>
|
|
824
|
+
<div class="flex flex-wrap items-center justify-center gap-2">
|
|
825
|
+
<a class={linkClass} href={href()} target="_blank" rel="noopener noreferrer">
|
|
826
|
+
Open in new tab
|
|
827
|
+
<ExternalLink size={13} aria-hidden="true" />
|
|
828
|
+
</a>
|
|
829
|
+
<a class={linkClass} href={href()} download="">
|
|
830
|
+
<Download size={13} aria-hidden="true" />
|
|
831
|
+
Download
|
|
832
|
+
</a>
|
|
833
|
+
</div>
|
|
834
|
+
</Show>
|
|
749
835
|
</div>
|
|
750
836
|
);
|
|
751
837
|
}
|
|
@@ -1,10 +1,86 @@
|
|
|
1
1
|
import { splitProps, createMemo, createUniqueId, For, Show, Switch, Match } from 'solid-js';
|
|
2
2
|
import { cn } from '../utils/cn';
|
|
3
|
-
import {
|
|
3
|
+
import { Marked } from 'marked';
|
|
4
4
|
import { CodeBlock, CodeBlockCode } from './code-block';
|
|
5
5
|
import { useChatConfig, textClass } from '../primitives/chat-config';
|
|
6
|
+
import { isSafeUrl } from '../primitives/card-routing';
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
// --- The markdown sink -------------------------------------------------------
|
|
9
|
+
//
|
|
10
|
+
// `MarkdownBlock` writes this parser's output to `innerHTML`, so THIS is the one
|
|
11
|
+
// place in the kit where a string the model produced becomes live DOM in the
|
|
12
|
+
// host page's origin. `marked` passes raw HTML through verbatim by default and
|
|
13
|
+
// dropped its own URL sanitizing in v5, so both have to be handled here.
|
|
14
|
+
//
|
|
15
|
+
// The threat model is NOT a hostile provider. An attacker only needs to
|
|
16
|
+
// influence the model's OUTPUT -- a user asking to be shown an `<img onerror>`
|
|
17
|
+
// example, a prompt-injected model, or RAG over an untrusted document all reach
|
|
18
|
+
// this sink against a perfectly trusted provider.
|
|
19
|
+
//
|
|
20
|
+
// WHY ESCAPE RATHER THAN SANITIZE (no DOMPurify):
|
|
21
|
+
// - A sanitizer needs a DOM. In Node this package's ESM build has no
|
|
22
|
+
// `sanitize` at all (`isSupported === false`), so the SSR path either throws
|
|
23
|
+
// or gets "guarded" into returning the payload UNCHANGED -- a silent
|
|
24
|
+
// passthrough of exactly the string we were trying to neutralise. Escaping
|
|
25
|
+
// is pure string work and behaves identically on server and client.
|
|
26
|
+
// - It costs no dependency. Escaping is the ~20 lines below; DOMPurify is
|
|
27
|
+
// ~35 KB gzipped of ESM in a package that gates its own footprint.
|
|
28
|
+
// - For a CHAT UI it is also the better RENDERING, not just the safer one.
|
|
29
|
+
// Raw HTML in model output is either the model quoting markup -- which the
|
|
30
|
+
// reader should SEE as text -- or an injection. Escaping serves the first
|
|
31
|
+
// case correctly and defuses the second. Nothing in this repo emits markdown
|
|
32
|
+
// that relies on inline HTML.
|
|
33
|
+
// There is deliberately NO opt-in to restore raw HTML: nothing needs it today,
|
|
34
|
+
// and an unsafe switch is the hole again for whoever flips it.
|
|
35
|
+
//
|
|
36
|
+
// This is a PRIVATE `Marked` instance, and that is load-bearing rather than
|
|
37
|
+
// tidiness. Configuring the shared `marked` singleton would mean a consumer's
|
|
38
|
+
// own `marked.use({ renderer })` elsewhere in the same app silently replaces
|
|
39
|
+
// this renderer and reopens the vulnerability. It also stops the kit mutating
|
|
40
|
+
// global `marked` options out from under a consumer, which it used to do.
|
|
41
|
+
|
|
42
|
+
/** Escape for HTML TEXT content. */
|
|
43
|
+
function escapeText(s: string): string {
|
|
44
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Escape for a double-quoted ATTRIBUTE value. */
|
|
48
|
+
function escapeAttr(s: string): string {
|
|
49
|
+
return escapeText(s).replace(/"/g, '"');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const md = new Marked({
|
|
53
|
+
gfm: true,
|
|
54
|
+
breaks: true,
|
|
55
|
+
renderer: {
|
|
56
|
+
/** Raw HTML -- block (`Tokens.HTML`) and inline (`Tokens.Tag`) both land
|
|
57
|
+
* here. Rendered as visible text instead of markup. */
|
|
58
|
+
html({ text }) {
|
|
59
|
+
return escapeText(text);
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
/** Markdown link syntax. marked BUILDS this anchor itself, so escaping raw
|
|
63
|
+
* HTML does not cover it and it needs the scheme allowlist of its own.
|
|
64
|
+
* A blocked link keeps its LABEL -- the reader still sees what the model
|
|
65
|
+
* wrote, we simply refuse to make it clickable. */
|
|
66
|
+
link({ href, title, tokens }) {
|
|
67
|
+
const text = this.parser.parseInline(tokens);
|
|
68
|
+
if (!isSafeUrl(href)) return text;
|
|
69
|
+
const t = title ? ` title="${escapeAttr(title)}"` : '';
|
|
70
|
+
return `<a href="${escapeAttr(href)}"${t}>${text}</a>`;
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
/** Markdown image syntax. Same reasoning as `link`; a blocked image falls
|
|
74
|
+
* back to its alt text. NOTE this also blocks `data:` image URIs, which
|
|
75
|
+
* are inert inside `<img>` -- one allowlist for both is worth more than
|
|
76
|
+
* the rare inline-data-image case it costs. */
|
|
77
|
+
image({ href, title, text }) {
|
|
78
|
+
if (!isSafeUrl(href)) return escapeText(text);
|
|
79
|
+
const t = title ? ` title="${escapeAttr(title)}"` : '';
|
|
80
|
+
return `<img src="${escapeAttr(href)}" alt="${escapeAttr(text)}"${t}>`;
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
});
|
|
8
84
|
|
|
9
85
|
export interface MarkdownProps {
|
|
10
86
|
content: string;
|
|
@@ -23,7 +99,7 @@ interface ParsedBlock {
|
|
|
23
99
|
}
|
|
24
100
|
|
|
25
101
|
function parseMarkdownIntoBlocks(markdown: string): ParsedBlock[] {
|
|
26
|
-
const tokens =
|
|
102
|
+
const tokens = md.lexer(markdown);
|
|
27
103
|
return tokens.map((token) => {
|
|
28
104
|
if (token.type === 'code') {
|
|
29
105
|
return {
|
|
@@ -42,9 +118,11 @@ function parseMarkdownIntoBlocks(markdown: string): ParsedBlock[] {
|
|
|
42
118
|
function MarkdownBlock(props: { content: string }) {
|
|
43
119
|
const html = createMemo(() => {
|
|
44
120
|
try {
|
|
45
|
-
return
|
|
121
|
+
return md.parse(props.content, { async: false }) as string;
|
|
46
122
|
} catch {
|
|
47
|
-
|
|
123
|
+
// The parse failed, so nothing has been escaped -- the raw source must
|
|
124
|
+
// NOT go to `innerHTML`. Escape it and show it as text.
|
|
125
|
+
return escapeText(props.content);
|
|
48
126
|
}
|
|
49
127
|
});
|
|
50
128
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type JSX, createContext, useContext, Show, splitProps } from 'solid-js';
|
|
2
2
|
import { cn } from '../utils/cn';
|
|
3
3
|
import { HoverCardRoot, HoverCardTrigger, HoverCardContent } from '../ui/hover-card';
|
|
4
|
+
import { isRenderableLink } from '../primitives/link-preview';
|
|
4
5
|
|
|
5
6
|
// --- Context ---
|
|
6
7
|
|
|
@@ -30,12 +31,43 @@ export interface SourceProps {
|
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
function Source(props: SourceProps) {
|
|
33
|
-
const
|
|
34
|
+
const raw = () => props.href ?? '';
|
|
35
|
+
|
|
36
|
+
/** The url actually put in `href`.
|
|
37
|
+
*
|
|
38
|
+
* A citation url is MODEL-SUPPLIED: `wire/formats/openai.ts` (`sourcesOf`)
|
|
39
|
+
* and `wire/formats/anthropic.ts` (`citations_delta`) both take it as an
|
|
40
|
+
* arbitrary string. `javascript:`/`data:` in an href is script execution one
|
|
41
|
+
* click away, and today the only thing stopping it is `target="_blank"` plus
|
|
42
|
+
* current Chrome behaviour -- an accident of the browser, not a guard. Anyone
|
|
43
|
+
* dropping `target="_blank"` for styling would silently make it live.
|
|
44
|
+
*
|
|
45
|
+
* Filtered HERE, at render, rather than in `wire/`, for two reasons.
|
|
46
|
+
* (1) The wire layer is OPTIONAL: a consumer with their own backend adapter
|
|
47
|
+
* can set `messages` containing `source` parts directly and never touch
|
|
48
|
+
* `wire/`, so a wire-side filter would leave that path exploitable while
|
|
49
|
+
* looking fixed. Every path renders through here.
|
|
50
|
+
* (2) `parts` should keep reporting what the model actually said, so a
|
|
51
|
+
* consumer logging or auditing citations is not lied to. The kit refuses to
|
|
52
|
+
* LINK the url; it does not pretend the url was never there.
|
|
53
|
+
*
|
|
54
|
+
* `isRenderableLink` (absolute http(s) only) is the right guard rather than
|
|
55
|
+
* `isSafeUrl` from card-routing: a citation is a reference to a page on the
|
|
56
|
+
* web, so unlike a markdown body link it has no business being relative. */
|
|
57
|
+
const href = () => (isRenderableLink(raw()) ? raw() : '');
|
|
58
|
+
|
|
59
|
+
/** Display only, and deliberately derived from the RAW value: a blocked url
|
|
60
|
+
* still has to label its chip, and dropping to an empty string here would
|
|
61
|
+
* render a blank chip instead of showing the reader what arrived. The value
|
|
62
|
+
* lands in a text node, which Solid escapes. */
|
|
34
63
|
const domain = () => {
|
|
35
|
-
const h =
|
|
64
|
+
const h = raw();
|
|
36
65
|
if (!h) return '';
|
|
37
66
|
try {
|
|
38
|
-
|
|
67
|
+
// `new URL('javascript:x')` parses fine and yields an EMPTY hostname, so
|
|
68
|
+
// a blocked citation would otherwise label an empty chip. Fall back to the
|
|
69
|
+
// raw string: the reader sees what arrived, just not as a link.
|
|
70
|
+
return new URL(h).hostname || h;
|
|
39
71
|
} catch {
|
|
40
72
|
return h.split('/').pop() || h;
|
|
41
73
|
}
|
|
@@ -17,8 +17,60 @@ export function emitCardEvent(element: HTMLElement, event: CardEvent): void {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
const SAFE_SCHEMES = ['http:', 'https:', 'mailto:'];
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
const SCRIPT_SCHEMES = ['javascript:', 'vbscript:'];
|
|
21
|
+
|
|
22
|
+
/** The scheme the WHATWG parser reads out of `url`, resolved against a base so a
|
|
23
|
+
* relative input inherits `http:`. `undefined` when it will not parse at all.
|
|
24
|
+
*
|
|
25
|
+
* ONE parser for the two questions below, deliberately: the parsing is where
|
|
26
|
+
* the subtlety lives (it strips embedded tabs/newlines and trims surrounding
|
|
27
|
+
* whitespace before reading the scheme, so `java\nscript:` and
|
|
28
|
+
* ` javascript:... ` are both read as `javascript:` -- which a regex over the
|
|
29
|
+
* raw string would miss). Two predicates over one parse cannot drift on that;
|
|
30
|
+
* two hand-rolled parsers would. */
|
|
31
|
+
function schemeOf(url: string): string | undefined {
|
|
32
|
+
try { return new URL(url, 'http://_invalid_base').protocol; } catch { return undefined; }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** True when `url` is safe to put in an href / hand to `window.open`.
|
|
36
|
+
*
|
|
37
|
+
* Exported so the MARKDOWN renderer and the ARTIFACT viewer reuse this exact
|
|
38
|
+
* guard rather than growing a second scheme list that can drift from this one.
|
|
39
|
+
*
|
|
40
|
+
* Resolving against a base is deliberate and is what makes it correct for
|
|
41
|
+
* markdown: a relative or fragment link (`/docs`, `#section`, `./rel`) is
|
|
42
|
+
* ordinary markdown, and resolving it inherits the base's `http:` so it
|
|
43
|
+
* passes, while `javascript:`/`data:`/`vbscript:` keep their own protocol and
|
|
44
|
+
* fail. It is correct for the artifact viewer for the same reason: a file with
|
|
45
|
+
* no `src` to resolve against yields a bare relative path (`docs/report.pdf`),
|
|
46
|
+
* which is a legitimate artifact address. Contrast `isRenderableLink` in
|
|
47
|
+
* `primitives/link-preview.ts`, which takes NO base and so demands an absolute
|
|
48
|
+
* http(s) URL: that is the right guard for a model-supplied citation -- a
|
|
49
|
+
* reference to a page on the public web -- this one for markdown body links and
|
|
50
|
+
* artifact file addresses. */
|
|
51
|
+
export function isSafeUrl(url: string): boolean {
|
|
52
|
+
const scheme = schemeOf(url);
|
|
53
|
+
return scheme !== undefined && SAFE_SCHEMES.includes(scheme);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** True when navigating to `url` would EXECUTE it as script in the initiating
|
|
57
|
+
* document's origin.
|
|
58
|
+
*
|
|
59
|
+
* Strictly narrower than `!isSafeUrl(url)`, and not a competing policy: it
|
|
60
|
+
* answers a different question, for the one sink where the allowlist is the
|
|
61
|
+
* wrong tool. An `<iframe src>` legitimately takes `data:` and `blob:` (a
|
|
62
|
+
* `data:` blob artifact is a documented `<kai-artifact>` use -- it is what
|
|
63
|
+
* `displayUrl` exists for) and both get an OPAQUE origin in every modern
|
|
64
|
+
* browser, so neither can reach the host page. `javascript:`/`vbscript:` are
|
|
65
|
+
* the schemes that run in the EMBEDDER's origin, and they are the whole risk:
|
|
66
|
+
* the default sandbox (no `allow-same-origin`) already makes the browser refuse
|
|
67
|
+
* them, but a consumer who sets `allow-same-origin` turns a model-supplied
|
|
68
|
+
* `src` into host-origin script execution. The artifact viewer used to suggest
|
|
69
|
+
* that setting for "an artifact you trust"; it no longer does, and this guard
|
|
70
|
+
* is what protects the consumers who took the old advice. */
|
|
71
|
+
export function isScriptUrl(url: string): boolean {
|
|
72
|
+
const scheme = schemeOf(url);
|
|
73
|
+
return scheme !== undefined && SCRIPT_SCHEMES.includes(scheme);
|
|
22
74
|
}
|
|
23
75
|
function warnNoHandler(kind: string): void {
|
|
24
76
|
// eslint-disable-next-line no-console
|
|
@@ -84,6 +84,19 @@ function detailField(details: unknown[], key: 'index' | 'signature'): unknown {
|
|
|
84
84
|
return undefined;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
/** The sibling reasoning STRINGS, in precedence order. See `applyReasoning`. */
|
|
88
|
+
const REASONING_KEYS = ['reasoning', 'reasoning_content'] as const;
|
|
89
|
+
|
|
90
|
+
/** First non-empty sibling string wins. These are alternative SPELLINGS of the
|
|
91
|
+
* same text, never two halves of it, so this coalesces and never concatenates. */
|
|
92
|
+
function siblingReasoning(delta: Record<string, unknown>): string | undefined {
|
|
93
|
+
for (const key of REASONING_KEYS) {
|
|
94
|
+
const raw = str(delta[key]);
|
|
95
|
+
if (raw !== undefined && raw !== '') return raw;
|
|
96
|
+
}
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
87
100
|
/**
|
|
88
101
|
* FINDINGS: OpenRouter frequently puts the SAME text in `reasoning` AND in
|
|
89
102
|
* `reasoning_details` on the same delta. Concatenating both doubles every
|
|
@@ -92,11 +105,34 @@ function detailField(details: unknown[], key: 'index' | 'signature'): unknown {
|
|
|
92
105
|
* `reasoning_details` is still read in BOTH cases, for `reasoningRaw`, the block
|
|
93
106
|
* index and the signature. It is the provider's own block list, and dropping it
|
|
94
107
|
* is exactly the Anthropic 400 this entry exists to avoid.
|
|
108
|
+
*
|
|
109
|
+
* SIBLING NAMES. Chat-completions says nothing about reasoning, so every vendor
|
|
110
|
+
* extended it on its own and each parser in the wild reads a different subset:
|
|
111
|
+
* `reasoning` is what OpenRouter normalises to, `reasoning_content` is what
|
|
112
|
+
* DeepSeek's own API emits and what LiteLLM forwards. Nobody is ahead here and
|
|
113
|
+
* there is no canonical spelling. A stream that only ever went through
|
|
114
|
+
* OpenRouter never shows the other name, which is the whole reason this one
|
|
115
|
+
* stayed invisible: reading `reasoning` alone loses a DeepSeek-direct stream's
|
|
116
|
+
* reasoning completely, and silently.
|
|
117
|
+
*
|
|
118
|
+
* PRECEDENCE. The siblings coalesce, first non-empty wins, and both outrank the
|
|
119
|
+
* details text. `reasoning` is checked first, which is what keeps this additive:
|
|
120
|
+
* any stream that already carried `reasoning` parses byte for byte as it did
|
|
121
|
+
* before, so the order is only observable on a provider that disagrees with
|
|
122
|
+
* itself. A gateway that aliases one spelling onto the other sends identical
|
|
123
|
+
* text in both, and summing them would double every token exactly the way
|
|
124
|
+
* summing `reasoning` and `reasoning_details` does.
|
|
125
|
+
*
|
|
126
|
+
* SCOPE, and it is NOT a closed class. This covers the NAMING axis only.
|
|
127
|
+
* Reasoning on this wire is fragmented three ways STRUCTURALLY: a sibling string
|
|
128
|
+
* (here), a block array (`reasoning_details`), and reasoning carried inside a
|
|
129
|
+
* polymorphic `content` array. Aliasing bridges names, not shapes. A fourth
|
|
130
|
+
* spelling is one entry in REASONING_KEYS; a fourth SHAPE is not, and needs its
|
|
131
|
+
* own branch plus its own fixture.
|
|
95
132
|
*/
|
|
96
133
|
function applyReasoning(delta: Record<string, unknown>, out: ModelStreamChunk): void {
|
|
97
134
|
const details = Array.isArray(delta.reasoning_details) ? delta.reasoning_details : undefined;
|
|
98
|
-
const
|
|
99
|
-
const primary = primaryRaw !== undefined && primaryRaw !== '' ? primaryRaw : undefined;
|
|
135
|
+
const primary = siblingReasoning(delta);
|
|
100
136
|
const fallback = details ? detailText(details) : undefined;
|
|
101
137
|
const hasDetails = details !== undefined && details.length > 0;
|
|
102
138
|
const text = primary ?? fallback;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{c as a,m as P,s as ce,g as k,k as C,l as F,x as G,o as Z,q as ue,i as h,S as g,t as v,d as p,a as y,b as J,v as Q,j as he,h as fe,e as ge}from"./define-B_tXMYRm.js";import{c as N}from"./cn-CU7UAWzk.js";import{B as z}from"./button-B9okqG73.js";import{C as be,a as me}from"./code-block-ClhQO9RW.js";import{F as ve}from"./file-tree-yGWNK0Js.js";import{L as pe}from"./loader-B5QzK_8_.js";import{a as we}from"./arrow-left-iJVnQmxA.js";import{I as X}from"./Icon-b5A8hlHs.js";import{r as xe,h as ke,c as ye}from"./rotate-cw-DzVDJOA1.js";import{d as ze}from"./download-DnCyWFJE.js";import{e as Y}from"./external-link-DG2sRdQL.js";import{f as Te}from"./file-text-D0J9F6M7.js";import{m as Ce,a as Fe}from"./minimize-2-SegJ_oku.js";var Pe=[["path",{d:"M5 12h14",key:"1ays0h"}],["path",{d:"m12 5 7 7-7 7",key:"xquz4c"}]],Se=r=>a(X,P(r,{name:"ArrowRight",iconNode:Pe})),Oe=Se,Re=[["path",{d:"M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z",key:"rwhkz3"}],["circle",{cx:"12",cy:"12",r:"3",key:"1v7zrd"}]],je=r=>a(X,P(r,{name:"Eye",iconNode:Re})),Ie=je;const Ne="6.0.227",ee=`https://cdn.jsdelivr.net/npm/pdfjs-dist@${Ne}/build`,He=()=>import(/*@vite-ignore*/`${ee}/pdf.min.mjs`),Me=`${ee}/pdf.worker.min.mjs`;let Ee=He,Le=Me,H=null;function Ue(){if(!H){const r=Ee;H=(async()=>{const t=await r(),e=t.default??t;return e.GlobalWorkerOptions.workerSrc=Le,e})()}return H}async function $e(r,t,e){const o=await(await Ue()).getDocument({url:r}).promise;t.replaceChildren();const l=typeof window<"u"&&window.devicePixelRatio?window.devicePixelRatio:1;for(let s=1;s<=o.numPages;s++){const d=await o.getPage(s),c=d.getViewport({scale:1}),u=c.width>0?e/c.width:1,f=d.getViewport({scale:u*l}),b=document.createElement("canvas");b.width=Math.max(1,Math.floor(f.width)),b.height=Math.max(1,Math.floor(f.height)),b.setAttribute("role","img"),b.setAttribute("aria-label",`Page ${s}`),b.style.width="100%",b.style.height="auto",b.style.display="block";const w=b.getContext("2d");t.appendChild(b),await d.render({canvasContext:w,viewport:f}).promise}return{pages:o.numPages}}var Be=v('<div><div class="relative min-h-0 flex-1">'),De=v('<form class="min-w-0 flex-1"><label class=sr-only for=kai-artifact-path>Address</label><input id=kai-artifact-path name=kai-artifact-path type=text autocomplete=off placeholder="Enter a path or URL\u2026">'),Ae=v("<div class=flex-1 aria-hidden=true>"),Ve=v('<div role=tablist aria-label=View class="flex shrink-0 items-center gap-0.5 rounded-md bg-muted p-0.5">'),We=v('<div data-artifact-toolbar class="flex shrink-0 items-center gap-1.5 border-b border-border bg-surface px-2 py-1.5">'),qe=v("<button type=button role=tab>"),Ke=v('<iframe class="absolute inset-0 h-full w-full border-0 bg-white">'),_e=v('<div class="absolute inset-0 flex"><div class="w-56 shrink-0 overflow-auto border-r border-border bg-surface-sunken py-1.5 scrollbar-thin"></div><div class="min-w-0 flex-1 overflow-auto scrollbar-thin">'),Ge=v('<div class="flex h-full items-center justify-center p-6 text-sm text-muted-foreground">Select a file to view its source.'),Ze=v('<div class="flex h-full items-center justify-center p-6 text-sm text-muted-foreground">No source \u2014 this file (<!>) has no code view.'),Je=v(`<div role=region aria-label="PDF preview unavailable"class="absolute inset-0 flex flex-col items-center justify-center gap-3 bg-card p-6 text-center"><div class="text-sm font-medium text-foreground"></div><div class="text-xs text-muted-foreground">Can't preview this PDF inline.</div><div class="flex flex-wrap items-center justify-center gap-2"><a class="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"target=_blank rel="noopener noreferrer">Open in new tab</a><a class="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"download>Download`),Qe=v('<div class="pointer-events-none absolute inset-0 flex items-center justify-center">'),Xe=v('<div class="absolute inset-0"><div role=region aria-label="PDF preview"tabindex=0 class="absolute inset-0 flex flex-col items-center gap-3 overflow-auto bg-surface-sunken p-3 scrollbar-thin focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset">');const Ye="allow-scripts allow-forms";function er(r,t){if(r.url)return r.url;if(!t)return r.path;try{return new URL(r.path,t).href}catch{return r.path}}function rr(r,t){if(!r)return!1;const e=t.find(l=>l.url===r||l.path===r);if(e?.type==="pdf")return!0;const o=r.split(/[?#]/)[0];return/\.pdf$/i.test(o)}function nr(r){const t=P({files:[],defaultTab:"preview",sandbox:Ye,showNav:!0,showReload:!0,showHome:!0,showPathField:!0,showTabs:!0,expandable:!1,openInTab:!1,standalone:!1,readonlyPath:!1,maximized:!1},r),[e,o]=ce(t,["src","files","tab","defaultTab","activeFile","sandbox","iframeTitle","onNavigate","onTabChange","onFileSelect","maximized","onMaximizeChange","showNav","showReload","showHome","showPathField","showTabs","expandable","openInTab","standalone","readonlyPath","displayUrl","controllerRef","class"]),[l,s]=k(e.src?[e.src]:[]),[d,c]=k(e.src?0:-1),u=()=>l()[d()]??"",f=()=>d()>0,b=()=>d()<l().length-1,[w,M]=k(e.tab??e.defaultTab),[E,L]=k(e.activeFile),[ne,te]=k(0),[S,O]=k(e.maximized??!1);C(()=>O(e.maximized??!1));const ae=()=>{var n;const i=!S();O(i),(n=e.onMaximizeChange)==null||n.call(e,i)},U=F(()=>{const n=u();return!!n&&n!=="about:blank"}),$=()=>{U()&&window.open(u(),"_blank","noopener,noreferrer")},B=F(()=>e.expandable&&!e.standalone),ie=F(()=>e.showNav||e.showReload||e.showHome||e.showPathField||e.showTabs||B()||e.openInTab);let x;C(()=>{e.tab!==void 0&&M(e.tab)}),C(()=>L(e.activeFile)),C(G(()=>e.src,n=>{n&&n!==u()&&T(n)},{defer:!0}));const D=n=>n===void 0?void 0:e.files.find(i=>i.path===n),A=F(()=>D(E())),oe=F(()=>{const n=A();return!!n&&n.type!=="image"&&n.type!=="pdf"&&typeof n.code=="string"});function T(n){if(n===u()){j();return}s(i=>[...i.slice(0,d()+1),n]),c(i=>i+1),R()}function R(){var n;const i=u();x&&(x.src=i||"about:blank"),(n=e.onNavigate)==null||n.call(e,i)}function le(n){var i;n!==w()&&(M(n),(i=e.onTabChange)==null||i.call(e,n))}function V(n,i){var m;L(n),(m=e.onFileSelect)==null||m.call(e,n),T(er(i,e.src))}const W=()=>{f()&&(c(n=>n-1),R())},q=()=>{b()&&(c(n=>n+1),R())};function j(){var n;const i=u();x&&(x.src="about:blank",x.src=i||"about:blank"),te(m=>m+1),(n=e.onNavigate)==null||n.call(e,i)}const K=()=>{e.src&&T(e.src)},_=n=>{var i;S()!==n&&(O(n),(i=e.onMaximizeChange)==null||i.call(e,n))};Z(()=>{var n;(n=e.controllerRef)==null||n.call(e,{back:()=>W(),forward:()=>q(),reload:()=>j(),home:()=>K(),navigate:i=>T(i),selectFile:i=>{const m=D(i);m&&V(i,m)},openExternal:()=>$(),maximize:()=>_(!0),restore:()=>_(!1)})});const se=()=>{var n,i;try{const m=(n=x?.contentWindow)==null?void 0:n.location.href;m&&m!=="about:blank"&&m!==u()&&(s(I=>[...I.slice(0,d()+1),m]),c(I=>I+1),(i=e.onNavigate)==null||i.call(e,m))}catch{}},de=n=>{n.preventDefault();const i=n.currentTarget.elements.namedItem("kai-artifact-path");if(e.readonlyPath||e.displayUrl!=null){i&&(i.value=e.displayUrl??u());return}i&&i.value&&T(i.value)};return(()=>{var n=Be(),i=n.firstChild;return ue(n,P({get class(){return N("flex h-full w-full flex-col overflow-hidden bg-card text-card-foreground",e.standalone&&"rounded-xl border border-border",e.class)}},o),!1,!0),h(n,a(g,{get when(){return ie()},get children(){return a(tr,{url:()=>e.displayUrl??u(),tab:w,canBack:f,canForward:b,canHome:()=>!!e.src,onBack:W,onForward:q,onReload:j,onHome:K,onSubmitPath:de,onTab:le,showNav:()=>e.showNav,showReload:()=>e.showReload,showHome:()=>e.showHome,showPathField:()=>e.showPathField,showTabs:()=>e.showTabs,showExpand:B,showOpenInTab:()=>e.openInTab,maximized:S,onToggleMaximize:ae,canOpenInTab:U,onOpenInTab:$,readonlyPath:()=>e.readonlyPath||e.displayUrl!=null})}}),i),h(i,a(g,{get when(){return w()==="preview"},get fallback(){return a(ir,{get files(){return e.files},activeFile:E,activeFileObj:A,hasSource:oe,onSelect:V})},get children(){return a(g,{get when(){return rr(u(),e.files)},get fallback(){return a(ar,{ref:m=>x=m,src:u,get sandbox(){return e.sandbox},get title(){return e.iframeTitle??"Artifact preview"},onLoad:se})},get children(){return a(lr,{get url(){return u()},get reloadKey(){return ne()}})}})}})),n})()}function tr(r){return(()=>{var t=We();return h(t,a(g,{get when(){return r.showNav()},get children(){return[a(z,{variant:"ghost",size:"icon-sm","aria-label":"Back",get disabled(){return!r.canBack()},onClick:()=>r.onBack(),get children(){return a(we,{size:16,"aria-hidden":"true"})}}),a(z,{variant:"ghost",size:"icon-sm","aria-label":"Forward",get disabled(){return!r.canForward()},onClick:()=>r.onForward(),get children(){return a(Oe,{size:16,"aria-hidden":"true"})}})]}}),null),h(t,a(g,{get when(){return r.showReload()},get children(){return a(z,{variant:"ghost",size:"icon-sm","aria-label":"Reload",onClick:()=>r.onReload(),get children(){return a(xe,{size:15,"aria-hidden":"true"})}})}}),null),h(t,a(g,{get when(){return r.showHome()},get children(){return a(z,{variant:"ghost",size:"icon-sm","aria-label":"Home",get disabled(){return!r.canHome()},onClick:()=>r.onHome(),get children(){return a(ke,{size:15,"aria-hidden":"true"})}})}}),null),h(t,a(g,{get when(){return r.showPathField()},get children(){var e=De(),o=e.firstChild,l=o.nextSibling;return e.addEventListener("submit",s=>r.onSubmitPath(s)),p(l,"spellcheck",!1),y(s=>{var d=r.readonlyPath()||void 0,c=r.readonlyPath()?"true":void 0,u=N("h-7 w-full rounded-md border border-border px-2.5 text-xs text-foreground font-mono outline-none",r.readonlyPath()?"bg-muted/40 cursor-default":"bg-background focus-visible:ring-2 focus-visible:ring-ring");return d!==s.e&&(l.readOnly=s.e=d),c!==s.t&&p(l,"aria-readonly",s.t=c),u!==s.a&&J(l,s.a=u),s},{e:void 0,t:void 0,a:void 0}),y(()=>l.value=r.url()),e}}),null),h(t,a(g,{get when(){return!r.showPathField()},get children(){return Ae()}}),null),h(t,a(g,{get when(){return r.showExpand()},get children(){return a(z,{variant:"ghost",size:"icon-sm",get"aria-label"(){return r.maximized()?"Collapse":"Expand"},get"aria-expanded"(){return r.maximized()},onClick:()=>r.onToggleMaximize(),get children(){return a(g,{get when(){return r.maximized()},get fallback(){return a(Fe,{size:15,"aria-hidden":"true"})},get children(){return a(Ce,{size:15,"aria-hidden":"true"})}})}})}}),null),h(t,a(g,{get when(){return r.showOpenInTab()},get children(){return a(z,{variant:"ghost",size:"icon-sm","aria-label":"Open in new tab",get disabled(){return!r.canOpenInTab()},onClick:()=>r.onOpenInTab(),get children(){return a(Y,{size:15,"aria-hidden":"true"})}})}}),null),h(t,a(g,{get when(){return r.showTabs()},get children(){var e=Ve();return h(e,a(re,{label:"Preview",get icon(){return a(Ie,{size:14,"aria-hidden":"true"})},get selected(){return r.tab()==="preview"},onClick:()=>r.onTab("preview")}),null),h(e,a(re,{label:"Code",get icon(){return a(ye,{size:14,"aria-hidden":"true"})},get selected(){return r.tab()==="code"},onClick:()=>r.onTab("code")}),null),e}}),null),t})()}function re(r){return(()=>{var t=qe();return t.$$click=()=>r.onClick(),h(t,()=>r.icon,null),h(t,()=>r.label,null),y(e=>{var o=r.selected,l=N("inline-flex h-6 items-center gap-1.5 rounded px-2 text-xs font-medium transition-colors outline-none","focus-visible:ring-2 focus-visible:ring-ring",r.selected?"bg-background text-foreground shadow-sm":"text-muted-foreground hover:text-foreground");return o!==e.e&&p(t,"aria-selected",e.e=o),l!==e.t&&J(t,e.t=l),e},{e:void 0,t:void 0}),t})()}function ar(r){return(()=>{var t=Ke();t.addEventListener("load",()=>r.onLoad());var e=r.ref;return typeof e=="function"?Q(e,t):r.ref=t,y(o=>{var l=r.src()||"about:blank",s=r.sandbox,d=r.title;return l!==o.e&&p(t,"src",o.e=l),s!==o.t&&p(t,"sandbox",o.t=s),d!==o.a&&p(t,"title",o.a=d),o},{e:void 0,t:void 0,a:void 0}),t})()}function ir(r){return(()=>{var t=_e(),e=t.firstChild,o=e.nextSibling;return h(e,a(ve,{get files(){return r.files},get activeFile(){return r.activeFile()},onSelect:(l,s)=>r.onSelect(l,s)})),h(o,a(g,{get when(){return r.activeFileObj()},get fallback(){return Ge()},get children(){return a(g,{get when(){return r.hasSource()},get fallback(){return(()=>{var l=Ze(),s=l.firstChild,d=s.nextSibling;return d.nextSibling,h(l,()=>r.activeFileObj().type??"binary",d),l})()},get children(){return a(be,{class:"h-full rounded-none border-0",get children(){return a(me,{get code(){return r.activeFileObj().code??""},get language(){return r.activeFileObj().language}})}})}})}})),t})()}function or(r){const t=()=>{const e=r.url.split(/[?#]/)[0];return e.slice(e.lastIndexOf("/")+1)||"document.pdf"};return(()=>{var e=Je(),o=e.firstChild,l=o.nextSibling,s=l.nextSibling,d=s.firstChild;d.firstChild;var c=d.nextSibling,u=c.firstChild;return h(e,a(Te,{size:40,class:"text-muted-foreground","aria-hidden":"true"}),o),h(o,t),h(d,a(Y,{size:13,"aria-hidden":"true"}),null),h(c,a(ze,{size:13,"aria-hidden":"true"}),u),y(f=>{var b=r.url,w=r.url;return b!==f.e&&p(d,"href",f.e=b),w!==f.t&&p(c,"href",f.t=w),f},{e:void 0,t:void 0}),e})()}function lr(r){const[t,e]=k("loading");let o,l=0,s;const d=async()=>{if(!o){e("error");return}const c=++l;e("loading");try{const u=o.clientWidth||600;await $e(r.url,o,u),c===l&&e("success")}catch{c===l&&e("error")}};return C(G(()=>[r.url,r.reloadKey],()=>{d()})),Z(()=>{if(!o||typeof ResizeObserver>"u")return;const c=new ResizeObserver(()=>{t()==="success"&&(clearTimeout(s),s=setTimeout(()=>{d()},200))});c.observe(o),fe(()=>{c.disconnect(),clearTimeout(s),l++})}),(()=>{var c=Xe(),u=c.firstChild;return Q(f=>o=f,u),h(c,a(g,{get when(){return he(()=>!0)()&&t()==="loading"},get children(){var f=Qe();return h(f,a(pe,{variant:"circular",size:"md"})),f}}),null),h(c,a(g,{get when(){return t()==="error"},get children(){return a(or,{get url(){return r.url}})}}),null),y(()=>p(u,"aria-busy",t()==="loading")),c})()}ge(["click"]);export{nr as A};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
const o="kai-card";function c(t,e){t.dispatchEvent(new CustomEvent(o,{detail:e,bubbles:!0,composed:!0}))}const d=["http:","https:","mailto:"];function i(t){try{return d.includes(new URL(t,"http://_invalid_base").protocol)}catch{return!1}}function r(t){console.warn(`[kai-card] no policy handler for "${t}"`)}function p(t,e){const n=t??{};switch(e.kind){case"ready":break;case"submit":n.onSubmit?n.onSubmit(e.cardId,e.data):r("submit");break;case"action":n.onAction?n.onAction(e.cardId,e.action,e.payload):r("action");break;case"send-prompt":{const a=e.mode??"compose",s=n.maxSendPromptMode==="send"?a:"compose";n.onSendPrompt?n.onSendPrompt(e.text,{mode:s,context:e.context}):r("send-prompt");break}case"open":{if(!i(e.url)){n.onError?n.onError(e.cardId,`Blocked unsafe url: ${e.url}`):r("open(unsafe)");break}const a=e.target??"tab";n.onOpen?n.onOpen(e.url,a):typeof window<"u"&&window.open(e.url,"_blank","noopener,noreferrer");break}case"state":n.onState?n.onState(e.cardId,e.patch):r("state");break;case"dismiss":n.onDismiss?n.onDismiss(e.cardId):r("dismiss");break;case"reopen":n.onReopen?n.onReopen(e.cardId):r("reopen");break;case"error":n.onError?n.onError(e.cardId,e.message):r("error");break}}export{o as C,c as e,p as r};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{s as q,g as x,l as v,o as B,k as L,x as C,c as n,i as a,S as g,a as m,d as f,b as S,t as c,e as F}from"./define-B_tXMYRm.js";import{c as z}from"./cn-CU7UAWzk.js";import{S as w}from"./skeleton-N8BuWYmf.js";import{l as D}from"./link-BdsfLB7b.js";function G(d){try{return new URL(d).hostname.replace(/^www\./,"")}catch{return}}const H=["http:","https:"];function J(d){try{return H.includes(new URL(d).protocol)}catch{return!1}}var K=c('<div class="space-y-2 p-3">'),M=c('<img role=img class="aspect-[16/9] w-full object-cover">'),P=c('<img alt class="h-3.5 w-3.5 shrink-0 rounded-sm">'),Q=c('<div class="line-clamp-3 text-xs text-muted-foreground">'),T=c('<div class="space-y-1 p-3"><div class="flex items-center gap-1.5 text-xs text-muted-foreground"><span class=truncate></span></div><div class="line-clamp-2 text-sm font-semibold text-foreground">'),V=c('<a rel="noopener noreferrer">'),W=c('<div role=img aria-label="Invalid link"><span class=truncate>Invalid link');function X(d){return!!(d.title||d.description||d.image)}function Y(d){const[i]=q(d,["cardId","data","onEmit","class"]),b=r=>{var e;return(e=i.onEmit)==null?void 0:e.call(i,r)},[R,_]=x(),[y,Z]=x(!1),[U,I]=x(!1),u=()=>i.data.url,h=v(()=>J(u())),s=v(()=>({...i.data,...R()}));B(()=>b({kind:"ready",cardId:i.cardId})),L(C(h,r=>{r||b({kind:"error",cardId:i.cardId,message:`Invalid link url: ${u()}`})})),L(C(()=>[i.data,h()],([r,e])=>{I(!1),!e||X(r)}));const k=v(()=>s().domain??G(u())??u()),j=v(()=>s().siteName??k()),$=v(()=>s().title??k()),A=v(()=>!!s().image&&!U()),E=()=>{h()&&b({kind:"open",cardId:i.cardId,url:u(),target:"tab"})},N=r=>{r.preventDefault(),E()},O=r=>{(r.key==="Enter"||r.key===" ")&&(r.preventDefault(),E())};return n(g,{get when(){return h()},get fallback(){return(()=>{var r=W(),e=r.firstChild;return a(r,n(D,{size:16,"aria-hidden":"true"}),e),m(()=>S(r,z("flex items-center gap-2 rounded-xl border border-destructive/40 bg-destructive/10 px-3 py-2 text-sm text-destructive dark:border-red-400/50 dark:bg-destructive/20 dark:text-red-400",i.class))),r})()},get children(){var r=V();return r.$$keydown=O,r.$$click=N,a(r,n(g,{get when(){return y()},get children(){return[n(w,{class:"aspect-[16/9] w-full rounded-none"}),(()=>{var e=K();return a(e,n(w,{class:"h-3 w-1/3"}),null),a(e,n(w,{class:"h-4 w-2/3"}),null),a(e,n(w,{class:"h-3 w-full"}),null),e})()]}}),null),a(r,n(g,{get when(){return!y()},get children(){return[n(g,{get when(){return A()},get children(){var e=M();return e.addEventListener("error",()=>I(!0)),m(t=>{var l=s().image,o=s().imageAlt??"";return l!==t.e&&f(e,"src",t.e=l),o!==t.t&&f(e,"alt",t.t=o),t},{e:void 0,t:void 0}),e}}),(()=>{var e=T(),t=e.firstChild,l=t.firstChild,o=t.nextSibling;return a(t,n(g,{get when(){return s().favicon},get fallback(){return n(D,{size:12,class:"shrink-0 opacity-70","aria-hidden":"true"})},get children(){var p=P();return m(()=>f(p,"src",s().favicon)),p}}),l),a(l,j),a(o,$),a(e,n(g,{get when(){return s().description},get children(){var p=Q();return a(p,()=>s().description),p}}),null),e})()]}}),null),m(e=>{var t=u(),l=`Open ${$()} on ${k()}`,o=z("group block overflow-hidden rounded-xl border border-border bg-card text-card-foreground no-underline outline-none transition-shadow","cursor-pointer hover:shadow-md focus-visible:ring-2 focus-visible:ring-ring",i.class);return t!==e.e&&f(r,"href",e.e=t),l!==e.t&&f(r,"aria-label",e.t=l),o!==e.a&&S(r,e.a=o),e},{e:void 0,t:void 0,a:void 0}),r}})}F(["click","keydown"]);export{Y as L};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{i as o,a as c,d as u,b as m,c as a,S as d,j as w,t as l,z as x,A as b}from"./define-B_tXMYRm.js";import{c as f}from"./cn-CU7UAWzk.js";import{H as y,a as z,b as S}from"./hover-card-6KtXpLwT.js";var C=l('<img alt=favicon width=14 height=14 class="size-3.5 rounded-full">'),_=l('<a target=_blank rel="noopener noreferrer"><span class="truncate tabular-nums text-center font-normal">'),k=l('<div class="flex items-center gap-1.5"><img alt=favicon class="size-4 rounded-full"width=16 height=16><div class="text-primary truncate text-sm">'),R=l('<div class="line-clamp-2 text-sm font-medium">'),F=l('<div class="text-muted-foreground line-clamp-2 text-sm">'),I=l('<a target=_blank rel="noopener noreferrer"class="flex flex-col gap-2 p-3">'),U=l("<div>");const v=x();function p(){const r=b(v);if(!r)throw new Error("Source.* must be used inside <Source>");return r}function $(r){const n=()=>r.href??"",t=()=>{const e=n();if(!e)return"";try{return new URL(e).hostname}catch{return e.split("/").pop()||e}};return a(v.Provider,{value:{get href(){return n()},get domain(){return t()}},get children(){return a(y,{openDelay:150,get children(){return r.children}})}})}function j(r){const n=p(),t=()=>r.label??n.domain.replace("www.","");return a(z,{get children(){var e=_(),s=e.firstChild;return o(e,a(d,{get when(){return w(()=>!!r.showFavicon)()&&n.href},get children(){var i=C();return c(()=>u(i,"src",`https://www.google.com/s2/favicons?sz=64&domain_url=${encodeURIComponent(n.href)}`)),i}}),s),o(s,t),c(i=>{var h=n.href||void 0,g=f("bg-muted text-foreground/80 hover:bg-muted-foreground/30 hover:text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background inline-flex h-5 max-w-32 items-center gap-1 overflow-hidden rounded-full py-0 text-xs no-underline transition-colors duration-150",r.showFavicon?"pr-2 pl-1":"px-2",r.class);return h!==i.e&&u(e,"href",i.e=h),g!==i.t&&m(e,i.t=g),i},{e:void 0,t:void 0}),e}})}function H(r){const n=p();return a(S,{get class(){return f("w-80 p-0 shadow-xs",r.class)},get children(){var t=I();return o(t,a(d,{get when(){return n.href},get children(){var e=k(),s=e.firstChild,i=s.nextSibling;return o(i,()=>n.domain.replace("www.","")),c(()=>u(s,"src",`https://www.google.com/s2/favicons?sz=64&domain_url=${encodeURIComponent(n.href)}`)),e}}),null),o(t,a(d,{get when(){return r.title},get children(){var e=R();return o(e,()=>r.title),e}}),null),o(t,a(d,{get when(){return r.description},get children(){var e=F();return o(e,()=>r.description),e}}),null),c(()=>u(t,"href",n.href||void 0)),t}})}function L(r){return(()=>{var n=U();return o(n,()=>r.children),c(t=>{var e=r.part,s=f("flex flex-wrap gap-1.5 mt-3",r.class);return e!==t.e&&u(n,"part",t.e=e),s!==t.t&&m(n,t.t=s),t},{e:void 0,t:void 0}),n})()}export{L as S,$ as a,j as b,H as c};
|