@ryanfw/prompt-orchestration-pipeline 0.15.1 → 0.16.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ryanfw/prompt-orchestration-pipeline",
3
- "version": "0.15.1",
3
+ "version": "0.16.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",
@@ -45,6 +45,7 @@
45
45
  "@radix-ui/react-tooltip": "^1.2.8",
46
46
  "@radix-ui/themes": "^3.2.1",
47
47
  "ajv": "^8.17.1",
48
+ "better-sqlite3": "^11.7.0",
48
49
  "chokidar": "^3.5.3",
49
50
  "commander": "^14.0.2",
50
51
  "dotenv": "^17.2.3",
@@ -52,9 +53,11 @@
52
53
  "fflate": "^0.8.2",
53
54
  "lucide-react": "^0.544.0",
54
55
  "openai": "^5.23.1",
56
+ "p-limit": "^6.1.0",
55
57
  "react": "^19.2.0",
56
58
  "react-dom": "^19.2.0",
57
59
  "react-markdown": "^10.1.0",
60
+ "react-mentions": "^4.4.10",
58
61
  "react-router-dom": "^7.9.4",
59
62
  "react-syntax-highlighter": "^15.6.1",
60
63
  "rehype-highlight": "^7.0.2",
@@ -18,6 +18,7 @@ export default function Layout({
18
18
  pageTitle,
19
19
  breadcrumbs,
20
20
  actions,
21
+ subheader,
21
22
  backTo = "/",
22
23
  maxWidth = "max-w-7xl",
23
24
  }) {
@@ -227,6 +228,9 @@ export default function Layout({
227
228
  </Box>
228
229
  )}
229
230
 
231
+ {/* Subheader slot */}
232
+ {subheader}
233
+
230
234
  {/* Main content */}
231
235
  <main
232
236
  id="main-content"
@@ -1,4 +1,5 @@
1
1
  import { useState, useEffect, useRef } from "react";
2
+ import { MentionsInput, Mention } from "react-mentions";
2
3
  import { Button } from "./ui/button.jsx";
3
4
  import { Sidebar, SidebarFooter } from "./ui/sidebar.jsx";
4
5
  import { MarkdownRenderer } from "./MarkdownRenderer.jsx";
@@ -6,6 +7,59 @@ import { MarkdownRenderer } from "./MarkdownRenderer.jsx";
6
7
  const TASK_PROPOSAL_REGEX =
7
8
  /\[TASK_PROPOSAL\]\r?\nFILENAME:\s*(\S+)\r?\nTASKNAME:\s*(\S+)\r?\nCODE:\s*```javascript\s*([\s\S]*?)\s*```\s*\[\/TASK_PROPOSAL\]/;
8
9
 
10
+ const MENTION_REGEX = /@\[([^\]]+)\]\([^)]+\)/g;
11
+
12
+ function renderWithMentions(content) {
13
+ const parts = [];
14
+ let lastIndex = 0;
15
+ let match;
16
+ const regex = new RegExp(MENTION_REGEX.source, "g");
17
+
18
+ while ((match = regex.exec(content)) !== null) {
19
+ if (match.index > lastIndex) {
20
+ parts.push(content.slice(lastIndex, match.index));
21
+ }
22
+ parts.push(
23
+ <span
24
+ key={match.index}
25
+ className="bg-primary/20 text-primary rounded-md px-1"
26
+ >
27
+ @{match[1]}
28
+ </span>
29
+ );
30
+ lastIndex = regex.lastIndex;
31
+ }
32
+
33
+ if (lastIndex < content.length) {
34
+ parts.push(content.slice(lastIndex));
35
+ }
36
+
37
+ return parts.length > 0 ? parts : content;
38
+ }
39
+
40
+ const mentionsInputStyle = {
41
+ control: {
42
+ backgroundColor: "var(--background)",
43
+ border: "1px solid var(--border)",
44
+ },
45
+ highlighter: {
46
+ overflow: "hidden",
47
+ },
48
+ input: {
49
+ padding: "0.5rem 0.75rem",
50
+ },
51
+ suggestions: {
52
+ marginTop: "1.5rem",
53
+ list: {
54
+ backgroundColor: "var(--card)",
55
+ border: "1px solid var(--border)",
56
+ },
57
+ item: {
58
+ padding: "0.5rem",
59
+ },
60
+ },
61
+ };
62
+
9
63
  function parseTaskProposal(content) {
10
64
  const match = content.match(TASK_PROPOSAL_REGEX);
11
65
  if (!match) return null;
@@ -77,8 +131,31 @@ export default function TaskCreationSidebar({ isOpen, onClose, pipelineSlug }) {
77
131
  const [error, setError] = useState(null);
78
132
  const [taskProposals, setTaskProposals] = useState({});
79
133
  const [creatingTask, setCreatingTask] = useState({});
134
+ const [artifacts, setArtifacts] = useState([]);
135
+ const [activeTab, setActiveTab] = useState("conversation");
80
136
  const messagesEndRef = useRef(null);
81
137
 
138
+ // Fetch artifacts on mount
139
+ useEffect(() => {
140
+ if (!pipelineSlug) return;
141
+
142
+ const fetchArtifacts = async () => {
143
+ try {
144
+ const response = await fetch(
145
+ `/api/pipelines/${pipelineSlug}/artifacts`
146
+ );
147
+ if (response.ok) {
148
+ const data = await response.json();
149
+ setArtifacts(data.artifacts || []);
150
+ }
151
+ } catch (err) {
152
+ console.error("[TaskCreationSidebar] Failed to fetch artifacts:", err);
153
+ }
154
+ };
155
+
156
+ fetchArtifacts();
157
+ }, [pipelineSlug]);
158
+
82
159
  // Auto-scroll to bottom when messages change
83
160
  useEffect(() => {
84
161
  messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
@@ -125,6 +202,11 @@ export default function TaskCreationSidebar({ isOpen, onClose, pipelineSlug }) {
125
202
  onClose();
126
203
  };
127
204
 
205
+ const insertMention = (fileName) => {
206
+ setInput((prev) => prev + "@" + fileName + " ");
207
+ setActiveTab("conversation");
208
+ };
209
+
128
210
  const handleSend = (e) => {
129
211
  e.preventDefault();
130
212
 
@@ -333,55 +415,112 @@ export default function TaskCreationSidebar({ isOpen, onClose, pipelineSlug }) {
333
415
  description="Describe the task you want to create"
334
416
  contentClassName="flex flex-col max-h-screen"
335
417
  >
336
- {/* Messages area */}
418
+ {/* Tab Header */}
419
+ <div className="flex border-b border-border">
420
+ <button
421
+ type="button"
422
+ onClick={() => setActiveTab("conversation")}
423
+ className={`flex-1 px-4 py-2 text-sm font-medium ${
424
+ activeTab === "conversation"
425
+ ? "bg-primary text-primary-foreground"
426
+ : "bg-muted text-muted-foreground hover:text-foreground"
427
+ }`}
428
+ >
429
+ Conversation
430
+ </button>
431
+ <button
432
+ type="button"
433
+ onClick={() => setActiveTab("files")}
434
+ className={`flex-1 px-4 py-2 text-sm font-medium ${
435
+ activeTab === "files"
436
+ ? "bg-primary text-primary-foreground"
437
+ : "bg-muted text-muted-foreground hover:text-foreground"
438
+ }`}
439
+ >
440
+ Files ({artifacts.length})
441
+ </button>
442
+ </div>
443
+
444
+ {/* Content area */}
337
445
  <div className="flex-1 overflow-y-auto p-6 space-y-4">
338
- {messages.map((msg, i) => (
339
- <div key={i}>
340
- <div
341
- className={`flex ${msg.role === "user" ? "justify-end" : "justify-start"}`}
342
- >
343
- <div
344
- className={`rounded-lg p-3 max-w-full ${
345
- msg.role === "user"
346
- ? "bg-primary text-primary-foreground"
347
- : "bg-muted text-muted-foreground"
348
- }`}
446
+ {activeTab === "files" ? (
447
+ /* File List */
448
+ artifacts.length === 0 ? (
449
+ <p className="text-muted-foreground text-sm">
450
+ No artifact files available.
451
+ </p>
452
+ ) : (
453
+ artifacts.map((artifact, i) => (
454
+ <button
455
+ key={i}
456
+ type="button"
457
+ onClick={() => insertMention(artifact.fileName)}
458
+ className="w-full text-left p-3 rounded-lg border border-border hover:bg-muted transition-colors"
349
459
  >
350
- {msg.role === "assistant" ? (
351
- <>
352
- <MarkdownRenderer
353
- content={msg.content.replace(TASK_PROPOSAL_REGEX, "")}
354
- />
355
- {isWaiting && !msg.content && (
356
- <div className="flex items-center gap-1 text-muted-foreground mt-2">
357
- {Array.from({ length: 5 }).map((_, idx) => (
358
- <span
359
- key={idx}
360
- className="animate-bounce-wave"
361
- style={{ animationDelay: `${idx * 0.1}s` }}
362
- >
363
-
364
- </span>
365
- ))}
366
- </div>
460
+ <p className="font-medium text-foreground">
461
+ {artifact.fileName}
462
+ </p>
463
+ {artifact.sources && artifact.sources.length > 0 && (
464
+ <p className="text-xs text-muted-foreground mt-1">
465
+ {artifact.sources.map((s) => s.taskName).join(", ")}
466
+ </p>
467
+ )}
468
+ </button>
469
+ ))
470
+ )
471
+ ) : (
472
+ /* Conversation Messages */
473
+ <>
474
+ {messages.map((msg, i) => (
475
+ <div key={i}>
476
+ <div
477
+ className={`flex ${msg.role === "user" ? "justify-end" : "justify-start"}`}
478
+ >
479
+ <div
480
+ className={`rounded-lg p-3 max-w-full ${
481
+ msg.role === "user"
482
+ ? "bg-primary text-primary-foreground"
483
+ : "bg-muted text-muted-foreground"
484
+ }`}
485
+ >
486
+ {msg.role === "assistant" ? (
487
+ <>
488
+ <MarkdownRenderer
489
+ content={msg.content.replace(TASK_PROPOSAL_REGEX, "")}
490
+ />
491
+ {isWaiting && !msg.content && (
492
+ <div className="flex items-center gap-1 text-muted-foreground mt-2">
493
+ {Array.from({ length: 5 }).map((_, idx) => (
494
+ <span
495
+ key={idx}
496
+ className="animate-bounce-wave"
497
+ style={{ animationDelay: `${idx * 0.1}s` }}
498
+ >
499
+
500
+ </span>
501
+ ))}
502
+ </div>
503
+ )}
504
+ </>
505
+ ) : (
506
+ <p className="whitespace-pre-wrap">
507
+ {renderWithMentions(msg.content)}
508
+ </p>
367
509
  )}
368
- </>
369
- ) : (
370
- <p className="whitespace-pre-wrap">{msg.content}</p>
510
+ </div>
511
+ </div>
512
+ {taskProposals[i] && (
513
+ <TaskProposalCard
514
+ proposal={taskProposals[i]}
515
+ isCreating={creatingTask[i]}
516
+ onCreate={() => handleCreateTask(i, taskProposals[i])}
517
+ />
371
518
  )}
372
519
  </div>
373
- </div>
374
- {taskProposals[i] && (
375
- <TaskProposalCard
376
- proposal={taskProposals[i]}
377
- isCreating={creatingTask[i]}
378
- onCreate={() => handleCreateTask(i, taskProposals[i])}
379
- />
380
- )}
381
- </div>
382
- ))}
383
-
384
- <div ref={messagesEndRef} />
520
+ ))}
521
+ <div ref={messagesEndRef} />
522
+ </>
523
+ )}
385
524
 
386
525
  {error && (
387
526
  <div className="mt-4 p-4 bg-destructive/10 border border-destructive/20 rounded-lg">
@@ -418,15 +557,27 @@ export default function TaskCreationSidebar({ isOpen, onClose, pipelineSlug }) {
418
557
  {/* Input area */}
419
558
  <SidebarFooter className="bg-card">
420
559
  <form onSubmit={handleSend} className="w-full">
421
- <textarea
560
+ <MentionsInput
422
561
  value={input}
423
- onChange={(e) => setInput(e.target.value)}
562
+ onChange={(e, newValue) => setInput(newValue)}
424
563
  disabled={isSending || isWaiting || isReceiving}
425
564
  placeholder="Describe the task you want to create..."
426
565
  rows={3}
427
566
  className="w-full border rounded-md px-3 py-2 resize-none disabled:bg-muted disabled:cursor-not-allowed mb-3 focus:outline-none focus:ring-2 focus:ring-ring bg-background"
428
567
  aria-label="Task description input"
429
- />
568
+ style={mentionsInputStyle}
569
+ >
570
+ <Mention
571
+ trigger="@"
572
+ markup="@[__display__](__id__)"
573
+ data={artifacts.map((a) => ({
574
+ id: a.fileName,
575
+ display: a.fileName,
576
+ }))}
577
+ displayTransform={(id, display) => `@${display}`}
578
+ className="bg-primary/20 text-primary rounded-md px-1"
579
+ />
580
+ </MentionsInput>
430
581
  <div className="flex justify-end">
431
582
  <Button
432
583
  variant="solid"
@@ -0,0 +1,110 @@
1
+ import React, { useState } from "react";
2
+ import { Box, Code } from "@radix-ui/themes";
3
+ import { Button } from "./button.jsx";
4
+ import { Copy, Check } from "lucide-react";
5
+
6
+ /**
7
+ * CopyableCode component - displays code with a copy button
8
+ * Follows Tufte principles: minimal chrome, high data-ink ratio
9
+ */
10
+ export function CopyableCode({
11
+ children,
12
+ className = "",
13
+ block = false,
14
+ size = "2",
15
+ }) {
16
+ const [copied, setCopied] = useState(false);
17
+
18
+ const handleCopy = async () => {
19
+ const text = typeof children === "string" ? children : String(children);
20
+ await navigator.clipboard.writeText(text);
21
+ setCopied(true);
22
+ setTimeout(() => setCopied(false), 2000);
23
+ };
24
+
25
+ if (block) {
26
+ return (
27
+ <Box className={`relative group ${className}`}>
28
+ <pre className="text-sm bg-gray-50 p-4 rounded-lg overflow-auto border border-gray-200 font-mono">
29
+ {children}
30
+ </pre>
31
+ <Button
32
+ size="sm"
33
+ variant="ghost"
34
+ onClick={handleCopy}
35
+ className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity h-7 w-7 p-0"
36
+ aria-label={copied ? "Copied" : "Copy code"}
37
+ >
38
+ {copied ? (
39
+ <Check className="h-3.5 w-3.5 text-green-600" />
40
+ ) : (
41
+ <Copy className="h-3.5 w-3.5" />
42
+ )}
43
+ </Button>
44
+ </Box>
45
+ );
46
+ }
47
+
48
+ return (
49
+ <span className={`inline-flex items-center gap-1 group ${className}`}>
50
+ <Code size={size}>{children}</Code>
51
+ <button
52
+ onClick={handleCopy}
53
+ className="opacity-0 group-hover:opacity-100 transition-opacity p-0.5 hover:bg-gray-100 rounded"
54
+ aria-label={copied ? "Copied" : "Copy code"}
55
+ >
56
+ {copied ? (
57
+ <Check className="h-3 w-3 text-green-600" />
58
+ ) : (
59
+ <Copy className="h-3 w-3 text-gray-500" />
60
+ )}
61
+ </button>
62
+ </span>
63
+ );
64
+ }
65
+
66
+ /**
67
+ * CopyableCodeBlock - larger block display with syntax-like formatting
68
+ */
69
+ export function CopyableCodeBlock({ children, className = "", maxHeight }) {
70
+ const [copied, setCopied] = useState(false);
71
+
72
+ const handleCopy = async () => {
73
+ const text = typeof children === "string" ? children : String(children);
74
+ await navigator.clipboard.writeText(text);
75
+ setCopied(true);
76
+ setTimeout(() => setCopied(false), 2000);
77
+ };
78
+
79
+ return (
80
+ <Box className={`relative group ${className}`}>
81
+ <pre
82
+ className="text-sm bg-gray-50 p-4 rounded-lg overflow-auto border border-gray-200 font-mono leading-relaxed"
83
+ style={maxHeight ? { maxHeight } : undefined}
84
+ >
85
+ {children}
86
+ </pre>
87
+ <Button
88
+ size="sm"
89
+ variant="ghost"
90
+ onClick={handleCopy}
91
+ className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity h-8 px-2 bg-white/80 hover:bg-white border border-gray-200"
92
+ aria-label={copied ? "Copied" : "Copy code"}
93
+ >
94
+ {copied ? (
95
+ <>
96
+ <Check className="h-3.5 w-3.5 text-green-600 mr-1" />
97
+ <span className="text-xs text-green-600">Copied</span>
98
+ </>
99
+ ) : (
100
+ <>
101
+ <Copy className="h-3.5 w-3.5 mr-1" />
102
+ <span className="text-xs">Copy</span>
103
+ </>
104
+ )}
105
+ </Button>
106
+ </Box>
107
+ );
108
+ }
109
+
110
+ export default CopyableCode;