@gmickel/gno 0.41.1 → 0.42.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 +61 -0
- package/assets/screenshots/publish-reader.jpg +0 -0
- package/assets/skill/SKILL.md +2 -0
- package/assets/skill/cli-reference.md +28 -0
- package/package.json +1 -1
- package/src/cli/commands/index.ts +6 -0
- package/src/cli/commands/publish.ts +140 -0
- package/src/cli/options.ts +2 -0
- package/src/cli/program.ts +72 -0
- package/src/publish/artifact.ts +252 -0
- package/src/publish/export-service.ts +238 -0
- package/src/serve/AGENTS.md +17 -16
- package/src/serve/CLAUDE.md +17 -16
- package/src/serve/public/lib/publish-export.ts +21 -0
- package/src/serve/public/pages/Collections.tsx +63 -0
- package/src/serve/public/pages/DocView.tsx +71 -0
- package/src/serve/routes/api.ts +82 -0
- package/src/serve/server.ts +12 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core publish export service used by CLI and local web UI.
|
|
3
|
+
*
|
|
4
|
+
* @module src/publish/export-service
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Collection } from "../config/types";
|
|
8
|
+
import type { DocumentRow, StorePort, TagRow } from "../store/types";
|
|
9
|
+
|
|
10
|
+
import { parseRef } from "../cli/commands/ref-parser";
|
|
11
|
+
import { parseFrontmatter } from "../ingestion/frontmatter";
|
|
12
|
+
import {
|
|
13
|
+
buildPublishArtifact,
|
|
14
|
+
buildExportedMetadata,
|
|
15
|
+
derivePublishSlug,
|
|
16
|
+
deriveExportedSlug,
|
|
17
|
+
deriveExportedSummary,
|
|
18
|
+
deriveExportedTitle,
|
|
19
|
+
isPublishVisibility,
|
|
20
|
+
type PublishArtifact,
|
|
21
|
+
type PublishArtifactNote,
|
|
22
|
+
type PublishVisibility,
|
|
23
|
+
} from "./artifact";
|
|
24
|
+
|
|
25
|
+
export interface PublishExportCoreOptions {
|
|
26
|
+
routeSlug?: string;
|
|
27
|
+
summary?: string;
|
|
28
|
+
title?: string;
|
|
29
|
+
visibility?: PublishVisibility;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function resolveVisibility(visibility?: string): PublishVisibility {
|
|
33
|
+
if (visibility === undefined) {
|
|
34
|
+
return "public";
|
|
35
|
+
}
|
|
36
|
+
if (!isPublishVisibility(visibility)) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`Invalid visibility: ${visibility}. Must be public, secret-link, invite-only, or encrypted.`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
return visibility;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function lookupDocument(
|
|
45
|
+
store: StorePort,
|
|
46
|
+
ref: string
|
|
47
|
+
): Promise<DocumentRow | null> {
|
|
48
|
+
const parsed = parseRef(ref);
|
|
49
|
+
if ("error" in parsed) {
|
|
50
|
+
throw new Error(parsed.error);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
switch (parsed.type) {
|
|
54
|
+
case "docid": {
|
|
55
|
+
const result = await store.getDocumentByDocid(parsed.value);
|
|
56
|
+
if (!result.ok) {
|
|
57
|
+
throw new Error(result.error.message);
|
|
58
|
+
}
|
|
59
|
+
return result.value;
|
|
60
|
+
}
|
|
61
|
+
case "uri": {
|
|
62
|
+
const result = await store.getDocumentByUri(parsed.value);
|
|
63
|
+
if (!result.ok) {
|
|
64
|
+
throw new Error(result.error.message);
|
|
65
|
+
}
|
|
66
|
+
return result.value;
|
|
67
|
+
}
|
|
68
|
+
case "collPath": {
|
|
69
|
+
if (!(parsed.collection && parsed.relPath)) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
const result = await store.getDocument(parsed.collection, parsed.relPath);
|
|
73
|
+
if (!result.ok) {
|
|
74
|
+
throw new Error(result.error.message);
|
|
75
|
+
}
|
|
76
|
+
return result.value;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function loadDocumentMarkdown(
|
|
82
|
+
store: StorePort,
|
|
83
|
+
doc: DocumentRow
|
|
84
|
+
): Promise<string> {
|
|
85
|
+
if (!doc.mirrorHash) {
|
|
86
|
+
throw new Error(`Document has no converted content: ${doc.uri}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const result = await store.getContent(doc.mirrorHash);
|
|
90
|
+
if (!result.ok || !result.value) {
|
|
91
|
+
throw new Error(`Unable to load content for ${doc.uri}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return result.value;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function loadDocumentTags(
|
|
98
|
+
store: StorePort,
|
|
99
|
+
doc: DocumentRow
|
|
100
|
+
): Promise<TagRow[]> {
|
|
101
|
+
const result = await store.getTagsForDoc(doc.id);
|
|
102
|
+
if (!result.ok) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`Unable to load tags for ${doc.uri}: ${result.error.message}`
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return result.value;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function chooseHomeNoteSlug(notes: PublishArtifactNote[]) {
|
|
112
|
+
const preferred = notes.find((note) =>
|
|
113
|
+
["home", "index", "readme"].includes(note.slug)
|
|
114
|
+
);
|
|
115
|
+
return preferred?.slug ?? notes[0]?.slug;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function resolveCollection(
|
|
119
|
+
collections: Collection[],
|
|
120
|
+
target: string
|
|
121
|
+
): Collection | null {
|
|
122
|
+
return collections.find((collection) => collection.name === target) ?? null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function exportCollectionArtifact(
|
|
126
|
+
store: StorePort,
|
|
127
|
+
collections: Collection[],
|
|
128
|
+
target: string,
|
|
129
|
+
options: PublishExportCoreOptions
|
|
130
|
+
) {
|
|
131
|
+
const collection = resolveCollection(collections, target);
|
|
132
|
+
if (!collection) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const docsResult = await store.listDocuments(collection.name);
|
|
137
|
+
if (!docsResult.ok) {
|
|
138
|
+
throw new Error(docsResult.error.message);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const activeDocs = docsResult.value
|
|
142
|
+
.filter((doc) => doc.active)
|
|
143
|
+
.sort((left, right) => left.uri.localeCompare(right.uri));
|
|
144
|
+
|
|
145
|
+
if (activeDocs.length === 0) {
|
|
146
|
+
throw new Error(`Collection "${collection.name}" has no active documents`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const notes: PublishArtifactNote[] = [];
|
|
150
|
+
for (const doc of activeDocs) {
|
|
151
|
+
const markdown = await loadDocumentMarkdown(store, doc);
|
|
152
|
+
const tags = await loadDocumentTags(store, doc);
|
|
153
|
+
const frontmatter = parseFrontmatter(markdown).metadata;
|
|
154
|
+
const title = deriveExportedTitle(doc);
|
|
155
|
+
notes.push({
|
|
156
|
+
markdown,
|
|
157
|
+
metadata: buildExportedMetadata(doc, frontmatter, tags),
|
|
158
|
+
slug: deriveExportedSlug(doc),
|
|
159
|
+
summary: deriveExportedSummary(markdown, frontmatter),
|
|
160
|
+
title,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const title = options.title ?? collection.name;
|
|
165
|
+
const summary =
|
|
166
|
+
options.summary ??
|
|
167
|
+
`Published snapshot of the ${collection.name} collection from local GNO.`;
|
|
168
|
+
const routeSlug = derivePublishSlug([
|
|
169
|
+
options.routeSlug ?? "",
|
|
170
|
+
collection.name,
|
|
171
|
+
target,
|
|
172
|
+
]);
|
|
173
|
+
|
|
174
|
+
return buildPublishArtifact({
|
|
175
|
+
homeNoteSlug: chooseHomeNoteSlug(notes),
|
|
176
|
+
notes,
|
|
177
|
+
routeSlug,
|
|
178
|
+
source: collection.name,
|
|
179
|
+
sourceType: "collection",
|
|
180
|
+
summary,
|
|
181
|
+
title,
|
|
182
|
+
visibility: resolveVisibility(options.visibility),
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function exportDocumentArtifact(
|
|
187
|
+
store: StorePort,
|
|
188
|
+
target: string,
|
|
189
|
+
options: PublishExportCoreOptions
|
|
190
|
+
) {
|
|
191
|
+
const doc = await lookupDocument(store, target);
|
|
192
|
+
if (!doc?.active) {
|
|
193
|
+
throw new Error(`Document not found: ${target}`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const markdown = await loadDocumentMarkdown(store, doc);
|
|
197
|
+
const tags = await loadDocumentTags(store, doc);
|
|
198
|
+
const frontmatter = parseFrontmatter(markdown).metadata;
|
|
199
|
+
const title = options.title ?? deriveExportedTitle(doc);
|
|
200
|
+
const summary =
|
|
201
|
+
options.summary ?? deriveExportedSummary(markdown, frontmatter);
|
|
202
|
+
const slug = deriveExportedSlug(doc);
|
|
203
|
+
|
|
204
|
+
return buildPublishArtifact({
|
|
205
|
+
notes: [
|
|
206
|
+
{
|
|
207
|
+
markdown,
|
|
208
|
+
metadata: buildExportedMetadata(doc, frontmatter, tags),
|
|
209
|
+
slug,
|
|
210
|
+
summary,
|
|
211
|
+
title,
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
routeSlug: derivePublishSlug([options.routeSlug ?? "", slug, target]),
|
|
215
|
+
source: doc.uri,
|
|
216
|
+
sourceType: "note",
|
|
217
|
+
summary,
|
|
218
|
+
title,
|
|
219
|
+
visibility: resolveVisibility(options.visibility),
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export async function exportPublishArtifact(input: {
|
|
224
|
+
collections: Collection[];
|
|
225
|
+
options: PublishExportCoreOptions;
|
|
226
|
+
store: StorePort;
|
|
227
|
+
target: string;
|
|
228
|
+
}): Promise<PublishArtifact> {
|
|
229
|
+
return (
|
|
230
|
+
(await exportCollectionArtifact(
|
|
231
|
+
input.store,
|
|
232
|
+
input.collections,
|
|
233
|
+
input.target,
|
|
234
|
+
input.options
|
|
235
|
+
)) ??
|
|
236
|
+
(await exportDocumentArtifact(input.store, input.target, input.options))
|
|
237
|
+
);
|
|
238
|
+
}
|
package/src/serve/AGENTS.md
CHANGED
|
@@ -65,22 +65,23 @@ Answer generation uses shared module to stay in sync with CLI:
|
|
|
65
65
|
|
|
66
66
|
## API Endpoints
|
|
67
67
|
|
|
68
|
-
| Endpoint
|
|
69
|
-
|
|
|
70
|
-
| `/api/health`
|
|
71
|
-
| `/api/status`
|
|
72
|
-
| `/api/capabilities`
|
|
73
|
-
| `/api/collections`
|
|
74
|
-
| `/api/
|
|
75
|
-
| `/api/
|
|
76
|
-
| `/api/
|
|
77
|
-
| `/api/
|
|
78
|
-
| `/api/
|
|
79
|
-
| `/api/
|
|
80
|
-
| `/api/presets`
|
|
81
|
-
| `/api/
|
|
82
|
-
| `/api/models/
|
|
83
|
-
| `/api/
|
|
68
|
+
| Endpoint | Method | Description |
|
|
69
|
+
| --------------------- | ------ | ------------------------------------------ |
|
|
70
|
+
| `/api/health` | GET | Health check |
|
|
71
|
+
| `/api/status` | GET | Index stats, onboarding, health, bootstrap |
|
|
72
|
+
| `/api/capabilities` | GET | Available features |
|
|
73
|
+
| `/api/collections` | GET | List collections |
|
|
74
|
+
| `/api/publish/export` | POST | Export gno.sh publish artifact JSON |
|
|
75
|
+
| `/api/docs` | GET | List documents |
|
|
76
|
+
| `/api/doc` | GET | Get document content |
|
|
77
|
+
| `/api/search` | POST | BM25 search |
|
|
78
|
+
| `/api/query` | POST | Hybrid search |
|
|
79
|
+
| `/api/ask` | POST | AI answer with citations |
|
|
80
|
+
| `/api/presets` | GET | List model presets |
|
|
81
|
+
| `/api/presets` | POST | Switch preset (hot-reload) |
|
|
82
|
+
| `/api/models/status` | GET | Download progress |
|
|
83
|
+
| `/api/models/pull` | POST | Start model download |
|
|
84
|
+
| `/api/tags` | GET | List tags (with counts) |
|
|
84
85
|
|
|
85
86
|
## Frontend
|
|
86
87
|
|
package/src/serve/CLAUDE.md
CHANGED
|
@@ -67,22 +67,23 @@ Answer generation uses shared module to stay in sync with CLI:
|
|
|
67
67
|
|
|
68
68
|
## API Endpoints
|
|
69
69
|
|
|
70
|
-
| Endpoint
|
|
71
|
-
|
|
|
72
|
-
| `/api/health`
|
|
73
|
-
| `/api/status`
|
|
74
|
-
| `/api/capabilities`
|
|
75
|
-
| `/api/collections`
|
|
76
|
-
| `/api/
|
|
77
|
-
| `/api/
|
|
78
|
-
| `/api/
|
|
79
|
-
| `/api/
|
|
80
|
-
| `/api/
|
|
81
|
-
| `/api/
|
|
82
|
-
| `/api/presets`
|
|
83
|
-
| `/api/
|
|
84
|
-
| `/api/models/
|
|
85
|
-
| `/api/
|
|
70
|
+
| Endpoint | Method | Description |
|
|
71
|
+
| --------------------- | ------ | ------------------------------------------ |
|
|
72
|
+
| `/api/health` | GET | Health check |
|
|
73
|
+
| `/api/status` | GET | Index stats, onboarding, health, bootstrap |
|
|
74
|
+
| `/api/capabilities` | GET | Available features |
|
|
75
|
+
| `/api/collections` | GET | List collections |
|
|
76
|
+
| `/api/publish/export` | POST | Export gno.sh publish artifact JSON |
|
|
77
|
+
| `/api/docs` | GET | List documents |
|
|
78
|
+
| `/api/doc` | GET | Get document content |
|
|
79
|
+
| `/api/search` | POST | BM25 search |
|
|
80
|
+
| `/api/query` | POST | Hybrid search |
|
|
81
|
+
| `/api/ask` | POST | AI answer with citations |
|
|
82
|
+
| `/api/presets` | GET | List model presets |
|
|
83
|
+
| `/api/presets` | POST | Switch preset (hot-reload) |
|
|
84
|
+
| `/api/models/status` | GET | Download progress |
|
|
85
|
+
| `/api/models/pull` | POST | Start model download |
|
|
86
|
+
| `/api/tags` | GET | List tags (with counts) |
|
|
86
87
|
|
|
87
88
|
## Frontend
|
|
88
89
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { PublishArtifact } from "../../../publish/artifact";
|
|
2
|
+
|
|
3
|
+
export interface PublishExportResponse {
|
|
4
|
+
artifact: PublishArtifact;
|
|
5
|
+
fileName: string;
|
|
6
|
+
uploadUrl: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function downloadPublishArtifactFile(
|
|
10
|
+
input: PublishExportResponse
|
|
11
|
+
): void {
|
|
12
|
+
const blob = new Blob([JSON.stringify(input.artifact, null, 2)], {
|
|
13
|
+
type: "application/json",
|
|
14
|
+
});
|
|
15
|
+
const href = URL.createObjectURL(blob);
|
|
16
|
+
const anchor = document.createElement("a");
|
|
17
|
+
anchor.href = href;
|
|
18
|
+
anchor.download = input.fileName;
|
|
19
|
+
anchor.click();
|
|
20
|
+
URL.revokeObjectURL(href);
|
|
21
|
+
}
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
Loader2Icon,
|
|
24
24
|
MoreVerticalIcon,
|
|
25
25
|
RefreshCwIcon,
|
|
26
|
+
Share2Icon,
|
|
26
27
|
} from "lucide-react";
|
|
27
28
|
import { useCallback, useEffect, useState } from "react";
|
|
28
29
|
|
|
@@ -66,6 +67,10 @@ import {
|
|
|
66
67
|
TooltipTrigger,
|
|
67
68
|
} from "../components/ui/tooltip";
|
|
68
69
|
import { apiFetch } from "../hooks/use-api";
|
|
70
|
+
import {
|
|
71
|
+
downloadPublishArtifactFile,
|
|
72
|
+
type PublishExportResponse,
|
|
73
|
+
} from "../lib/publish-export";
|
|
69
74
|
|
|
70
75
|
interface PageProps {
|
|
71
76
|
navigate: (to: string | number) => void;
|
|
@@ -119,9 +124,11 @@ interface CollectionCardProps {
|
|
|
119
124
|
collection: CollectionStats;
|
|
120
125
|
onBrowse: () => void;
|
|
121
126
|
onEmbeddingCleanup: () => void;
|
|
127
|
+
onExport: () => void;
|
|
122
128
|
onModelSettings: () => void;
|
|
123
129
|
onReindex: () => void;
|
|
124
130
|
onRemove: () => void;
|
|
131
|
+
isExporting: boolean;
|
|
125
132
|
isReindexing: boolean;
|
|
126
133
|
}
|
|
127
134
|
|
|
@@ -149,9 +156,11 @@ function CollectionCard({
|
|
|
149
156
|
collection,
|
|
150
157
|
onBrowse,
|
|
151
158
|
onEmbeddingCleanup,
|
|
159
|
+
onExport,
|
|
152
160
|
onModelSettings,
|
|
153
161
|
onReindex,
|
|
154
162
|
onRemove,
|
|
163
|
+
isExporting,
|
|
155
164
|
isReindexing,
|
|
156
165
|
}: CollectionCardProps) {
|
|
157
166
|
const embedPercent =
|
|
@@ -216,6 +225,20 @@ function CollectionCard({
|
|
|
216
225
|
<DatabaseIcon className="mr-2 size-4" />
|
|
217
226
|
Embedding cleanup
|
|
218
227
|
</DropdownMenuItem>
|
|
228
|
+
<DropdownMenuItem
|
|
229
|
+
disabled={actionsDisabled || isExporting}
|
|
230
|
+
onClick={(event) => {
|
|
231
|
+
event.stopPropagation();
|
|
232
|
+
onExport();
|
|
233
|
+
}}
|
|
234
|
+
>
|
|
235
|
+
{isExporting ? (
|
|
236
|
+
<Loader2Icon className="mr-2 size-4 animate-spin" />
|
|
237
|
+
) : (
|
|
238
|
+
<Share2Icon className="mr-2 size-4" />
|
|
239
|
+
)}
|
|
240
|
+
Export for gno.sh
|
|
241
|
+
</DropdownMenuItem>
|
|
219
242
|
<DropdownMenuSeparator />
|
|
220
243
|
<DropdownMenuItem
|
|
221
244
|
disabled={actionsDisabled || isReindexing}
|
|
@@ -337,6 +360,10 @@ export default function Collections({ navigate }: PageProps) {
|
|
|
337
360
|
const [syncJobId, setSyncJobId] = useState<string | null>(null);
|
|
338
361
|
const [syncTarget, setSyncTarget] = useState<SyncTarget>(null);
|
|
339
362
|
const [syncError, setSyncError] = useState<string | null>(null);
|
|
363
|
+
const [exportError, setExportError] = useState<string | null>(null);
|
|
364
|
+
const [exportingCollectionName, setExportingCollectionName] = useState<
|
|
365
|
+
string | null
|
|
366
|
+
>(null);
|
|
340
367
|
const [removeDialog, setRemoveDialog] = useState<CollectionStats | null>(
|
|
341
368
|
null
|
|
342
369
|
);
|
|
@@ -470,6 +497,30 @@ export default function Collections({ navigate }: PageProps) {
|
|
|
470
497
|
await loadCollections();
|
|
471
498
|
};
|
|
472
499
|
|
|
500
|
+
const handleExport = async (name: string) => {
|
|
501
|
+
setExportError(null);
|
|
502
|
+
setExportingCollectionName(name);
|
|
503
|
+
|
|
504
|
+
const { data, error: err } = await apiFetch<PublishExportResponse>(
|
|
505
|
+
"/api/publish/export",
|
|
506
|
+
{
|
|
507
|
+
body: JSON.stringify({ target: name }),
|
|
508
|
+
method: "POST",
|
|
509
|
+
}
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
setExportingCollectionName(null);
|
|
513
|
+
|
|
514
|
+
if (err) {
|
|
515
|
+
setExportError(err);
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (data) {
|
|
520
|
+
downloadPublishArtifactFile(data);
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
|
|
473
524
|
// Loading state
|
|
474
525
|
if (loading) {
|
|
475
526
|
return (
|
|
@@ -590,6 +641,14 @@ export default function Collections({ navigate }: PageProps) {
|
|
|
590
641
|
</Card>
|
|
591
642
|
)}
|
|
592
643
|
|
|
644
|
+
{exportError && (
|
|
645
|
+
<Card className="mx-auto mb-6 max-w-3xl border-destructive bg-destructive/10">
|
|
646
|
+
<CardContent className="py-4">
|
|
647
|
+
<p className="text-destructive text-sm">{exportError}</p>
|
|
648
|
+
</CardContent>
|
|
649
|
+
</Card>
|
|
650
|
+
)}
|
|
651
|
+
|
|
593
652
|
{/* Error */}
|
|
594
653
|
{error && (
|
|
595
654
|
<Card className="mx-auto mb-6 max-w-md border-destructive bg-destructive/10">
|
|
@@ -626,6 +685,7 @@ export default function Collections({ navigate }: PageProps) {
|
|
|
626
685
|
<CollectionCard
|
|
627
686
|
actionsDisabled={Boolean(syncJobId)}
|
|
628
687
|
collection={collection}
|
|
688
|
+
isExporting={exportingCollectionName === collection.name}
|
|
629
689
|
isReindexing={
|
|
630
690
|
Boolean(syncJobId) &&
|
|
631
691
|
syncTarget?.kind === "collection" &&
|
|
@@ -641,6 +701,9 @@ export default function Collections({ navigate }: PageProps) {
|
|
|
641
701
|
setEmbeddingCleanupDialog(collection);
|
|
642
702
|
setEmbeddingCleanupNote(null);
|
|
643
703
|
}}
|
|
704
|
+
onExport={() => {
|
|
705
|
+
void handleExport(collection.name);
|
|
706
|
+
}}
|
|
644
707
|
onModelSettings={() => setModelDialogCollection(collection)}
|
|
645
708
|
onReindex={() => void handleReindex(collection.name)}
|
|
646
709
|
onRemove={() => setRemoveDialog(collection)}
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
LinkIcon,
|
|
14
14
|
Loader2Icon,
|
|
15
15
|
PencilIcon,
|
|
16
|
+
Share2Icon,
|
|
16
17
|
SquareArrowOutUpRightIcon,
|
|
17
18
|
TextIcon,
|
|
18
19
|
TrashIcon,
|
|
@@ -63,6 +64,10 @@ import {
|
|
|
63
64
|
parseDocumentDeepLink,
|
|
64
65
|
} from "../lib/deep-links";
|
|
65
66
|
import { waitForDocumentAvailability } from "../lib/document-availability";
|
|
67
|
+
import {
|
|
68
|
+
downloadPublishArtifactFile,
|
|
69
|
+
type PublishExportResponse,
|
|
70
|
+
} from "../lib/publish-export";
|
|
66
71
|
import { subscribeWorkspaceActionRequest } from "../lib/workspace-events";
|
|
67
72
|
|
|
68
73
|
interface PageProps {
|
|
@@ -287,6 +292,11 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
287
292
|
const [externalChangeNotice, setExternalChangeNotice] = useState<
|
|
288
293
|
string | null
|
|
289
294
|
>(null);
|
|
295
|
+
const [exportingPublishArtifact, setExportingPublishArtifact] =
|
|
296
|
+
useState(false);
|
|
297
|
+
const [publishExportError, setPublishExportError] = useState<string | null>(
|
|
298
|
+
null
|
|
299
|
+
);
|
|
290
300
|
|
|
291
301
|
// Tag editing state
|
|
292
302
|
const [editingTags, setEditingTags] = useState(false);
|
|
@@ -518,6 +528,32 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
518
528
|
}
|
|
519
529
|
}, [doc, navigate]);
|
|
520
530
|
|
|
531
|
+
const handlePublishExport = useCallback(async () => {
|
|
532
|
+
if (!doc) {
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
setPublishExportError(null);
|
|
537
|
+
setExportingPublishArtifact(true);
|
|
538
|
+
const { data, error: err } = await apiFetch<PublishExportResponse>(
|
|
539
|
+
"/api/publish/export",
|
|
540
|
+
{
|
|
541
|
+
body: JSON.stringify({ target: doc.uri }),
|
|
542
|
+
method: "POST",
|
|
543
|
+
}
|
|
544
|
+
);
|
|
545
|
+
setExportingPublishArtifact(false);
|
|
546
|
+
|
|
547
|
+
if (err) {
|
|
548
|
+
setPublishExportError(err);
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (data) {
|
|
553
|
+
downloadPublishArtifactFile(data);
|
|
554
|
+
}
|
|
555
|
+
}, [doc]);
|
|
556
|
+
|
|
521
557
|
const handleDelete = async () => {
|
|
522
558
|
if (!doc) return;
|
|
523
559
|
|
|
@@ -1309,6 +1345,22 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
1309
1345
|
<PencilIcon className="size-4" />
|
|
1310
1346
|
Edit
|
|
1311
1347
|
</Button>
|
|
1348
|
+
<Button
|
|
1349
|
+
className="gap-1.5"
|
|
1350
|
+
disabled={exportingPublishArtifact}
|
|
1351
|
+
onClick={() => {
|
|
1352
|
+
void handlePublishExport();
|
|
1353
|
+
}}
|
|
1354
|
+
size="sm"
|
|
1355
|
+
variant="outline"
|
|
1356
|
+
>
|
|
1357
|
+
{exportingPublishArtifact ? (
|
|
1358
|
+
<Loader2Icon className="size-4 animate-spin" />
|
|
1359
|
+
) : (
|
|
1360
|
+
<Share2Icon className="size-4" />
|
|
1361
|
+
)}
|
|
1362
|
+
Export for gno.sh
|
|
1363
|
+
</Button>
|
|
1312
1364
|
<Button
|
|
1313
1365
|
className="gap-1.5"
|
|
1314
1366
|
onClick={handleStartRename}
|
|
@@ -1356,6 +1408,22 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
1356
1408
|
Create editable copy
|
|
1357
1409
|
</Button>
|
|
1358
1410
|
)}
|
|
1411
|
+
<Button
|
|
1412
|
+
className="gap-1.5"
|
|
1413
|
+
disabled={exportingPublishArtifact}
|
|
1414
|
+
onClick={() => {
|
|
1415
|
+
void handlePublishExport();
|
|
1416
|
+
}}
|
|
1417
|
+
size="sm"
|
|
1418
|
+
variant="outline"
|
|
1419
|
+
>
|
|
1420
|
+
{exportingPublishArtifact ? (
|
|
1421
|
+
<Loader2Icon className="size-4 animate-spin" />
|
|
1422
|
+
) : (
|
|
1423
|
+
<Share2Icon className="size-4" />
|
|
1424
|
+
)}
|
|
1425
|
+
Export for gno.sh
|
|
1426
|
+
</Button>
|
|
1359
1427
|
{doc.source.absPath && (
|
|
1360
1428
|
<>
|
|
1361
1429
|
<Button
|
|
@@ -1408,6 +1476,9 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
1408
1476
|
</>
|
|
1409
1477
|
)}
|
|
1410
1478
|
</div>
|
|
1479
|
+
{publishExportError && (
|
|
1480
|
+
<p className="pt-2 text-destructive text-sm">{publishExportError}</p>
|
|
1481
|
+
)}
|
|
1411
1482
|
</header>
|
|
1412
1483
|
|
|
1413
1484
|
<div className="mx-auto flex max-w-[1800px] gap-5 px-6 xl:px-8">
|