@ryanfw/prompt-orchestration-pipeline 0.4.0 → 0.6.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 (36) hide show
  1. package/package.json +1 -1
  2. package/src/components/JobCard.jsx +1 -1
  3. package/src/components/JobDetail.jsx +45 -12
  4. package/src/components/JobTable.jsx +40 -1
  5. package/src/components/Layout.jsx +146 -22
  6. package/src/components/PageSubheader.jsx +75 -0
  7. package/src/components/UploadSeed.jsx +0 -70
  8. package/src/components/ui/Logo.jsx +16 -0
  9. package/src/core/config.js +145 -13
  10. package/src/core/file-io.js +12 -27
  11. package/src/core/orchestrator.js +92 -78
  12. package/src/core/pipeline-runner.js +13 -6
  13. package/src/core/status-writer.js +63 -52
  14. package/src/core/task-runner.js +61 -1
  15. package/src/llm/index.js +97 -40
  16. package/src/pages/Code.jsx +297 -0
  17. package/src/pages/PipelineDetail.jsx +47 -8
  18. package/src/pages/PromptPipelineDashboard.jsx +6 -53
  19. package/src/providers/deepseek.js +17 -1
  20. package/src/providers/openai.js +1 -1
  21. package/src/ui/client/adapters/job-adapter.js +26 -2
  22. package/src/ui/client/hooks/useJobDetailWithUpdates.js +0 -1
  23. package/src/ui/client/index.css +6 -0
  24. package/src/ui/client/index.html +1 -1
  25. package/src/ui/client/main.jsx +2 -0
  26. package/src/ui/dist/assets/{index-CxcrauYR.js → index-WgJUlSmE.js} +716 -307
  27. package/src/ui/dist/assets/style-x0V-5m8e.css +62 -0
  28. package/src/ui/dist/index.html +3 -3
  29. package/src/ui/job-reader.js +0 -108
  30. package/src/ui/server.js +54 -0
  31. package/src/ui/sse-enhancer.js +0 -1
  32. package/src/ui/transformers/list-transformer.js +32 -12
  33. package/src/ui/transformers/status-transformer.js +11 -11
  34. package/src/utils/token-cost-calculator.js +297 -0
  35. package/src/utils/ui.jsx +4 -4
  36. package/src/ui/dist/assets/style-D6K_oQ12.css +0 -62
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ryanfw/prompt-orchestration-pipeline",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "A Prompt-orchestration pipeline (POP) is a framework for building, running, and experimenting with complex chains of LLM tasks.",
5
5
  "type": "module",
6
6
  "main": "src/ui/server.js",
