@saadnvd1/agent-os 0.3.758 → 0.3.762

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.
@@ -1,14 +1,23 @@
1
1
  import { NextResponse } from "next/server";
2
- import { getArtifact, readArtifactHtml } from "@/lib/artifacts/store";
2
+ import {
3
+ getArtifact,
4
+ latestVersion,
5
+ readArtifactHtml,
6
+ } from "@/lib/artifacts/store";
3
7
  import { ARTIFACT_HEADERS, prepareArtifact } from "@/lib/artifacts/serve";
4
8
 
5
- // GET /api/artifacts/[id] - a page an agent showed, sandboxed
9
+ // GET /api/artifacts/[id] - a page an agent showed, sandboxed. That exact
10
+ // version, or with ?v=latest the page's newest.
6
11
  export async function GET(
7
- _request: Request,
12
+ request: Request,
8
13
  { params }: { params: Promise<{ id: string }> }
9
14
  ) {
10
15
  const { id } = await params;
11
- const artifact = getArtifact(id);
16
+ const asked = getArtifact(id);
17
+ const artifact =
18
+ asked && new URL(request.url).searchParams.get("v") === "latest"
19
+ ? latestVersion(asked.page_id)
20
+ : asked;
12
21
  const html = artifact ? await readArtifactHtml(artifact) : null;
13
22
  if (html === null)
14
23
  return NextResponse.json({ error: "Artifact not found" }, { status: 404 });
@@ -0,0 +1,16 @@
1
+ import { NextResponse } from "next/server";
2
+ import { pageVersions } from "@/lib/artifacts/versions";
3
+ import { pdfAvailable } from "@/lib/share/pdf";
4
+
5
+ // GET /api/artifacts/[id]/versions - every version of the page this is one
6
+ // of, oldest first, and what can be done with it
7
+ export async function GET(
8
+ _request: Request,
9
+ { params }: { params: Promise<{ id: string }> }
10
+ ) {
11
+ const { id } = await params;
12
+ const page = pageVersions(id, { pdf: await pdfAvailable() });
13
+ if (!page)
14
+ return NextResponse.json({ error: "Artifact not found" }, { status: 404 });
15
+ return NextResponse.json(page);
16
+ }
@@ -1,4 +1,6 @@
1
1
  import { NextResponse } from "next/server";
2
+ import { artifactActions } from "@/lib/artifacts/actions";
3
+ import { pdfAvailable } from "@/lib/share/pdf";
2
4
  import { peerLinkOf } from "@/lib/hosts/peer-actions";
3
5
  import { collectArtifacts, getSession } from "@/lib/session-artifacts/collect";
4
6
  import { peerArtifacts } from "@/lib/session-artifacts/peer";
@@ -29,5 +31,11 @@ export async function GET(
29
31
  }
30
32
  }
31
33
  const entries = await collectArtifacts(session);
32
- return NextResponse.json({ artifacts: entries.map((e) => e.artifact) });
34
+ const ctx = { pdf: await pdfAvailable() };
35
+ return NextResponse.json({
36
+ artifacts: entries.map((e) => ({
37
+ ...e.artifact,
38
+ actions: artifactActions(e.artifact, ctx),
39
+ })),
40
+ });
33
41
  }
