@markbrutx/promptbook-viewer 0.1.0 → 0.3.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.
Files changed (42) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/server/api.d.ts +4 -5
  4. package/dist/server/api.d.ts.map +1 -1
  5. package/dist/server/api.js +49 -12
  6. package/dist/server/api.js.map +1 -1
  7. package/dist/server/book-source.d.ts +24 -0
  8. package/dist/server/book-source.d.ts.map +1 -1
  9. package/dist/server/book-source.js +39 -0
  10. package/dist/server/book-source.js.map +1 -1
  11. package/dist/server/server.d.ts +1 -1
  12. package/dist/server/server.d.ts.map +1 -1
  13. package/dist/server/server.js +21 -8
  14. package/dist/server/server.js.map +1 -1
  15. package/dist/server/workspace.d.ts +13 -0
  16. package/dist/server/workspace.d.ts.map +1 -0
  17. package/dist/server/workspace.js +55 -0
  18. package/dist/server/workspace.js.map +1 -0
  19. package/dist/shared/types.d.ts +9 -0
  20. package/dist/shared/types.d.ts.map +1 -1
  21. package/dist/web/assets/index-BCBuW76o.css +1 -0
  22. package/dist/web/assets/index-BwIAKPNq.js +51 -0
  23. package/dist/web/favicon.png +0 -0
  24. package/dist/web/index.html +3 -2
  25. package/dist/web/promptbook-logo.png +0 -0
  26. package/package.json +19 -2
  27. package/src/index.ts +2 -0
  28. package/src/server/api.ts +57 -18
  29. package/src/server/book-source.ts +67 -0
  30. package/src/server/server.ts +22 -9
  31. package/src/server/workspace.ts +64 -0
  32. package/src/shared/types.ts +11 -0
  33. package/src/web/App.tsx +91 -28
  34. package/src/web/api.ts +26 -10
  35. package/src/web/components/Sidebar.tsx +37 -0
  36. package/src/web/index.html +1 -0
  37. package/src/web/public/favicon.png +0 -0
  38. package/src/web/public/promptbook-logo.png +0 -0
  39. package/src/web/styles.css +35 -0
  40. package/src/web/types.ts +2 -0
  41. package/dist/web/assets/index-B2Wxtb-f.css +0 -1
  42. package/dist/web/assets/index-C8f_6lr_.js +0 -51
