Skip to content

Commit a1185e1

Browse files
authored
Merge pull request #28 from ssdeanx/develop
feat: Implement governed RAG workflows and indexing
2 parents bd16286 + 8486bdc commit a1185e1

16 files changed

Lines changed: 2597 additions & 30 deletions

app/chat/components/chat-messages.tsx

Lines changed: 192 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable no-console */
12
"use client"
23

34
import {
@@ -32,7 +33,7 @@ import { AgentSources } from "./agent-sources"
3233
import { AgentArtifact, type ArtifactData } from "./agent-artifact"
3334
import { AgentPlan, extractPlanFromText } from "./agent-plan"
3435
import { AgentCheckpoint } from "./agent-checkpoint"
35-
import { AgentTask, type AgentTaskData } from "./agent-task"
36+
import { AgentTask, type AgentTaskData, type TaskStep } from "./agent-task"
3637
import { AgentQueue } from "./agent-queue"
3738
import { AgentConfirmation } from "./agent-confirmation"
3839
import { parseInlineCitations } from "./agent-inline-citation"
@@ -54,6 +55,56 @@ import { mapDataToolPartToDynamicToolPart } from "../helpers/tool-part-transform
5455
import type { BundledLanguage } from "shiki"
5556
import { Button } from "@/ui/button"
5657

58+
// Extract extractTasksFromText to module level to fix scope issues
59+
function extractTasksFromText(content: string): AgentTaskData[] {
60+
const taskSections: AgentTaskData[] = []
61+
const sectionRegex = /(?:tasks?|checklist|todo)[:\s]*\n((?:[-\d[\]xX\s].+\n?)+)/gi
62+
let match: RegExpExecArray | null
63+
let sectionIndex = 0
64+
65+
while ((match = sectionRegex.exec(content)) !== null) {
66+
const sectionBody = match[1]
67+
const lines = sectionBody
68+
.split("\n")
69+
.map((line) => line.trim())
70+
.filter((line) => line.length > 0)
71+
72+
if (lines.length === 0) {continue}
73+
74+
const steps: TaskStep[] = lines.map((line, idx) => {
75+
const statusMatch = /\[([ xX-])\]/.exec(line)
76+
let status: TaskStep["status"] = "pending"
77+
if (statusMatch) {
78+
const symbol = statusMatch[1].toLowerCase()
79+
if (symbol === "x") {
80+
status = "completed"
81+
} else if (symbol === "-") {
82+
status = "running"
83+
} else {
84+
status = "pending"
85+
}
86+
}
87+
88+
const sanitized = line.replace(/\[[ xX-]\]\s*/, "").replace(/^[-\d.]+\s*/, "")
89+
90+
return {
91+
id: `task-${sectionIndex}-${idx}`,
92+
text: sanitized,
93+
status,
94+
}
95+
})
96+
97+
taskSections.push({
98+
title: `Task Group ${taskSections.length + 1}`,
99+
steps,
100+
})
101+
102+
sectionIndex += 1
103+
}
104+
105+
return taskSections
106+
}
107+
57108
function CopyButton({ text }: { text: string }) {
58109
const [copied, setCopied] = useState(false)
59110

@@ -148,7 +199,13 @@ function MessageItem({
148199
const isAssistant = message.role === "assistant"
149200
const isUser = message.role === "user"
150201
const textPart = message.parts?.find(isTextUIPart)
151-
const rawContent = textPart?.text || ""
202+
const rawContent = textPart?.text ?? ""
203+
const [inlinePreview, setInlinePreview] = useState<WebPreviewData | null>(null)
204+
const [sandboxPreview, setSandboxPreview] = useState<{
205+
code: string
206+
language: string
207+
title: string
208+
} | null>(null)
152209

153210
const { content, artifacts, codeBlocks } = useMemo(() => {
154211
if (isAssistant && showArtifacts) {
@@ -185,11 +242,15 @@ function MessageItem({
185242
const otherFileParts = fileParts?.filter((f) => !f.mediaType?.startsWith("image/"))
186243

187244
const reasoningSteps = useMemo(() => {
188-
if (showChainOfThought && messageReasoning?.text) {
245+
if (messageReasoning?.text) {
189246
return parseReasoningToSteps(messageReasoning.text)
190247
}
191248
return []
192-
}, [showChainOfThought, messageReasoning])
249+
}, [messageReasoning])
250+
251+
const hasChainOfThoughtSteps = showChainOfThought && reasoningSteps.length > 0
252+
const shouldShowReasoningFallback =
253+
showReasoning && (!showChainOfThought || !hasChainOfThoughtSteps) && !!messageReasoning?.text
193254

194255
const plan = useMemo(() => {
195256
if (isAssistant) {
@@ -206,6 +267,8 @@ function MessageItem({
206267
return null
207268
}, [hasCitations, content, sources])
208269

270+
const extractedTasks = useMemo(() => extractTasksFromText(rawContent), [rawContent])
271+
209272
// Find checkpoint for this message
210273
const checkpointIndex = checkpointMessageIndices.indexOf(messageIndex)
211274
const isCheckpoint = checkpointIndex !== -1
@@ -252,11 +315,67 @@ function MessageItem({
252315
</MessageAttachments>
253316
)}
254317

318+
{/* Non-image files with inline preview controls */}
319+
{otherFileParts && otherFileParts.length > 0 && (
320+
<div className="my-2 space-y-2 rounded-lg border bg-muted/30 p-3">
321+
<p className="text-xs font-medium text-muted-foreground">Attachments</p>
322+
{otherFileParts.map((file, idx) => (
323+
<div key={`other-file-${idx}`} className="flex items-center justify-between text-sm">
324+
<span className="truncate">{file.filename ?? file.mediaType ?? `File ${idx + 1}`}</span>
325+
<div className="flex items-center gap-2">
326+
{file.url && (
327+
<Button
328+
variant="outline"
329+
size="sm"
330+
className="h-6 px-2 text-xs"
331+
onClick={() =>
332+
setInlinePreview({
333+
id: (file as { id?: string }).id ?? `file-${idx}`,
334+
url: file.url,
335+
title: file.filename ?? "Preview",
336+
})
337+
}
338+
>
339+
Preview
340+
</Button>
341+
)}
342+
<Button
343+
variant="ghost"
344+
size="sm"
345+
className="h-6 px-2 text-xs"
346+
onClick={() => {
347+
if (file.url) {
348+
window.open(file.url, "_blank")
349+
}
350+
}}
351+
>
352+
Download
353+
</Button>
354+
</div>
355+
</div>
356+
))}
357+
</div>
358+
)}
359+
360+
{inlinePreview && (
361+
<div className="my-3">
362+
<AgentWebPreview
363+
preview={inlinePreview}
364+
onClose={() => setInlinePreview(null)}
365+
defaultTab="preview"
366+
height={360}
367+
editable={false}
368+
/>
369+
</div>
370+
)}
371+
255372
{/* Chain of Thought / Reasoning - mutually exclusive display */}
256-
{isAssistant && messageReasoning && (showChainOfThought || showReasoning) && (
257-
showChainOfThought && reasoningSteps.length > 0
258-
? <AgentChainOfThought steps={reasoningSteps} isStreaming={false} />
259-
: <AgentReasoning reasoning={messageReasoning.text || ""} isStreaming={false} />
373+
{isAssistant && messageReasoning && (hasChainOfThoughtSteps || shouldShowReasoningFallback) && (
374+
hasChainOfThoughtSteps ? (
375+
<AgentChainOfThought steps={reasoningSteps} isStreaming={false} />
376+
) : (
377+
<AgentReasoning reasoning={messageReasoning.text || ""} isStreaming={false} />
378+
)
260379
)}
261380

262381
{/* Plan */}
@@ -289,11 +408,37 @@ function MessageItem({
289408
) : codeBlocks.length > 0 ? (
290409
<div className="prose prose-sm max-w-none dark:prose-invert">
291410
{renderContentWithCodeBlocks(content)}
411+
<Button
412+
variant="ghost"
413+
size="sm"
414+
className="mt-2 gap-1 text-sm"
415+
onClick={() =>
416+
setSandboxPreview({
417+
code: codeBlocks[0].code,
418+
language: codeBlocks[0].language,
419+
title: messageReasoning?.text ? "Reasoning Snippet" : "Code Snippet",
420+
})
421+
}
422+
>
423+
Open first snippet in sandbox
424+
</Button>
292425
</div>
293426
) : (
294427
<MessageResponse>{content}</MessageResponse>
295428
)}
296429

430+
{sandboxPreview && (
431+
<div className="my-3">
432+
<AgentCodeSandbox
433+
code={sandboxPreview.code}
434+
language={sandboxPreview.language}
435+
title={sandboxPreview.title}
436+
onClose={() => setSandboxPreview(null)}
437+
onCodeChange={(code) => setSandboxPreview((prev) => (prev ? { ...prev, code } : prev))}
438+
/>
439+
</div>
440+
)}
441+
297442
{/* Artifacts */}
298443
{isAssistant && showArtifacts && artifacts.length > 0 && (
299444
<div className="mt-3 space-y-3">
@@ -303,6 +448,15 @@ function MessageItem({
303448
</div>
304449
)}
305450

451+
{/* Parsed Tasks */}
452+
{isAssistant && extractedTasks && extractedTasks.length > 0 && (
453+
<div className="mt-4 space-y-3">
454+
{extractedTasks.map((task, idx: number) => (
455+
<AgentTask key={`task-${idx}`} task={task} defaultOpen={false} />
456+
))}
457+
</div>
458+
)}
459+
306460
{/* Tool Confirmations */}
307461
{isAssistant && showConfirmation && messageTools && messageTools.length > 0 && (
308462
<>
@@ -358,35 +512,36 @@ function MessageItem({
358512
)
359513
}
360514

361-
function WebPreviewPanel() {
362-
const { webPreview, setWebPreview, agentConfig } = useChatContext()
515+
function WebPreviewPanel({ preview }: { preview: WebPreviewData | null }) {
516+
const { setWebPreview, agentConfig } = useChatContext()
363517

364-
if (!webPreview || !agentConfig?.features.webPreview) {return null}
518+
if (!preview || !agentConfig?.features.webPreview) {return null}
365519

366520
const handleCodeChange = useCallback((newCode: string) => {
367-
if (webPreview) {
521+
if (preview) {
368522
setWebPreview({
369-
...webPreview,
523+
...preview,
370524
code: newCode,
371525
})
372526
}
373-
}, [webPreview, setWebPreview])
527+
}, [preview, setWebPreview])
374528

375529
const handleClose = useCallback(() => {
376530
setWebPreview(null)
377531
}, [setWebPreview])
378532

379533
// If we have code, use the enhanced preview with live editing
380-
if (webPreview.code) {
534+
if (preview.code) {
381535
return (
382536
<div className="mx-auto mb-4 max-w-4xl">
383537
<AgentWebPreview
384538
preview={{
385-
id: webPreview.id,
386-
url: webPreview.url,
387-
title: webPreview.title,
388-
code: webPreview.code,
389-
language: webPreview.language,
539+
id: preview.id,
540+
url: preview.url,
541+
title: preview.title,
542+
code: preview.code,
543+
language: preview.language,
544+
// html: preview.html
390545
}}
391546
onClose={handleClose}
392547
onCodeChange={handleCodeChange}
@@ -404,13 +559,15 @@ function WebPreviewPanel() {
404559
<div className="mx-auto mb-4 max-w-4xl">
405560
<AgentWebPreview
406561
preview={{
407-
id: webPreview.id,
408-
url: webPreview.url,
409-
title: webPreview.title,
562+
id: preview.id,
563+
url: preview.url,
564+
title: preview.title,
410565
}}
411566
onClose={handleClose}
567+
// onCodeChange={handleCodeChange}
412568
defaultTab="preview"
413-
height={400}
569+
showConsole={false}
570+
height={450}
414571
editable={false}
415572
/>
416573
</div>
@@ -460,11 +617,17 @@ export function ChatMessages() {
460617
)
461618

462619
const streamingReasoningSteps = useMemo(() => {
463-
if (showChainOfThought && streamingReasoning) {
620+
if (streamingReasoning) {
464621
return parseReasoningToSteps(streamingReasoning)
465622
}
466623
return []
467-
}, [showChainOfThought, streamingReasoning])
624+
}, [streamingReasoning])
625+
626+
const hasStreamingChainOfThought = showChainOfThought && streamingReasoningSteps.length > 0
627+
const shouldShowStreamingReasoningFallback =
628+
showReasoning &&
629+
(!showChainOfThought || !hasStreamingChainOfThought) &&
630+
!!streamingReasoning
468631

469632
// Get checkpoint data for message items
470633
const checkpointIds = useMemo(() => checkpoints.map((cp) => cp.id), [checkpoints])
@@ -495,7 +658,7 @@ export function ChatMessages() {
495658
)}
496659

497660
{/* Web Preview Panel */}
498-
<WebPreviewPanel />
661+
<WebPreviewPanel preview={webPreview} />
499662

500663
{messages.map((message, index) => (
501664
<MessageItem
@@ -521,11 +684,11 @@ export function ChatMessages() {
521684
{isLoading && (
522685
<Message from="assistant">
523686
<MessageContent>
524-
{showChainOfThought && streamingReasoningSteps.length > 0 && (
687+
{hasStreamingChainOfThought && (
525688
<AgentChainOfThought steps={streamingReasoningSteps} isStreaming={true} />
526689
)}
527690

528-
{showReasoning && !showChainOfThought && streamingReasoning && (
691+
{shouldShowStreamingReasoningFallback && (
529692
<AgentReasoning reasoning={streamingReasoning} isStreaming={true} />
530693
)}
531694

0 commit comments

Comments
 (0)