@@ -21,7 +21,7 @@ export default function JobCard({
21
21
  : 0;
22
22
  const totalCompleted = countCompleted(job);
23
23
  const hasValidId = Boolean(job.id);
24
- const jobTitle = job.title || job.name; // Fallback for backward compatibility
24
+ const jobTitle = job.title;
25
25
 
26
26
  return (
27
27
  <Card
@@ -6,6 +6,25 @@ import DAGGrid from "./DAGGrid.jsx";
6
6
  import { computeDagItems, computeActiveIndex } from "../utils/dag.js";
7
7
  import { getTaskFilesForTask } from "../utils/task-files.js";
8
8
 
9
+ // Local helpers for formatting costs and tokens
10
+ function formatCurrency4(x) {
11
+ if (typeof x !== "number" || x === 0) return "$0.0000";
12
+ const formatted = x.toFixed(4);
13
+ // Trim trailing zeros and unnecessary decimal point
14
+ return `$${formatted.replace(/\.?0+$/, "")}`;
15
+ }
16
+
17
+ function formatTokensCompact(n) {
18
+ if (typeof n !== "number" || n === 0) return "0 tok";
19
+
20
+ if (n >= 1000000) {
21
+ return `${(n / 1000000).toFixed(1).replace(/\.0$/, "")}M tokens`;
22
+ } else if (n >= 1000) {
23
+ return `${(n / 1000).toFixed(1).replace(/\.0$/, "")}k tokens`;
24
+ }
25
+ return `${n} tokens`;
26
+ }
27
+
9
28
  export default function JobDetail({ job, pipeline, onClose, onResume }) {
10
29
  const now = useTicker(1000);
11
30
  const [resumeFrom, setResumeFrom] = useState(
@@ -16,16 +35,6 @@ export default function JobDetail({ job, pipeline, onClose, onResume }) {
16
35
  : ""
17
36
  );
18
37
 
19
- useEffect(() => {
20
- setResumeFrom(
21
- pipeline?.tasks?.[0]
22
- ? typeof pipeline.tasks[0] === "string"
23
- ? pipeline.tasks[0]
24
- : (pipeline.tasks[0].id ?? pipeline.tasks[0].name ?? "")
25
- : ""
26
- );
27
- }, [job.id, pipeline?.tasks?.length]);
28
-
29
38
  // job.tasks is expected to be an object keyed by task name; normalize from array if needed
30
39
  const taskById = React.useMemo(() => {
31
40
  const tasks = job?.tasks;
@@ -49,6 +58,16 @@ export default function JobDetail({ job, pipeline, onClose, onResume }) {
49
58
  return result;
50
59
  }, [job?.tasks]);
51
60
 
61
+ useEffect(() => {
62
+ setResumeFrom(
63
+ pipeline?.tasks?.[0]
64
+ ? typeof pipeline.tasks[0] === "string"
65
+ ? pipeline.tasks[0]
66
+ : (pipeline.tasks[0].id ?? pipeline.tasks[0].name ?? "")
67
+ : ""
68
+ );
69
+ }, [job.id, pipeline?.tasks?.length]);
70
+
52
71
  // Compute pipeline tasks from pipeline or derive from job tasks
53
72
  const computedPipeline = React.useMemo(() => {
54
73
  let result;
@@ -112,9 +131,21 @@ export default function JobDetail({ job, pipeline, onClose, onResume }) {
112
131
  }
113
132
  }
114
133
 
115
- // Include error message in body when task status is error
134
+ // Prefer taskBreakdown totals for consistency with backend
135
+ const taskBreakdown = job?.costs?.taskBreakdown?.[item.id]?.summary || {};
136
+ if (taskBreakdown.totalTokens > 0) {
137
+ subtitleParts.push(formatTokensCompact(taskBreakdown.totalTokens));
138
+ }
139
+ if (taskBreakdown.totalCost > 0) {
140
+ subtitleParts.push(formatCurrency4(taskBreakdown.totalCost));
141
+ }
142
+
143
+ // Include error message in body when task status is error or failed
116
144
  const errorMsg = task?.error?.message;
117
- const body = item.status === "failed" && errorMsg ? errorMsg : null;
145
+ const body =
146
+ (item.status === "failed" || item.status === "error") && errorMsg
147
+ ? errorMsg
148
+ : null;
118
149
 
119
150
  const resultItem = {
120
151
  ...item,
@@ -146,6 +177,8 @@ export default function JobDetail({ job, pipeline, onClose, onResume }) {
146
177
  [job]
147
178
  );
148
179
 
180
+ console.log("dagItems", dagItems);
181
+
149
182
  return (
150
183
  <div className="flex h-full flex-col">
151
184
  <DAGGrid
@@ -7,6 +7,25 @@ import { taskDisplayDurationMs } from "../utils/duration.js";
7
7
  import { countCompleted } from "../utils/jobs";
8
8
  import { progressClasses, statusBadge } from "../utils/ui";
9
9
 
10
+ // Local helpers for formatting costs and tokens
11
+ function formatCurrency4(x) {
12
+ if (typeof x !== "number" || x === 0) return "$0.0000";
13
+ const formatted = x.toFixed(4);
14
+ // Trim trailing zeros and unnecessary decimal point
15
+ return `$${formatted.replace(/\.?0+$/, "")}`;
16
+ }
17
+
18
+ function formatTokensCompact(n) {
19
+ if (typeof n !== "number" || n === 0) return "0 tok";
20
+
21
+ if (n >= 1000000) {
22
+ return `${(n / 1000000).toFixed(1).replace(/\.0$/, "")}M tok`;
23
+ } else if (n >= 1000) {
24
+ return `${(n / 1000).toFixed(1).replace(/\.0$/, "")}k tok`;
25
+ }
26
+ return `${n} tok`;
27
+ }
28
+
10
29
  export default function JobTable({
11
30
  jobs,
12
31
  pipeline,
@@ -35,6 +54,7 @@ export default function JobTable({
35
54
  <Table.ColumnHeaderCell>Current Task</Table.ColumnHeaderCell>
36
55
  <Table.ColumnHeaderCell>Progress</Table.ColumnHeaderCell>
37
56
  <Table.ColumnHeaderCell>Tasks</Table.ColumnHeaderCell>
57
+ <Table.ColumnHeaderCell>Cost</Table.ColumnHeaderCell>
38
58
  <Table.ColumnHeaderCell>Duration</Table.ColumnHeaderCell>
39
59
  <Table.ColumnHeaderCell className="w-12"></Table.ColumnHeaderCell>
40
60
  </Table.Row>
@@ -42,7 +62,7 @@ export default function JobTable({
42
62
 
43
63
  <Table.Body>
44
64
  {jobs.map((job) => {
45
- const jobTitle = job.title || job.name; // Fallback for backward compatibility
65
+ const jobTitle = job.name;
46
66
  const taskById = Array.isArray(job.tasks)
47
67
  ? Object.fromEntries(
48
68
  (job.tasks || []).map((t) => {
@@ -73,6 +93,12 @@ export default function JobTable({
73
93
  (currentTask?.config || pipeline?.taskConfig?.[job.current])) ||
74
94
  {};
75
95
 
96
+ // Cost and token data
97
+ const costsSummary = job.costsSummary || {};
98
+ const totalCost = job.totalCost || costsSummary.totalCost || 0;
99
+ const totalTokens =
100
+ job.totalTokens || costsSummary.totalTokens || 0;
101
+
76
102
  const hasValidId = Boolean(job.id);
77
103
  return (
78
104
  <Table.Row
@@ -173,6 +199,19 @@ export default function JobTable({
173
199
  </Text>
174
200
  </Table.Cell>
175
201
 
202
+ <Table.Cell>
203
+ <Flex direction="column" gap="1">
204
+ <Text size="2" className="text-slate-700">
205
+ {totalCost > 0 ? formatCurrency4(totalCost) : "—"}
206
+ </Text>
207
+ {totalTokens > 0 && (
208
+ <Text size="1" className="text-slate-500">
209
+ {formatTokensCompact(totalTokens)}
210
+ </Text>
211
+ )}
212
+ </Flex>
213
+ </Table.Cell>
214
+
176
215
  <Table.Cell>
177
216
  <Flex align="center" gap="1">
178
217
  <TimerReset className="h-3 w-3 text-slate-500" />
@@ -1,9 +1,12 @@
1
- import React from "react";
2
- import { useNavigate, useLocation } from "react-router-dom";
1
+ import React, { useState, useRef, useEffect } from "react";
2
+ import { useNavigate, useLocation, Link } from "react-router-dom";
3
3
  import * as Tooltip from "@radix-ui/react-tooltip";
4
4
  import { Box, Flex, Text, Heading, Link as RadixLink } from "@radix-ui/themes";
5
5
  import { Button } from "./ui/button.jsx";
6
- import { ArrowLeft, Home } from "lucide-react";
6
+ import Logo from "./ui/Logo.jsx";
7
+ import PageSubheader from "./PageSubheader.jsx";
8
+ import UploadSeed from "./UploadSeed.jsx";
9
+ import { ArrowLeft, Code2, Upload } from "lucide-react";
7
10
  import "./ui/focus-styles.css";
8
11
 
9
12
  /**
@@ -13,6 +16,8 @@ import "./ui/focus-styles.css";
13
16
  export default function Layout({
14
17
  children,
15
18
  title,
19
+ pageTitle,
20
+ breadcrumbs,
16
21
  actions,
17
22
  showBackButton = false,
18
23
  backTo = "/",
@@ -20,6 +25,10 @@ export default function Layout({
20
25
  }) {
21
26
  const navigate = useNavigate();
22
27
  const location = useLocation();
28
+ const [isUploadOpen, setIsUploadOpen] = useState(false);
29
+ const [seedUploadSuccess, setSeedUploadSuccess] = useState(null);
30
+ const [seedUploadTimer, setSeedUploadTimer] = useState(null);
31
+ const uploadPanelRef = useRef(null);
23
32
 
24
33
  // Determine active navigation based on current path
25
34
  const isActivePath = (path) => {
@@ -32,6 +41,50 @@ export default function Layout({
32
41
  navigate(backTo);
33
42
  };
34
43
 
44
+ const toggleUploadPanel = () => {
45
+ setIsUploadOpen(!isUploadOpen);
46
+ };
47
+
48
+ // Handle seed upload success
49
+ const handleSeedUploadSuccess = ({ jobName }) => {
50
+ // Clear any existing timer
51
+ if (seedUploadTimer) {
52
+ clearTimeout(seedUploadTimer);
53
+ }
54
+
55
+ // Set success message
56
+ setSeedUploadSuccess(jobName);
57
+
58
+ // Auto-clear after exactly 5000 ms
59
+ const timer = setTimeout(() => {
60
+ setSeedUploadSuccess(null);
61
+ setSeedUploadTimer(null);
62
+ }, 5000);
63
+
64
+ setSeedUploadTimer(timer);
65
+ };
66
+
67
+ // Cleanup timer on unmount
68
+ useEffect(() => {
69
+ return () => {
70
+ if (seedUploadTimer) {
71
+ clearTimeout(seedUploadTimer);
72
+ }
73
+ };
74
+ }, [seedUploadTimer]);
75
+
76
+ // Focus upload panel when opened
77
+ useEffect(() => {
78
+ if (isUploadOpen && uploadPanelRef.current) {
79
+ const uploadArea = uploadPanelRef.current.querySelector(
80
+ '[data-testid="upload-area"]'
81
+ );
82
+ if (uploadArea) {
83
+ uploadArea.focus();
84
+ }
85
+ }
86
+ }, [isUploadOpen]);
87
+
35
88
  return (
36
89
  <Tooltip.Provider delayDuration={200}>
37
90
  <Box className="min-h-screen bg-gray-1">
@@ -56,7 +109,7 @@ export default function Layout({
56
109
  gap="4"
57
110
  >
58
111
  {/* Left side: Navigation and title */}
59
- <Flex align="center" gap="3" className="min-w-0 flex-1">
112
+ <Flex align="center" className="min-w-0 flex-1">
60
113
  {/* Back button (conditional) */}
61
114
  {showBackButton && (
62
115
  <Tooltip.Root delayDuration={200}>
@@ -77,14 +130,39 @@ export default function Layout({
77
130
  </Tooltip.Root>
78
131
  )}
79
132
 
80
- {/* App title */}
81
- <Heading
82
- size="5"
83
- weight="medium"
84
- className="text-gray-12 truncate"
133
+ {/* Logo */}
134
+ <Box
135
+ asChild
136
+ className="shrink-0"
137
+ style={{ width: "80px", height: "60px" }}
85
138
  >
86
- {title || "Prompt Pipeline"}
87
- </Heading>
139
+ <Link
140
+ to="/"
141
+ aria-label="Go to homepage"
142
+ className="rounded focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500"
143
+ >
144
+ <Logo />
145
+ </Link>
146
+ </Box>
147
+
148
+ {/* App title - clickable to navigate to dashboard */}
149
+ <Box
150
+ asChild
151
+ className="shrink-0 cursor-pointer hover:bg-gray-3 rounded p-1 -m-1 transition-colors"
152
+ onClick={() => navigate("/")}
153
+ >
154
+ <Heading
155
+ size="6"
156
+ weight="medium"
157
+ className="text-gray-12 truncate"
158
+ >
159
+ <>
160
+ Prompt
161
+ <br />
162
+ Pipeline
163
+ </>
164
+ </Heading>
165
+ </Box>
88
166
  </Flex>
89
167
 
90
168
  {/* Center: Navigation */}
@@ -95,36 +173,82 @@ export default function Layout({
95
173
  >
96
174
  <Flex align="center" gap="6">
97
175
  <RadixLink
98
- href="/"
176
+ href="/code"
99
177
  className={`text-sm font-medium transition-colors hover:text-blue-600 ${
100
- isActivePath("/")
178
+ isActivePath("/code")
101
179
  ? "text-blue-600"
102
180
  : "text-gray-11 hover:text-gray-12"
103
181
  }`}
104
- aria-current={isActivePath("/") ? "page" : undefined}
182
+ aria-current={isActivePath("/code") ? "page" : undefined}
105
183
  >
106
184
  <Flex align="center" gap="2">
107
- <Home className="h-4 w-4" />
108
- Dashboard
185
+ <Code2 className="h-4 w-4" />
186
+ Help
109
187
  </Flex>
110
188
  </RadixLink>
111
189
  </Flex>
112
190
  </nav>
113
191
 
114
192
  {/* Right side: Actions */}
115
- {actions && (
116
- <Flex align="center" gap="3" className="shrink-0">
117
- {actions}
118
- </Flex>
119
- )}
193
+ <Flex align="center" gap="3" className="shrink-0">
194
+ {actions}
195
+ <Tooltip.Root delayDuration={200}>
196
+ <Tooltip.Trigger asChild>
197
+ <Button
198
+ size="sm"
199
+ variant="default"
200
+ onClick={toggleUploadPanel}
201
+ aria-controls="layout-upload-panel"
202
+ aria-expanded={isUploadOpen}
203
+ >
204
+ <Upload className="h-4 w-4" />
205
+ <Text size="2" className="ml-2">
206
+ Upload Seed
207
+ </Text>
208
+ </Button>
209
+ </Tooltip.Trigger>
210
+ <Tooltip.Content side="bottom" sideOffset={5}>
211
+ <Text size="2">Upload seed file</Text>
212
+ </Tooltip.Content>
213
+ </Tooltip.Root>
214
+ </Flex>
120
215
  </Flex>
121
216
  </Box>
122
217
 
218
+ {/* Upload Panel */}
219
+ {isUploadOpen && (
220
+ <Box
221
+ id="layout-upload-panel"
222
+ ref={uploadPanelRef}
223
+ role="region"
224
+ aria-label="Upload seed file"
225
+ className="bg-blue-50"
226
+ >
227
+ <Flex
228
+ direction="column"
229
+ gap="3"
230
+ className={`mx-auto w-full ${maxWidth} px-4 sm:px-6 lg:px-8 py-4`}
231
+ >
232
+ {/* Success Message */}
233
+ {seedUploadSuccess && (
234
+ <Box className="rounded-md bg-green-50 p-3 border border-green-200">
235
+ <Text size="2" className="text-green-800">
236
+ Job <strong>{seedUploadSuccess}</strong> created
237
+ successfully
238
+ </Text>
239
+ </Box>
240
+ )}
241
+
242
+ <UploadSeed onUploadSuccess={handleSeedUploadSuccess} />
243
+ </Flex>
244
+ </Box>
245
+ )}
246
+
123
247
  {/* Main content */}
124
248
  <main
125
249
  id="main-content"
126
250
  role="main"
127
- className={`mx-auto w-full ${maxWidth} px-4 sm:px-6 lg:px-8 py-6`}
251
+ className={`mx-auto w-full ${maxWidth} px-4 sm:px-6 lg:px-8`}
128
252
  >
129
253
  {children}
130
254
  </main>
@@ -0,0 +1,75 @@
1
+ import React from "react";
2
+ import { Link } from "react-router-dom";
3
+ import { Box, Flex, Text } from "@radix-ui/themes";
4
+ import { ChevronRight } from "lucide-react";
5
+
6
+ /**
7
+ * PageSubheader renders a secondary header with breadcrumbs and optional right-side content.
8
+ * Intended to be placed directly below main navigation header.
9
+ */
10
+ export default function PageSubheader({
11
+ breadcrumbs = [],
12
+ children,
13
+ maxWidth = "max-w-7xl",
14
+ }) {
15
+ return (
16
+ <Box
17
+ role="region"
18
+ aria-label="Page header"
19
+ className="border-b border-gray-300 bg-gray-1/60 backdrop-blur supports-[backdrop-filter]:bg-gray-1/40 mb-4"
20
+ >
21
+ <Flex
22
+ align="center"
23
+ justify="between"
24
+ className={`mx-auto w-full ${maxWidth} px-1.5 py-3`}
25
+ gap="4"
26
+ wrap="wrap"
27
+ >
28
+ <Flex align="center" gap="3" className="min-w-0 flex-1">
29
+ {breadcrumbs.length > 0 && (
30
+ <nav aria-label="Breadcrumb" className="shrink-0">
31
+ <ol className="flex items-center gap-2 text-sm text-gray-11">
32
+ {breadcrumbs.map((crumb, index) => {
33
+ const isLast = index === breadcrumbs.length - 1;
34
+ return (
35
+ <React.Fragment key={index}>
36
+ {index > 0 && (
37
+ <ChevronRight
38
+ className="h-4 w-4 text-gray-9"
39
+ aria-hidden="true"
40
+ />
41
+ )}
42
+ {crumb.href ? (
43
+ <Link
44
+ to={crumb.href}
45
+ className="hover:text-gray-12 transition-colors underline-offset-4 hover:underline"
46
+ >
47
+ {crumb.label}
48
+ </Link>
49
+ ) : (
50
+ <Text
51
+ as="span"
52
+ aria-current={isLast ? "page" : undefined}
53
+ className={isLast ? "text-gray-12 font-medium" : ""}
54
+ >
55
+ {crumb.label}
56
+ </Text>
57
+ )}
58
+ </React.Fragment>
59
+ );
60
+ })}
61
+ </ol>
62
+ </nav>
63
+ )}
64
+ </Flex>
65
+
66
+ {/* Right side content */}
67
+ {children && (
68
+ <Flex align="center" gap="3" className="shrink-0">
69
+ {children}
70
+ </Flex>
71
+ )}
72
+ </Flex>
73
+ </Box>
74
+ );
75
+ }
@@ -24,23 +24,8 @@ export const normalizeUploadError = (err) => {
24
24
  */
25
25
  export default function UploadSeed({ onUploadSuccess }) {
26
26
  const fileInputRef = React.useRef(null);
27
- const [showSample, setShowSample] = useState(false);
28
27
  const [error, setError] = useState(null);
29
28
 
30
- // Sample seed file structure for reference
31
- const sampleSeed = {
32
- name: "some-name",
33
- pipeline: "content-generation",
34
- data: {
35
- type: "some-type",
36
- contentType: "blog-post",
37
- targetAudience: "software-developers",
38
- tone: "professional-yet-accessible",
39
- length: "1500-2000 words",
40
- outputFormat: "blog-post",
41
- },
42
- };
43
-
44
29
  const handleFileChange = async (event) => {
45
30
  const files = event.target.files;
46
31
  if (!files || files.length === 0) return;
@@ -179,61 +164,6 @@ export default function UploadSeed({ onUploadSuccess }) {
179
164
  onChange={handleFileChange}
180
165
  data-testid="file-input"
181
166
  />
182
-
183
- {/* Sample seed file section */}
184
- <div className="border border-gray-200 rounded-lg overflow-hidden">
185
- <button
186
- type="button"
187
- onClick={() => setShowSample(!showSample)}
188
- className="w-full px-4 py-3 text-left bg-gray-50 hover:bg-gray-100 transition-colors flex items-center justify-between"
189
- aria-expanded={showSample}
190
- data-testid="sample-toggle"
191
- >
192
- <span className="text-sm font-medium text-gray-700">
193
- Need help? View sample seed file structure
194
- </span>
195
- <svg
196
- className={`w-4 h-4 text-gray-500 transition-transform ${
197
- showSample ? "rotate-180" : ""
198
- }`}
199
- fill="none"
200
- viewBox="0 0 24 24"
201
- stroke="currentColor"
202
- >
203
- <path
204
- strokeLinecap="round"
205
- strokeLinejoin="round"
206
- strokeWidth={2}
207
- d="M19 9l-7 7-7-7"
208
- />
209
- </svg>
210
- </button>
211
-
212
- {showSample && (
213
- <div className="p-4 bg-white border-t border-gray-200">
214
- <div className="flex items-center justify-between mb-2">
215
- <p className="text-xs text-gray-600">
216
- Use this structure as a reference for your seed file:
217
- </p>
218
- <button
219
- type="button"
220
- onClick={() =>
221
- navigator.clipboard.writeText(
222
- JSON.stringify(sampleSeed, null, 2)
223
- )
224
- }
225
- className="text-xs bg-blue-100 text-blue-700 px-2 py-1 rounded hover:bg-blue-200 transition-colors"
226
- data-testid="copy-sample"
227
- >
228
- Copy
229
- </button>
230
- </div>
231
- <pre className="text-xs bg-gray-50 p-3 rounded overflow-auto max-h-60">
232
- {JSON.stringify(sampleSeed, null, 2)}
233
- </pre>
234
- </div>
235
- )}
236
- </div>
237
167
  </div>
238
168
  );
239
169
  }
@@ -0,0 +1,16 @@
1
+ export const Logo = () => (
2
+ <svg
3
+ xmlns="http://www.w3.org/2000/svg"
4
+ width="100%"
5
+ height="100%"
6
+ viewBox="0 0 1200 1200"
7
+ >
8
+ <path
9
+ fill="#009966"
10
+ d="M406.13 988.31c-17.297 75.047-84.562 131.11-164.86 131.11-93.375 0-169.18-75.797-169.18-169.18s75.797-169.18 169.18-169.18 169.18 75.797 169.18 169.18v1.266h447.74v-167.9H671.63c-14.859 0-29.062-5.906-39.562-16.406s-16.406-24.703-16.406-39.562v-37.312h-317.16c-10.312 0-18.656-8.344-18.656-18.656V355.78h-147.94c-14.859 0-29.062-5.906-39.562-16.406s-16.406-24.75-16.406-39.562v-111.94c0-14.859 5.906-29.109 16.406-39.562 10.5-10.5 24.75-16.406 39.562-16.406h391.78c14.859 0 29.062 5.906 39.562 16.406s16.406 24.75 16.406 39.562v37.312h202.4c9.281-84.609 81.094-150.52 168.14-150.52 93.375 0 169.18 75.797 169.18 169.18s-75.797 169.18-169.18 169.18c-87.047 0-158.86-65.906-168.14-150.52h-202.4v37.312c0 14.859-5.906 29.062-16.406 39.562s-24.75 16.406-39.562 16.406h-206.53v297.24h298.5v-37.312c0-14.859 5.906-29.062 16.406-39.562s24.703-16.406 39.562-16.406h392.63c14.859 0 29.062 5.906 39.562 16.406s16.406 24.703 16.406 39.562v111.94c0 14.859-5.906 29.062-16.406 39.562s-24.75 16.406-39.562 16.406h-168.74v186.56c0 10.312-8.344 18.656-18.656 18.656h-466.4c-1.5 0-2.906-.187-4.312-.516zM225.19 262.45h18.656c10.312 0 18.656-8.344 18.656-18.656s-8.344-18.656-18.656-18.656H225.19c-10.312 0-18.656 8.344-18.656 18.656s8.344 18.656 18.656 18.656zm186.56 0h18.656c10.312 0 18.656-8.344 18.656-18.656s-8.344-18.656-18.656-18.656H411.75c-10.312 0-18.656 8.344-18.656 18.656s8.344 18.656 18.656 18.656zm-93.281 0h18.656c10.312 0 18.656-8.344 18.656-18.656s-8.344-18.656-18.656-18.656h-18.656c-10.312 0-18.656 8.344-18.656 18.656s8.344 18.656 18.656 18.656zm616.18 0h85.5c10.312 0 18.656-8.344 18.656-18.656s-8.344-18.656-18.656-18.656h-85.5l29.062-22.594c8.109-6.328 9.609-18.047 3.281-26.156s-18.047-9.609-26.156-3.281l-71.953 55.969a18.61 18.61 0 0 0 0 29.438l71.953 55.969c8.109 6.328 19.875 4.875 26.156-3.281 6.328-8.109 4.875-19.875-3.281-26.203l-29.062-22.594zm-779.95 696.66l50.391 50.391c7.266 7.313 19.078 7.313 26.391 0l100.73-100.73c7.266-7.266 7.266-19.078 0-26.391-7.266-7.266-19.078-7.266-26.391 0l-87.562 87.562-37.172-37.172c-7.266-7.266-19.078-7.266-26.391 0-7.266 7.266-7.266 19.078 0 26.391zm797.21-268.78h18.656c10.312 0 18.656-8.344 18.656-18.656s-8.344-18.656-18.656-18.656h-18.656c-10.312 0-18.656 8.344-18.656 18.656s8.344 18.656 18.656 18.656zm-186.56 0h18.656c10.312 0 18.656-8.344 18.656-18.656s-8.344-18.656-18.656-18.656h-18.656c-10.312 0-18.656 8.344-18.656 18.656s8.344 18.656 18.656 18.656zm93.281 0h18.656c10.312 0 18.656-8.344 18.656-18.656s-8.344-18.656-18.656-18.656H858.63c-10.312 0-18.656 8.344-18.656 18.656s8.344 18.656 18.656 18.656z"
11
+ fillRule="evenodd"
12
+ />
13
+ </svg>
14
+ );
15
+
16
+ export default Logo;