@@ -0,0 +1,308 @@
1
+ "use client";
2
+
3
+ import type { ReactNode } from "react";
4
+
5
+ import {
6
+ Download,
7
+ ExternalLink,
8
+ Loader2,
9
+ Maximize2,
10
+ MoreHorizontal,
11
+ Share2,
12
+ } from "lucide-react";
13
+ import { Button } from "@/components/ui/button";
14
+ import {
15
+ DropdownMenu,
16
+ DropdownMenuContent,
17
+ DropdownMenuItem,
18
+ DropdownMenuTrigger,
19
+ } from "@/components/ui/dropdown-menu";
20
+ import { artifactUrl } from "@/components/Chat/ArtifactFrame";
21
+ import {
22
+ DownloadMenu,
23
+ useShareDownload,
24
+ } from "@/components/Share/DownloadMenu";
25
+ import type { PreviewRequest } from "@/data/shares";
26
+ import type { ArtifactAction, DownloadFormat } from "@/lib/artifacts/actions";
27
+ import { shareUiActions } from "@/stores/shareUi";
28
+ import { cn } from "@/lib/utils";
29
+
30
+ export const ICON_BUTTON =
31
+ "text-muted-foreground hover:text-foreground hover:bg-foreground/[0.06] inline-flex h-11 w-11 items-center justify-center rounded-lg md:h-8 md:w-8";
32
+
33
+ const FORMAT_LABEL: Record<DownloadFormat, string> = {
34
+ pdf: "Download PDF",
35
+ html: "Download HTML page",
36
+ md: "Download Markdown",
37
+ };
38
+
39
+ interface Props {
40
+ // The exact version every action acts on.
41
+ artifactId: string;
42
+ title: string;
43
+ // Named in the share dialog when the page has more than one.
44
+ version?: number;
45
+ actions: ArtifactAction[];
46
+ // icons: the chat card's title bar. labelled: the Artifacts view's header.
47
+ // bar: the Artifacts view's bottom bar on a phone.
48
+ variant: "icons" | "labelled" | "bar";
49
+ onFull?: () => void;
50
+ }
51
+
52
+ // A rendered page's actions: full screen, share, open, download. One place
53
+ // for the chat card and the Artifacts view, from the server's list.
54
+ export function ArtifactActions({
55
+ artifactId,
56
+ title,
57
+ version,
58
+ actions,
59
+ variant,
60
+ onFull,
61
+ }: Props) {
62
+ const has = (id: ArtifactAction["id"]) => actions.some((a) => a.id === id);
63
+ const formats = actions.find((a) => a.id === "download")?.formats ?? [];
64
+ const full = has("fullscreen") && onFull;
65
+ const request: PreviewRequest = {
66
+ kind: "artifact",
67
+ artifactId,
68
+ includeTools: false,
69
+ };
70
+ const href = artifactUrl(artifactId);
71
+ const share = () =>
72
+ shareUiActions.open({ kind: "artifact", artifactId, title, version });
73
+
74
+ if (variant === "bar")
75
+ return (
76
+ <div className="flex gap-2">
77
+ {has("share") && (
78
+ <Button className="h-11 flex-1" onClick={share}>
79
+ <Share2 className="h-4 w-4" /> Share
80
+ </Button>
81
+ )}
82
+ {has("open") && (
83
+ <Button variant="outline" className="h-11 flex-1" asChild>
84
+ <a href={href} target="_blank" rel="noopener noreferrer">
85
+ <ExternalLink className="h-4 w-4" /> Open
86
+ </a>
87
+ </Button>
88
+ )}
89
+ {full && (
90
+ <Button
91
+ variant="outline"
92
+ className="h-11 w-11 shrink-0"
93
+ onClick={onFull}
94
+ aria-label="Full screen"
95
+ >
96
+ <Maximize2 className="h-4 w-4" />
97
+ </Button>
98
+ )}
99
+ {formats.length > 0 && (
100
+ <More request={request} title={title} formats={formats}>
101
+ {(pending) => (
102
+ <Button
103
+ variant="outline"
104
+ className="h-11 w-11 shrink-0"
105
+ aria-label="More"
106
+ >
107
+ {pending ? (
108
+ <Loader2 className="h-4 w-4 animate-spin" />
109
+ ) : (
110
+ <MoreHorizontal className="h-4 w-4" />
111
+ )}
112
+ </Button>
113
+ )}
114
+ </More>
115
+ )}
116
+ </div>
117
+ );
118
+
119
+ if (variant === "labelled") {
120
+ const label = "hidden @lg:inline";
121
+ return (
122
+ <div className="flex shrink-0 gap-1.5">
123
+ {full && (
124
+ <Button
125
+ variant="outline"
126
+ size="sm"
127
+ onClick={onFull}
128
+ title="Full screen"
129
+ >
130
+ <Maximize2 className="h-3.5 w-3.5" />
131
+ <span className={label}>Full screen</span>
132
+ </Button>
133
+ )}
134
+ {has("share") && (
135
+ <Button variant="outline" size="sm" onClick={share} title="Share">
136
+ <Share2 className="h-3.5 w-3.5" />
137
+ <span className={label}>Share</span>
138
+ </Button>
139
+ )}
140
+ {has("open") && (
141
+ <Button variant="outline" size="sm" asChild>
142
+ <a
143
+ href={href}
144
+ target="_blank"
145
+ rel="noopener noreferrer"
146
+ title="Open in new tab"
147
+ >
148
+ <ExternalLink className="h-3.5 w-3.5" />
149
+ <span className={label}>Open</span>
150
+ </a>
151
+ </Button>
152
+ )}
153
+ {formats.length > 0 && (
154
+ <DownloadMenu
155
+ formats={formats}
156
+ request={request}
157
+ title={title}
158
+ disabled={false}
159
+ trigger={(pending) => (
160
+ <Button variant="outline" size="sm" title="Download">
161
+ {pending ? (
162
+ <Loader2 className="h-3.5 w-3.5 animate-spin" />
163
+ ) : (
164
+ <Download className="h-3.5 w-3.5" />
165
+ )}
166
+ <span className={label}>Download</span>
167
+ </Button>
168
+ )}
169
+ />
170
+ )}
171
+ </div>
172
+ );
173
+ }
174
+
175
+ // The card: every action as an icon from md up; on a phone, Share and a
176
+ // More menu, so the title keeps its room.
177
+ const wide = "hidden md:inline-flex";
178
+ return (
179
+ <div className="flex shrink-0 items-center">
180
+ {full && (
181
+ <button
182
+ type="button"
183
+ onClick={onFull}
184
+ className={cn(ICON_BUTTON, wide)}
185
+ aria-label="Full screen"
186
+ title="Full screen"
187
+ >
188
+ <Maximize2 className="h-4 w-4" />
189
+ </button>
190
+ )}
191
+ {has("share") && (
192
+ <button
193
+ type="button"
194
+ onClick={share}
195
+ className={ICON_BUTTON}
196
+ aria-label="Share"
197
+ title="Share"
198
+ >
199
+ <Share2 className="h-4 w-4" />
200
+ </button>
201
+ )}
202
+ {has("open") && (
203
+ <a
204
+ href={href}
205
+ target="_blank"
206
+ rel="noopener noreferrer"
207
+ className={cn(ICON_BUTTON, wide)}
208
+ aria-label="Open in new tab"
209
+ title="Open in new tab"
210
+ >
211
+ <ExternalLink className="h-4 w-4" />
212
+ </a>
213
+ )}
214
+ {formats.length > 0 && (
215
+ <DownloadMenu
216
+ formats={formats}
217
+ request={request}
218
+ title={title}
219
+ disabled={false}
220
+ trigger={(pending) => (
221
+ <button
222
+ type="button"
223
+ className={cn(ICON_BUTTON, wide)}
224
+ aria-label="Download"
225
+ title="Download"
226
+ >
227
+ {pending ? (
228
+ <Loader2 className="h-4 w-4 animate-spin" />
229
+ ) : (
230
+ <Download className="h-4 w-4" />
231
+ )}
232
+ </button>
233
+ )}
234
+ />
235
+ )}
236
+ <More
237
+ request={request}
238
+ title={title}
239
+ formats={formats}
240
+ onFull={full ? onFull : undefined}
241
+ href={has("open") ? href : undefined}
242
+ >
243
+ {(pending) => (
244
+ <button
245
+ type="button"
246
+ className={cn(ICON_BUTTON, "md:hidden")}
247
+ aria-label="More"
248
+ >
249
+ {pending ? (
250
+ <Loader2 className="h-4 w-4 animate-spin" />
251
+ ) : (
252
+ <MoreHorizontal className="h-4 w-4" />
253
+ )}
254
+ </button>
255
+ )}
256
+ </More>
257
+ </div>
258
+ );
259
+ }
260
+
261
+ // The phone's overflow menu: full screen and open when they didn't fit, then
262
+ // each download format.
263
+ function More({
264
+ request,
265
+ title,
266
+ formats,
267
+ onFull,
268
+ href,
269
+ children,
270
+ }: {
271
+ request: PreviewRequest;
272
+ title: string;
273
+ formats: DownloadFormat[];
274
+ onFull?: () => void;
275
+ href?: string;
276
+ children: (pending: boolean) => ReactNode;
277
+ }) {
278
+ const { choose, pending } = useShareDownload(request, title);
279
+ if (!onFull && !href && !formats.length) return null;
280
+ return (
281
+ <DropdownMenu>
282
+ <DropdownMenuTrigger asChild>{children(pending)}</DropdownMenuTrigger>
283
+ <DropdownMenuContent align="end" className="min-w-52">
284
+ {onFull && (
285
+ <DropdownMenuItem className="min-h-11 gap-2" onSelect={onFull}>
286
+ <Maximize2 className="h-4 w-4" /> Full screen
287
+ </DropdownMenuItem>
288
+ )}
289
+ {href && (
290
+ <DropdownMenuItem className="min-h-11 gap-2" asChild>
291
+ <a href={href} target="_blank" rel="noopener noreferrer">
292
+ <ExternalLink className="h-4 w-4" /> Open in new tab
293
+ </a>
294
+ </DropdownMenuItem>
295
+ )}
296
+ {formats.map((f) => (
297
+ <DropdownMenuItem
298
+ key={f}
299
+ className="min-h-11 gap-2"
300
+ onSelect={() => void choose(f)}
301
+ >
302
+ <Download className="h-4 w-4" /> {FORMAT_LABEL[f]}
303
+ </DropdownMenuItem>
304
+ ))}
305
+ </DropdownMenuContent>
306
+ </DropdownMenu>
307
+ );
308
+ }
@@ -0,0 +1,58 @@
1
+ "use client";
2
+
3
+ import type { ReactNode } from "react";
4
+ import { X } from "lucide-react";
5
+ import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
6
+ import { ArtifactFrame } from "@/components/Chat/ArtifactFrame";
7
+ import { ICON_BUTTON } from "./ArtifactActions";
8
+
9
+ // A rendered page over the whole screen, its actions in the bar.
10
+ export function ArtifactFullscreen({
11
+ open,
12
+ onOpenChange,
13
+ artifactId,
14
+ src,
15
+ title,
16
+ actions,
17
+ }: {
18
+ open: boolean;
19
+ onOpenChange: (open: boolean) => void;
20
+ artifactId: string;
21
+ src?: string;
22
+ title: string;
23
+ actions?: ReactNode;
24
+ }) {
25
+ return (
26
+ <Dialog open={open} onOpenChange={onOpenChange}>
27
+ <DialogContent
28
+ showCloseButton={false}
29
+ className="flex h-[100dvh] w-screen max-w-none flex-col gap-0 rounded-none p-0 sm:max-w-none"
30
+ >
31
+ <div className="flex items-center gap-2 py-1 pt-[max(0.25rem,env(safe-area-inset-top))] pr-1 pl-4">
32
+ <DialogTitle className="min-w-0 flex-1 truncate text-sm font-medium">
33
+ {title}
34
+ </DialogTitle>
35
+ {actions}
36
+ <button
37
+ type="button"
38
+ onClick={() => onOpenChange(false)}
39
+ className={ICON_BUTTON}
40
+ aria-label="Close"
41
+ >
42
+ <X className="h-4 w-4" />
43
+ </button>
44
+ </div>
45
+ <div className="min-h-0 flex-1 pb-[env(safe-area-inset-bottom)]">
46
+ {open && (
47
+ <ArtifactFrame
48
+ artifactId={artifactId}
49
+ src={src}
50
+ title={title}
51
+ fill
52
+ />
53
+ )}
54
+ </div>
55
+ </DialogContent>
56
+ </Dialog>
57
+ );
58
+ }