Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions app/api/artifacts/[artifactId]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Effect } from "effect"

import { AllukaRepository, runRepository } from "@/lib/alluka/repository"

export async function GET(
request: Request,
{ params }: { params: Promise<{ artifactId: string }> }
) {
const { artifactId } = await params
const url = new URL(request.url)
const sessionId = url.searchParams.get("sessionId")
const agentId = url.searchParams.get("agentId")
if (!sessionId || !agentId) {
return Response.json(
{ error: "Artifact context is required." },
{ status: 400 }
)
}

const result = await runRepository(
Effect.gen(function* () {
const repository = yield* AllukaRepository
return yield* repository.readArtifact(artifactId, sessionId, agentId)
})
)
if (!result) {
return Response.json({ error: "Artifact not found." }, { status: 404 })
}

if (url.searchParams.get("download") === "1") {
return new Response(result.content, {
headers: {
"content-disposition": `attachment; filename="${result.artifact.name.replaceAll('"', "")}"`,
"content-type": "text/plain; charset=utf-8",
"x-content-type-options": "nosniff",
},
})
}

return Response.json(
{
id: result.artifact.id,
name: result.artifact.name,
mediaType: result.artifact.mediaType,
content: result.content,
},
{
headers: {
"cache-control": "no-store",
"x-content-type-options": "nosniff",
},
}
)
}
55 changes: 55 additions & 0 deletions components/artifact-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"use client"

import { Eye, FileCode2 } from "lucide-react"

import { cn } from "@/lib/utils"

export type ArtifactReference = {
artifactId: string
name: string
sizeBytes: number
sessionId: string
agentId: string
}

export function ArtifactCard({
artifact,
onOpen,
className,
}: {
artifact: ArtifactReference
onOpen: (artifact: ArtifactReference) => void
className?: string
}) {
return (
<section
data-artifact-id={artifact.artifactId}
className={cn("alluka-surface-raised rounded-xl px-3 py-2.5", className)}
aria-label={`Artifact: ${artifact.name}`}
>
<div className="flex min-h-8 items-center gap-3">
<FileCode2
className="size-4 shrink-0 text-muted-foreground"
aria-hidden="true"
/>
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium text-foreground">
{artifact.name}
</p>
<p className="text-xs text-muted-foreground">
{artifact.sizeBytes.toLocaleString()} bytes
</p>
</div>
<button
type="button"
onClick={() => onOpen(artifact)}
className="alluka-control inline-flex h-7 shrink-0 items-center gap-1.5 rounded-md px-2.5 text-xs font-medium text-foreground hover:bg-white/10 focus-visible:ring-2 focus-visible:ring-ring"
aria-label={`Open ${artifact.name} in artifact inspector`}
>
<Eye className="size-3.5" />
Inspect
</button>
</div>
</section>
)
}
179 changes: 179 additions & 0 deletions components/artifact-inspector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
"use client"

import * as React from "react"
import { Check, Copy, Download, Loader2, PanelRightClose } from "lucide-react"

import {
createIsolatedHtmlDocument,
isHtmlArtifact,
} from "@/lib/alluka/artifact-preview"
import type { ArtifactReference } from "@/components/artifact-card"

type ArtifactContents = {
id: string
name: string
mediaType: string
content: string
}

function artifactUrl(artifact: ArtifactReference) {
const query = new URLSearchParams({
sessionId: artifact.sessionId,
agentId: artifact.agentId,
})
return `/api/artifacts/${encodeURIComponent(artifact.artifactId)}?${query}`
}