package/src/web/api.ts CHANGED
@@ -3,6 +3,7 @@ import type {
3
3
  Annotation,
4
4
  AnnotationsResponse,
5
5
  BookResponse,
6
+ BooksResponse,
6
7
  Context,
7
8
  LintResponse,
8
9
  ResolveResponse,
@@ -30,15 +31,30 @@ async function sendJson<T>(method: "POST" | "DELETE", path: string, body?: unkno
30
31
  return (await res.json()) as T;
31
32
  }
32
33
 
34
+ /** Append `?book=<name>` so a request targets the active book in the workspace. */
35
+ function scoped(path: string, book: string | null): string {
36
+ if (book === null || book === "") {
37
+ return path;
38
+ }
39
+ const sep = path.includes("?") ? "&" : "?";
40
+ return `${path}${sep}book=${encodeURIComponent(book)}`;
41
+ }
42
+
33
43
  export const api = {
34
- book: () => getJson<BookResponse>("/api/book"),
35
- resolve: (prompt: string, context: Context) =>
36
- sendJson<ResolveResponse>("POST", "/api/resolve", { prompt, context }),
37
- lint: (prompt: string, context: Context) =>
38
- sendJson<LintResponse>("POST", "/api/lint", { prompt, context }),
39
- usedIn: (fragmentId: string) => getJson<UsedInResponse>(`/api/used-in/${encodeURIComponent(fragmentId)}`),
40
- annotations: () => getJson<AnnotationsResponse>("/api/annotations"),
41
- annotate: (body: AnnotateRequest) => sendJson<Annotation>("POST", "/api/annotate", body),
42
- resolveAnnotation: (id: string) =>
43
- sendJson<{ id: string; removed: boolean }>("DELETE", `/api/annotations/${encodeURIComponent(id)}`),
44
+ books: () => getJson<BooksResponse>("/api/books"),
45
+ book: (book: string | null) => getJson<BookResponse>(scoped("/api/book", book)),
46
+ resolve: (book: string | null, prompt: string, context: Context) =>
47
+ sendJson<ResolveResponse>("POST", scoped("/api/resolve", book), { prompt, context }),
48
+ lint: (book: string | null, prompt: string, context: Context) =>
49
+ sendJson<LintResponse>("POST", scoped("/api/lint", book), { prompt, context }),
50
+ usedIn: (book: string | null, fragmentId: string) =>
51
+ getJson<UsedInResponse>(scoped(`/api/used-in/${encodeURIComponent(fragmentId)}`, book)),
52
+ annotations: (book: string | null) => getJson<AnnotationsResponse>(scoped("/api/annotations", book)),
53
+ annotate: (book: string | null, body: AnnotateRequest) =>
54
+ sendJson<Annotation>("POST", scoped("/api/annotate", book), body),
55
+ resolveAnnotation: (book: string | null, id: string) =>
56
+ sendJson<{ id: string; removed: boolean }>(
57
+ "DELETE",
58
+ scoped(`/api/annotations/${encodeURIComponent(id)}`, book),
59
+ ),
44
60
  };
@@ -2,8 +2,12 @@ import type { ReactNode } from "react";
2
2
  import { fragmentAccent } from "../colors.js";
3
3
  import type { Selection } from "../selection.js";
4
4
  import type { CompositionTreeNode, FragmentGroup, GroupNode } from "../tree.js";
5
+ import type { WorkspaceBook } from "../types.js";
5
6
 
6
7
  interface SidebarProps {
8
+ books: WorkspaceBook[];
9
+ activeBook: string | null;
10
+ onSelectBook: (name: string) => void;
7
11
  tree: CompositionTreeNode[];
8
12
  fragmentGroups: FragmentGroup[];
9
13
  selection: Selection | null;
@@ -12,6 +16,35 @@ interface SidebarProps {
12
16
  onSelectFragment: (id: string) => void;
13
17
  }
14
18
 
19
+ /** Brand header + (when the workspace holds more than one book) a book switcher. */
20
+ function BookSwitcher({
21
+ books,
22
+ activeBook,
23
+ onSelectBook,
24
+ }: {
25
+ books: WorkspaceBook[];
26
+ activeBook: string | null;
27
+ onSelectBook: (name: string) => void;
28
+ }) {
29
+ return (
30
+ <header className="sidebar-brand">
31
+ <img className="sidebar-logo" src="/promptbook-logo.png" alt="promptbook" />
32
+ {books.length > 1 ? (
33
+ <label className="book-switcher">
34
+ <span className="book-switcher-label">Book</span>
35
+ <select value={activeBook ?? ""} onChange={(event) => onSelectBook(event.target.value)}>
36
+ {books.map((b) => (
37
+ <option key={b.name} value={b.name}>
38
+ {b.name}
39
+ </option>
40
+ ))}
41
+ </select>
42
+ </label>
43
+ ) : null}
44
+ </header>
45
+ );
46
+ }
47
+
15
48
  function isVariantSelected(selection: Selection | null, composition: string, variant: string): boolean {
16
49
  return (
17
50
  selection?.kind === "variant" && selection.composition === composition && selection.variant === variant
@@ -129,6 +162,9 @@ function GroupItem({
129
162
  }
130
163
 
131
164
  export function Sidebar({
165
+ books,
166
+ activeBook,
167
+ onSelectBook,
132
168
  tree,
133
169
  fragmentGroups,
134
170
  selection,
@@ -139,6 +175,7 @@ export function Sidebar({
139
175
  const handlers: NodeHandlers = { onSelectVariant, onSelectCode };
140
176
  return (
141
177
  <nav className="sidebar">
178
+ <BookSwitcher books={books} activeBook={activeBook} onSelectBook={onSelectBook} />
142
179
  <section>
143
180
  <h2 className="sidebar-title">Compositions</h2>
144
181
  {tree.length === 0 ? (
@@ -3,6 +3,7 @@
3
3
  <head>
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <link rel="icon" type="image/png" href="/favicon.png" />
6
7
  <title>promptbook viewer</title>
7
8
  </head>
8
9
  <body>
Binary file
@@ -34,6 +34,36 @@ body {
34
34
  background: var(--panel);
35
35
  }
36
36
 
37
+ .sidebar-brand {
38
+ display: flex;
39
+ flex-direction: column;
40
+ gap: 10px;
41
+ padding: 4px 4px 12px;
42
+ border-bottom: 1px solid var(--border);
43
+ margin-bottom: 4px;
44
+ }
45
+
46
+ .sidebar-logo {
47
+ width: 168px;
48
+ max-width: 80%;
49
+ height: auto;
50
+ display: block;
51
+ margin: 8px auto 4px;
52
+ }
53
+
54
+ .book-switcher {
55
+ display: flex;
56
+ align-items: center;
57
+ gap: 6px;
58
+ }
59
+
60
+ .book-switcher-label {
61
+ font-size: 11px;
62
+ text-transform: uppercase;
63
+ letter-spacing: 0.08em;
64
+ color: var(--muted);
65
+ }
66
+
37
67
  .sidebar-title {
38
68
  font-size: 11px;
39
69
  text-transform: uppercase;
@@ -201,6 +231,11 @@ input {
201
231
  background: #fff;
202
232
  }
203
233
 
234
+ .book-switcher select {
235
+ flex: 1;
236
+ width: auto;
237
+ }
238
+
204
239
  /* Controls */
205
240
  .control-row {
206
241
  display: grid;
package/src/web/types.ts CHANGED
@@ -6,6 +6,7 @@ export type {
6
6
  Annotation,
7
7
  AnnotationsResponse,
8
8
  BookResponse,
9
+ BooksResponse,
9
10
  CodePromptSummary,
10
11
  CompositionSummary,
11
12
  FragmentSummary,
@@ -14,4 +15,5 @@ export type {
14
15
  Segment,
15
16
  UsedInResponse,
16
17
  VariantSummary,
18
+ WorkspaceBook,
17
19
  } from "../shared/types.js";
@@ -1 +0,0 @@
1
- :root{--bg: #fff;--panel: #f6f8fa;--border: #e1e4e8;--text: #1f2328;--muted: #6e7781;--accent: #4f46e5;--warn: #b35900;font-family:system-ui,-apple-system,Segoe UI,sans-serif;font-size:14px;color:var(--text)}*{box-sizing:border-box}body{margin:0;background:var(--bg)}.layout{display:grid;grid-template-columns:260px minmax(0,1fr) 380px;height:100vh}.sidebar{border-right:1px solid var(--border);overflow-y:auto;padding:12px;background:var(--panel)}.sidebar-title{font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);margin:16px 0 6px}.tree{list-style:none;margin:0;padding-left:12px}.tree-folder,.tree-composition{cursor:pointer;font-weight:600;padding:2px 0}.tree-item,.link{display:inline-flex;align-items:center;gap:6px;width:100%;text-align:left;background:none;border:none;padding:3px 6px;border-radius:5px;color:var(--text);cursor:pointer;font:inherit}.tree-item:hover{background:#eaeef2}.tree-item.active{background:var(--accent);color:#fff}.link{width:auto;color:var(--accent);padding:0;text-decoration:underline}.swatch{width:10px;height:10px;border-radius:2px;flex:none}.canvas{overflow-y:auto;padding:20px 24px}.canvas-head{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;margin-bottom:12px}.canvas-title{font-size:18px;margin:0;display:flex;align-items:center;gap:8px}.legend{list-style:none;display:flex;flex-wrap:wrap;gap:10px;padding:0;margin:0 0 14px;font-size:12px;color:var(--muted)}.legend li{display:inline-flex;align-items:center;gap:5px}.prompt{display:flex;flex-direction:column;gap:14px}.segment{position:relative;margin:0;padding:16px 12px 12px;border-radius:6px;border:1px solid var(--border);white-space:pre-wrap;word-break:break-word;font-family:ui-monospace,SF Mono,Menlo,monospace;font-size:13px;line-height:1.5}.segment-tag{position:absolute;top:0;right:0;font-size:10px;padding:1px 6px;background:#0000000f;border-bottom-left-radius:6px;font-family:system-ui,sans-serif;color:var(--muted)}.rail{border-left:1px solid var(--border);overflow-y:auto;padding:16px;background:var(--panel)}.rail section,.addons section{margin-bottom:18px}.rail h3,.addons h3{font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);margin:0 0 8px}.rail h4{margin:12px 0 4px;font-size:12px}select,input{width:100%;padding:4px 6px;border:1px solid var(--border);border-radius:5px;font:inherit;background:#fff}.control-row{display:grid;grid-template-columns:90px 1fr;align-items:center;gap:8px;margin-bottom:6px}.control-key{font-family:ui-monospace,monospace;font-size:12px;color:var(--muted)}.control-add{display:grid;grid-template-columns:1fr auto;gap:6px;margin-top:6px}.control-add button{border:1px solid var(--border);border-radius:5px;background:#fff;cursor:pointer;padding:0 10px}.rules,.findings,.warnings,.used-list{list-style:none;margin:0;padding:0}.rules li{padding:4px 0;border-bottom:1px solid var(--border);font-size:12px}.rules li.skipped{color:var(--muted)}.rule-mark{margin-right:6px}.rule-action{font-weight:600;margin-right:6px}.rule-effect{font-family:ui-monospace,monospace;margin-top:2px}.rule-reason{color:var(--muted);margin-top:2px}.order{font-family:ui-monospace,monospace;font-size:12px;word-break:break-word}.finding{display:flex;flex-wrap:wrap;gap:6px;align-items:baseline;padding:5px 0;border-bottom:1px solid var(--border);font-size:12px}.finding-sev{text-transform:uppercase;font-size:10px;font-weight:700;padding:1px 5px;border-radius:4px;background:#eaeef2}.finding.error .finding-sev{background:#ffd7d5;color:#a40e26}.finding.warning .finding-sev{background:#fff1c2;color:var(--warn)}.finding-rule{font-family:ui-monospace,monospace;color:var(--muted)}.diff-body{margin:8px 0 0;font-family:ui-monospace,monospace;font-size:12px;border:1px solid var(--border);border-radius:6px;overflow-x:auto;background:#fff}.diff-row{padding:0 8px;white-space:pre-wrap}.diff-row.add{background:#e6ffec}.diff-row.remove{background:#ffebe9}.diff-mark{display:inline-block;width:12px;color:var(--muted)}.annot-mark{background:#fff3b0;border-bottom:2px solid var(--warn);border-radius:2px;padding:0 1px}.annot-popover{position:fixed;z-index:20;width:280px;padding:10px;background:#fff;border:1px solid var(--border);border-radius:8px;box-shadow:0 8px 24px #00000029}.annot-popover textarea{width:100%;min-height:64px;resize:vertical;margin:6px 0}.annot-actions{display:flex;justify-content:flex-end;gap:10px;align-items:center}.annot-actions button[type=button]:last-child{border:1px solid var(--border);border-radius:5px;background:var(--accent);color:#fff;padding:4px 12px;cursor:pointer}.annot-actions button[disabled]{opacity:.5;cursor:default}.annot-quote{margin:0;font-style:italic;color:var(--muted);font-size:12px;word-break:break-word}.annot-list{list-style:none;margin:0;padding:0}.annot-list li{padding:8px 0;border-bottom:1px solid var(--border)}.annot-comment{margin:4px 0;font-size:13px;word-break:break-word}.annot-meta{display:flex;justify-content:space-between;align-items:center;gap:8px;font-size:12px}.badge{font-size:12px;padding:2px 8px;border-radius:999px;background:#eaeef2;white-space:nowrap}.badge-code{font-size:10px;text-transform:uppercase;letter-spacing:.06em;background:#2b2b40;color:#fff}.code-tabs{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:12px}.code-tab{font:inherit;font-size:12px;padding:3px 10px;border-radius:999px;border:1px solid var(--border);background:var(--panel);color:var(--text);cursor:pointer}.code-tab.active{background:var(--accent);border-color:var(--accent)}.code-diff{margin-top:18px}.tokens{font-size:20px;font-weight:600;margin:0}.muted{color:var(--muted)}.warn{color:var(--warn)}.used-list li{padding:3px 0}