Skip to content

Commit b1b4f05

Browse files
authored
Merge pull request #25 from ssdeanx/develop
Develop
2 parents 2fc0302 + e0bb4b0 commit b1b4f05

67 files changed

Lines changed: 4712 additions & 1478 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

app/chat/components/agent-artifact.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ const PREVIEWABLE_LANGUAGES = [
5555
"react",
5656
]
5757

58+
const normalizeLanguage = (lang?: string): string => lang?.toLowerCase() ?? ""
59+
5860
export function AgentArtifact({
5961
artifact,
6062
onClose,
@@ -66,7 +68,7 @@ export function AgentArtifact({
6668

6769
const isPreviewable =
6870
artifact.type === "code" &&
69-
PREVIEWABLE_LANGUAGES.includes(artifact.language?.toLowerCase() || "")
71+
PREVIEWABLE_LANGUAGES.includes(normalizeLanguage(artifact.language))
7072

7173
const handleCopy = useCallback(async () => {
7274
try {

app/chat/components/agent-chain-of-thought.tsx

Lines changed: 68 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"use client"
22

3+
import { useMemo } from "react"
34
import {
45
ChainOfThought,
56
ChainOfThoughtHeader,
@@ -8,33 +9,48 @@ import {
89
ChainOfThoughtSearchResults,
910
ChainOfThoughtSearchResult,
1011
} from "@/src/components/ai-elements/chain-of-thought"
11-
import { SearchIcon, BrainIcon, CheckIcon, LoaderIcon } from "lucide-react"
12+
import { Badge } from "@/ui/badge"
13+
import { SearchIcon, BrainIcon, CheckIcon, LoaderIcon, ClockIcon } from "lucide-react"
1214

1315
export interface ReasoningStep {
1416
id: string
1517
label: string
1618
description?: string
1719
status: "complete" | "active" | "pending"
1820
searchResults?: string[]
21+
duration?: number
1922
}
2023

2124
interface AgentChainOfThoughtProps {
2225
steps: ReasoningStep[]
2326
isStreaming?: boolean
2427
defaultOpen?: boolean
28+
className?: string
2529
}
2630

2731
export function AgentChainOfThought({
2832
steps,
2933
isStreaming = false,
3034
defaultOpen = true,
35+
className,
3136
}: AgentChainOfThoughtProps) {
3237
if (!steps || steps.length === 0) {return null}
3338

39+
const completedCount = useMemo(() => steps.filter((s) => s.status === "complete").length, [steps])
40+
const activeStep = useMemo(() => steps.find((s) => s.status === "active"), [steps])
41+
3442
return (
35-
<ChainOfThought defaultOpen={defaultOpen}>
36-
<ChainOfThoughtHeader>
37-
{isStreaming ? "Thinking..." : "Chain of Thought"}
43+
<ChainOfThought defaultOpen={defaultOpen} className={className}>
44+
<ChainOfThoughtHeader className="flex items-center gap-2">
45+
<span className="flex-1">
46+
{isStreaming
47+
? activeStep?.label ?? "Thinking..."
48+
: "Chain of Thought"
49+
}
50+
</span>
51+
<Badge variant="secondary" className="text-xs font-normal">
52+
{completedCount}/{steps.length}
53+
</Badge>
3854
</ChainOfThoughtHeader>
3955
<ChainOfThoughtContent>
4056
{steps.map((step) => (
@@ -51,6 +67,12 @@ export function AgentChainOfThought({
5167
description={step.description}
5268
status={step.status}
5369
>
70+
{step.duration && step.status === "complete" && (
71+
<span className="flex items-center gap-1 text-xs text-muted-foreground">
72+
<ClockIcon className="size-3" />
73+
{step.duration}s
74+
</span>
75+
)}
5476
{step.searchResults && step.searchResults.length > 0 && (
5577
<ChainOfThoughtSearchResults>
5678
{step.searchResults.map((result, i) => (
@@ -68,29 +90,61 @@ export function AgentChainOfThought({
6890
)
6991
}
7092

93+
type StepType = "step" | "search" | "analysis" | "decision"
94+
95+
function categorizeStep(text: string): StepType {
96+
const lower = text.toLowerCase()
97+
if (lower.includes("search") || lower.includes("looking for") || lower.includes("finding")) {
98+
return "search"
99+
}
100+
if (lower.includes("analyzing") || lower.includes("examining") || lower.includes("reviewing")) {
101+
return "analysis"
102+
}
103+
if (lower.includes("decided") || lower.includes("conclusion") || lower.includes("therefore")) {
104+
return "decision"
105+
}
106+
return "step"
107+
}
108+
71109
export function parseReasoningToSteps(reasoning: string): ReasoningStep[] {
72110
if (!reasoning) {return []}
73111

74112
const lines = reasoning.split("\n").filter((line) => line.trim())
75113
const steps: ReasoningStep[] = []
114+
let currentSearchTerms: string[] = []
76115

77116
lines.forEach((line, index) => {
78117
const trimmed = line.trim()
79-
if (trimmed.startsWith("-") || trimmed.startsWith("•") || (/^\d+\./.exec(trimmed))) {
80-
steps.push({
81-
id: `step-${index}`,
82-
label: trimmed.replace(/^[-\d.]+\s*/, ""),
83-
status: "complete",
84-
})
85-
} else if (trimmed.length > 10) {
118+
119+
// Skip very short lines
120+
if (trimmed.length < 5) {return}
121+
122+
// Check for bullet points or numbered lists
123+
const isBullet = trimmed.startsWith("-") || trimmed.startsWith("•") || /^\d+\./.test(trimmed)
124+
const content = isBullet
125+
? trimmed.replace(/^[-\d.]+\s*/, "")
126+
: trimmed
127+
128+
// Extract search terms if mentioned
129+
const searchMatch = /(?:search|looking for|finding)[:\s]+["']?([^"'\n]+)["']?/i.exec(content)
130+
if (searchMatch) {
131+
currentSearchTerms.push(searchMatch[1].trim())
132+
}
133+
134+
if (content.length > 10) {
135+
const stepType = categorizeStep(content)
86136
steps.push({
87137
id: `step-${index}`,
88-
label: trimmed.slice(0, 80) + (trimmed.length > 80 ? "..." : ""),
89-
description: trimmed.length > 80 ? trimmed : undefined,
138+
label: content.length > 80 ? content.slice(0, 77) + "..." : content,
139+
description: content.length > 80 ? content : undefined,
90140
status: "complete",
141+
searchResults: stepType === "search" ? [...currentSearchTerms] : undefined,
91142
})
143+
144+
// Reset after each step to prevent accumulation
145+
currentSearchTerms = []
92146
}
93147
})
94148

95-
return steps.slice(0, 10)
149+
return steps.slice(0, 15)
96150
}

app/chat/components/agent-checkpoint.tsx

Lines changed: 74 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,104 @@
11
"use client"
22

3+
import React from "react"
34
import {
45
Checkpoint,
56
CheckpointIcon,
67
CheckpointTrigger,
78
} from "@/src/components/ai-elements/checkpoint"
8-
import { BookmarkIcon, RotateCcwIcon } from "lucide-react"
9+
import { Badge } from "@/ui/badge"
10+
import {
11+
BookmarkIcon,
12+
RotateCcwIcon,
13+
MessageSquareIcon,
14+
ClockIcon,
15+
} from "lucide-react"
16+
import { cn } from "@/lib/utils"
917

18+
/**
19+
* Props for the AgentCheckpoint component.
20+
* @param messageIndex - The index of the message.
21+
* @param timestamp - The timestamp of the checkpoint. Can be a Date object or a valid ISO string. Assumed to be in local timezone.
22+
* @param label - Optional label for the checkpoint.
23+
* @param messageCount - Number of messages included in this checkpoint. If 0, shows 0; if undefined, not shown.
24+
* @param onRestore - Callback to restore to this checkpoint.
25+
* @param className - Optional CSS class.
26+
*/
1027
interface AgentCheckpointProps {
1128
messageIndex: number
12-
timestamp?: Date
29+
timestamp?: Date | string
1330
label?: string
31+
messageCount?: number
1432
onRestore: () => void
33+
className?: string
34+
}
35+
36+
function formatTime(date: Date): string {
37+
return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })
38+
}
39+
40+
function formatRelativeTime(date: Date): string {
41+
const now = new Date()
42+
const diffMs = now.getTime() - date.getTime()
43+
const diffMins = Math.floor(diffMs / 60000)
44+
45+
if (diffMins < 1) {return "just now"}
46+
if (diffMins < 60) {return `${diffMins}m ago`}
47+
const diffHours = Math.floor(diffMins / 60)
48+
if (diffHours < 24) {return `${diffHours}h ago`}
49+
return formatTime(date)
1550
}
1651

1752
export function AgentCheckpoint({
1853
messageIndex,
1954
timestamp,
2055
label,
56+
messageCount,
2157
onRestore,
58+
className,
2259
}: AgentCheckpointProps) {
23-
const displayLabel = label ?? (timestamp
24-
? `Restore to ${timestamp.toLocaleTimeString()}`
25-
: "Restore checkpoint")
60+
const date = timestamp ? (typeof timestamp === 'string' ? new Date(timestamp) : timestamp) : undefined
61+
const displayLabel = label ?? (date
62+
? `Checkpoint at ${formatTime(date)}`
63+
: `Checkpoint ${messageIndex + 1}`)
2664

2765
return (
28-
<Checkpoint>
66+
<Checkpoint className={cn("group", className)}>
2967
<CheckpointIcon>
30-
<BookmarkIcon className="size-4 shrink-0 text-primary" />
68+
<div className="relative">
69+
<BookmarkIcon className="size-4 shrink-0 text-primary" aria-hidden="true" />
70+
<span className="absolute -top-1 -right-1 size-2 bg-primary rounded-full opacity-0 group-hover:opacity-100 transition-opacity" />
71+
</div>
3172
</CheckpointIcon>
73+
74+
<div className="flex items-center gap-2 flex-1 min-w-0">
75+
<span className="text-xs text-muted-foreground truncate">
76+
{displayLabel}
77+
</span>
78+
79+
{messageCount !== undefined && (
80+
<Badge variant="secondary" className="text-xs gap-1 shrink-0">
81+
<MessageSquareIcon className="size-3" aria-hidden="true" />
82+
{messageCount}
83+
</Badge>
84+
)}
85+
86+
{date && (
87+
<span className="text-xs text-muted-foreground/60 hidden sm:flex items-center gap-1 shrink-0">
88+
<ClockIcon className="size-3" aria-hidden="true" />
89+
{formatRelativeTime(date)}
90+
</span>
91+
)}
92+
</div>
93+
3294
<CheckpointTrigger
3395
onClick={onRestore}
34-
tooltip={displayLabel}
35-
className="gap-1"
96+
tooltip={`Restore to ${displayLabel}`}
97+
aria-label={`Restore to ${displayLabel}`}
98+
className="gap-1.5 opacity-60 hover:opacity-100 transition-opacity"
3699
>
37-
<RotateCcwIcon className="size-3" />
38-
<span className="text-xs">Restore</span>
100+
<RotateCcwIcon className="size-3" aria-hidden="true" />
101+
<span className="text-xs hidden sm:inline">Restore</span>
39102
</CheckpointTrigger>
40103
</Checkpoint>
41104
)

0 commit comments

Comments
 (0)