export function ArtifactInspector({
artifact,
onClose,
}: {
artifact: ArtifactReference | null
onClose: () => void
}) {
const [cache, setCache] = React.useState<Record<string, ArtifactContents>>({})
const [error, setError] = React.useState<{
artifactId: string
message: string
} | null>(null)
const [copied, setCopied] = React.useState(false)
const contents = artifact ? cache[artifact.artifactId] : undefined

React.useEffect(() => {
if (!artifact || contents) return
const controller = new AbortController()
void fetch(artifactUrl(artifact), {
cache: "no-store",
signal: controller.signal,
})
.then(async (response) => {
const payload: { error?: string } & Partial<ArtifactContents> =
await response.json().catch(() => ({}))
const { content, mediaType, name } = payload
if (!response.ok || !content || !mediaType || !name) {
throw new Error(payload.error ?? "Could not load this artifact.")
}
setCache((current) => ({
...current,
[artifact.artifactId]: {
id: payload.id ?? artifact.artifactId,
name,
mediaType,
content,
},
}))
})
.catch((cause) => {
if (!controller.signal.aborted) {
setError({
artifactId: artifact.artifactId,
message:
cause instanceof Error
? cause.message
: "Could not load this artifact.",
})
}
})
return () => controller.abort()
}, [artifact, contents])

if (!artifact) return null

const previewIsHtml = contents
? isHtmlArtifact(contents.name, contents.mediaType)
: false
const url = artifactUrl(artifact)
const currentError =
error?.artifactId === artifact.artifactId ? error.message : null

async function copyArtifact() {
if (!contents) return
await navigator.clipboard.writeText(contents.content)
setCopied(true)
window.setTimeout(() => setCopied(false), 1600)
}

return (
<aside
aria-label="Artifact inspector"
className="flex h-full min-h-0 w-full flex-col bg-[#111] shadow-[-18px_0_45px_rgba(0,0,0,.32)]"
>
<div className="flex min-h-14 items-center gap-3 border-b border-white/8 px-3">
<div className="min-w-0 flex-1">
<p className="text-[10px] font-semibold tracking-[0.16em] text-muted-foreground uppercase">
Artifact inspector
</p>
<p className="truncate text-sm font-medium">{artifact.name}</p>
</div>
<button
type="button"
onClick={onClose}
className="alluka-control grid size-7 place-items-center rounded-md text-muted-foreground hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
aria-label="Close artifact inspector"
>
<PanelRightClose className="size-3.5" />
</button>
</div>

<div className="flex flex-wrap items-center gap-2 border-b border-white/8 px-3 py-2">
<button
type="button"
onClick={() => void copyArtifact()}
disabled={!contents}
className="alluka-control inline-flex h-7 items-center gap-1.5 rounded-md px-2.5 text-xs text-foreground disabled:opacity-40"
>
{copied ? (
<Check className="size-3.5" />
) : (
<Copy className="size-3.5" />
)}
{copied ? "Copied" : "Copy source"}
</button>
<a
href={`${url}&download=1`}
className="alluka-control inline-flex h-7 items-center gap-1.5 rounded-md px-2.5 text-xs text-foreground"
>
<Download className="size-3.5" />
Download raw file
</a>
<p className="ml-auto text-xs text-muted-foreground">
Resize from the divider
</p>
</div>

<div className="min-h-0 flex-1 p-3">
{currentError ? (
<div
role="alert"
className="rounded-lg border border-destructive/30 bg-destructive/10 p-3 text-sm text-destructive"
>
{currentError}
</div>
) : !contents ? (
<div className="grid h-full place-items-center rounded-lg border border-white/8 bg-black/35 text-xs text-muted-foreground">
<Loader2 className="mr-2 inline size-3.5 animate-spin" /> Loading
artifact
</div>
) : previewIsHtml ? (
<iframe
title={`Isolated preview of ${contents.name}`}
sandbox="allow-scripts"
referrerPolicy="no-referrer"
srcDoc={createIsolatedHtmlDocument(contents.content)}
className="h-full w-full rounded-lg border border-white/10 bg-white"
/>
) : (
<div className="flex h-full flex-col gap-2">
<p className="text-xs text-muted-foreground">
Source preview. Use Download raw file for a native viewer or
unsupported format.
</p>
<pre className="alluka-scrollbar min-h-0 flex-1 overflow-auto rounded-lg border border-white/8 bg-black/35 p-3 font-mono text-xs leading-5 whitespace-pre-wrap text-foreground/85">
{contents.content}
</pre>
</div>
)}
</div>
</aside>
)
}
Loading