Skip to content

Commit 621d44e

Browse files
committed
feat: Add AI Elements Integration with Agents
- Created a Product Requirements Document (PRD) for integrating AI Elements with agents, detailing the problem statement, goals, user stories, scope, success metrics, dependencies, and risks. - Developed a comprehensive task list for the integration, including tasks for creating context providers, agent configurations, and various UI components. - Implemented critical fixes for AI SDK v5 compatibility, updating deprecated types and patterns in chat components. - Established a task overview with status, priority, estimates, and dependencies for each task related to the integration. - Defined acceptance criteria for each task to ensure clear deliverables and expectations. - Included implementation notes and code snippets for clarity on how to approach each task. - Outlined progress tracking phases to monitor the completion of foundational, core UI, enhanced features, and integration/testing tasks.
1 parent 1e3352e commit 621d44e

28 files changed

Lines changed: 3136 additions & 72 deletions
File renamed without changes.
File renamed without changes.

.github/instructions/next-js.mdc renamed to .github/instructions/next-js.instructions.md

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
---
2+
name: Next.js Best Practices
23
description: This rule provides comprehensive guidance for Next.js development, covering code organization, performance, security, testing, and common pitfalls. It helps developers build robust, scalable, and maintainable Next.js applications by adhering to community-accepted best practices and coding standards.
3-
applyTo: ['**.js', '**.jsx', '**.ts', '**.tsx', '**/next.config.js', '**/pages/**', '**/app/**']
4+
applyTo: "**.js, **.jsx, **.ts, **.tsx, **/next.config.ts, **/pages/**, **/app/**, **/src/components/**, **/ui/**, **/lib/**, **/types/**, **/hooks/**"
45
---
56
# Next.js Best Practices
67

@@ -19,10 +20,6 @@ This document outlines best practices for developing Next.js applications, focus
1920
* `route.ts`: Defines server-side route handlers (API routes).
2021
* `[dynamic-segment]`: Dynamic route segments, using brackets.
2122
* `@folder-name`: Route Groups to organize routes without affecting URL structure.
22-
* **`pages/`**: (Legacy - Before Next.js 13) Contains page components.
23-
* `api/`: Serverless functions (API routes).
24-
* `_app.js/tsx`: Custom App component (wraps all pages).
25-
* `_document.js/tsx`: Custom Document component (control the entire HTML document).
2623
* **`components/`**: Reusable UI components.
2724
* **`lib/`**: Utility functions, helper functions, and third-party integrations.
2825
* **`hooks/`**: Custom React hooks.
@@ -36,9 +33,9 @@ This document outlines best practices for developing Next.js applications, focus
3633
### File Naming Conventions
3734

3835
* **Components:** `ComponentName.jsx` or `ComponentName.tsx`
39-
* **Pages:** `page.js`, `page.jsx`, `page.ts`, `page.tsx` (within the `app` or `pages` directory)
36+
* **Pages:** `page.js`, `page.jsx`, `page.ts`, `page.tsx` (within the `app`)
4037
* **Layouts:** `layout.js`, `layout.jsx`, `layout.ts`, `layout.tsx` (within the `app` directory)
41-
* **API Routes:** `route.js`, `route.ts` (within the `app/api` directory or `pages/api` directory)
38+
* **API Routes:** `route.js`, `route.ts` (within the `app/api` directory)
4239
* **Hooks:** `useHookName.js` or `useHookName.ts`
4340
* **Styles:** `ComponentName.module.css` or `ComponentName.module.scss`
4441
* **Types:** `types.ts` or `interfaces.ts`

.github/instructions/self-explanatory-code-commenting.instructions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
---
2+
name: Self-explanatory Code Commenting
23
description: 'Guidelines for GitHub Copilot to write comments to achieve self-explanatory code with less comments. Examples are in JavaScript but it should work on any language that has comments.'
34
applyTo: '**'
45
---

.github/instructions/taming-copilot.instructions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
---
2+
name: Taming Copilot
23
applyTo: '**'
34
description: 'Prevent Copilot from wreaking havoc across your codebase, keeping it under control.'
45
---
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
"use client"
2+
3+
import {
4+
Artifact,
5+
ArtifactHeader,
6+
ArtifactTitle,
7+
ArtifactDescription,
8+
ArtifactActions,
9+
ArtifactAction,
10+
ArtifactClose,
11+
ArtifactContent,
12+
} from "@/src/components/ai-elements/artifact"
13+
import { CodeBlock } from "@/src/components/ai-elements/code-block"
14+
import { CopyIcon, DownloadIcon } from "lucide-react"
15+
import { useCallback } from "react"
16+
import { BundledLanguage } from "shiki"
17+
18+
export interface ArtifactData {
19+
id: string
20+
title: string
21+
description?: string
22+
type: "code" | "markdown" | "json" | "text"
23+
language?: string
24+
content: string
25+
}
26+
27+
export interface AgentArtifactProps {
28+
artifact: ArtifactData
29+
onClose?: () => void
30+
}
31+
32+
export function AgentArtifact({ artifact, onClose }: AgentArtifactProps) {
33+
const handleCopy = useCallback(async () => {
34+
try {
35+
await navigator.clipboard.writeText(artifact.content)
36+
} catch (err) {
37+
console.error("Failed to copy:", err)
38+
}
39+
}, [artifact.content])
40+
41+
const handleDownload = useCallback(() => {
42+
const extensions: Record<string, string> = {
43+
code: artifact.language || "txt",
44+
markdown: "md",
45+
json: "json",
46+
text: "txt",
47+
}
48+
const ext = extensions[artifact.type] || "txt"
49+
const filename = `${artifact.title.toLowerCase().replace(/\s+/g, "-")}.${ext}`
50+
51+
const blob = new Blob([artifact.content], { type: "text/plain" })
52+
const url = URL.createObjectURL(blob)
53+
const a = document.createElement("a")
54+
a.href = url
55+
a.download = filename
56+
a.click()
57+
URL.revokeObjectURL(url)
58+
}, [artifact])
59+
60+
const language: BundledLanguage = (artifact.language || (artifact.type === "json" ? "json" : "plaintext")) as BundledLanguage
61+
62+
return (
63+
<Artifact className="my-4">
64+
<ArtifactHeader>
65+
<div className="flex flex-col gap-0.5">
66+
<ArtifactTitle>{artifact.title}</ArtifactTitle>
67+
{artifact.description && (
68+
<ArtifactDescription>{artifact.description}</ArtifactDescription>
69+
)}
70+
</div>
71+
<ArtifactActions>
72+
<ArtifactAction
73+
tooltip="Copy"
74+
icon={CopyIcon}
75+
onClick={handleCopy}
76+
/>
77+
<ArtifactAction
78+
tooltip="Download"
79+
icon={DownloadIcon}
80+
onClick={handleDownload}
81+
/>
82+
{onClose && <ArtifactClose onClick={onClose} />}
83+
</ArtifactActions>
84+
</ArtifactHeader>
85+
<ArtifactContent className="p-0">
86+
<CodeBlock code={artifact.content} language={language} />
87+
</ArtifactContent>
88+
</Artifact>
89+
)
90+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"use client"
2+
3+
import {
4+
Reasoning,
5+
ReasoningTrigger,
6+
ReasoningContent,
7+
} from "@/src/components/ai-elements/reasoning"
8+
9+
export interface AgentReasoningProps {
10+
reasoning: string
11+
isStreaming: boolean
12+
duration?: number
13+
}
14+
15+
export function AgentReasoning({
16+
reasoning,
17+
isStreaming,
18+
duration,
19+
}: AgentReasoningProps) {
20+
if (!reasoning) return null
21+
22+
return (
23+
<Reasoning isStreaming={isStreaming} duration={duration}>
24+
<ReasoningTrigger />
25+
<ReasoningContent>{reasoning}</ReasoningContent>
26+
</Reasoning>
27+
)
28+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"use client"
2+
3+
import {
4+
Sources,
5+
SourcesTrigger,
6+
SourcesContent,
7+
Source,
8+
} from "@/src/components/ai-elements/sources"
9+
import type { Source as SourceType } from "@/app/chat/providers/chat-context"
10+
11+
export interface AgentSourcesProps {
12+
sources: SourceType[]
13+
}
14+
15+
export function AgentSources({ sources }: AgentSourcesProps) {
16+
if (!sources || sources.length === 0) return null
17+
18+
return (
19+
<Sources>
20+
<SourcesTrigger count={sources.length} />
21+
<SourcesContent>
22+
{sources.map((source, index) => (
23+
<Source
24+
key={`${source.url}-${index}`}
25+
href={source.url}
26+
title={source.title}
27+
/>
28+
))}
29+
</SourcesContent>
30+
</Sources>
31+
)
32+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"use client"
2+
3+
import {
4+
Tool,
5+
ToolHeader,
6+
ToolContent,
7+
ToolInput,
8+
ToolOutput,
9+
} from "@/src/components/ai-elements/tool"
10+
import type { ToolInvocationState } from "@/app/chat/providers/chat-context"
11+
import type { DynamicToolUIPart } from "ai"
12+
13+
export interface AgentToolsProps {
14+
tools: (ToolInvocationState | DynamicToolUIPart)[]
15+
}
16+
17+
export function AgentTools({ tools }: AgentToolsProps) {
18+
if (!tools || tools.length === 0) return null
19+
20+
return (
21+
<div className="space-y-2">
22+
{tools.map((tool) => {
23+
const t = tool as ToolInvocationState
24+
const toolName = t.toolName || t.type?.replace(/^(tool-|dynamic-tool)/, "") || "unknown"
25+
const toolType = `tool-${toolName}` as const
26+
27+
return (
28+
<Tool key={t.toolCallId} defaultOpen={false}>
29+
<ToolHeader
30+
title={toolName}
31+
type={toolType}
32+
state={t.state}
33+
/>
34+
<ToolContent>
35+
<ToolInput input={t.input} />
36+
{(t.state === "output-available" || t.state === "output-error") && (
37+
<ToolOutput
38+
output={t.state === "output-available" ? t.output : undefined}
39+
errorText={t.state === "output-error" ? t.errorText : undefined}
40+
/>
41+
)}
42+
</ToolContent>
43+
</Tool>
44+
)
45+
})}
46+
</div>
47+
)
48+
}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
"use client"
2+
3+
import { Button } from "@/ui/button"
4+
import {
5+
ModelSelector,
6+
ModelSelectorTrigger,
7+
ModelSelectorContent,
8+
ModelSelectorInput,
9+
ModelSelectorList,
10+
ModelSelectorGroup,
11+
ModelSelectorItem,
12+
ModelSelectorEmpty,
13+
ModelSelectorName,
14+
} from "@/src/components/ai-elements/model-selector"
15+
import {
16+
Context,
17+
ContextTrigger,
18+
ContextContent,
19+
ContextContentHeader,
20+
ContextContentBody,
21+
ContextContentFooter,
22+
ContextInputUsage,
23+
ContextOutputUsage,
24+
} from "@/src/components/ai-elements/context"
25+
import { useChatContext } from "@/app/chat/providers/chat-context"
26+
import {
27+
getAgentsByCategory,
28+
CATEGORY_LABELS,
29+
CATEGORY_ORDER,
30+
type AgentConfig,
31+
} from "@/app/chat/config/agents"
32+
import { CheckIcon, MessageSquareIcon, Trash2Icon } from "lucide-react"
33+
import { useMemo, useState } from "react"
34+
35+
const DEFAULT_MAX_TOKENS = 128000
36+
37+
export function ChatHeader() {
38+
const { selectedAgent, selectAgent, clearMessages, agentConfig, messages, usage } =
39+
useChatContext()
40+
const [open, setOpen] = useState(false)
41+
42+
const agentsByCategory = useMemo(() => getAgentsByCategory(), [])
43+
44+
const handleSelectAgent = (agent: AgentConfig) => {
45+
selectAgent(agent.id)
46+
setOpen(false)
47+
}
48+
49+
const usedTokens = usage ? usage.inputTokens + usage.outputTokens : 0
50+
51+
return (
52+
<header className="flex items-center justify-between border-b border-border px-4 py-3">
53+
<div className="flex items-center gap-3">
54+
<MessageSquareIcon className="size-5 text-muted-foreground" />
55+
<div className="flex flex-col">
56+
<span className="font-semibold text-sm">AgentStack Chat</span>
57+
<span className="text-muted-foreground text-xs">
58+
{agentConfig?.description || "AI-powered assistant"}
59+
</span>
60+
</div>
61+
</div>
62+
63+
<div className="flex items-center gap-2">
64+
{usage && usedTokens > 0 && (
65+
<Context
66+
usedTokens={usedTokens}
67+
maxTokens={DEFAULT_MAX_TOKENS}
68+
usage={{
69+
inputTokens: usage.inputTokens,
70+
outputTokens: usage.outputTokens,
71+
totalTokens: usage.totalTokens,
72+
}}
73+
>
74+
<ContextTrigger />
75+
<ContextContent align="end">
76+
<ContextContentHeader />
77+
<ContextContentBody className="space-y-1">
78+
<ContextInputUsage />
79+
<ContextOutputUsage />
80+
</ContextContentBody>
81+
<ContextContentFooter />
82+
</ContextContent>
83+
</Context>
84+
)}
85+
86+
<ModelSelector open={open} onOpenChange={setOpen}>
87+
<ModelSelectorTrigger asChild>
88+
<Button variant="outline" size="sm" className="min-w-[180px] justify-between">
89+
<span className="truncate">{agentConfig?.name || selectedAgent}</span>
90+
</Button>
91+
</ModelSelectorTrigger>
92+
<ModelSelectorContent className="w-[320px]">
93+
<ModelSelectorInput placeholder="Search agents..." />
94+
<ModelSelectorList className="max-h-[400px]">
95+
<ModelSelectorEmpty>No agents found.</ModelSelectorEmpty>
96+
{CATEGORY_ORDER.map((category) => {
97+
const agents = agentsByCategory[category]
98+
if (agents.length === 0) return null
99+
100+
return (
101+
<ModelSelectorGroup
102+
key={category}
103+
heading={CATEGORY_LABELS[category]}
104+
>
105+
{agents.map((agent) => (
106+
<ModelSelectorItem
107+
key={agent.id}
108+
value={agent.id}
109+
onSelect={() => handleSelectAgent(agent)}
110+
className="flex items-center justify-between"
111+
>
112+
<div className="flex flex-col gap-0.5">
113+
<ModelSelectorName>{agent.name}</ModelSelectorName>
114+
<span className="text-muted-foreground text-xs line-clamp-1">
115+
{agent.description}
116+
</span>
117+
</div>
118+
{selectedAgent === agent.id && (
119+
<CheckIcon className="size-4 text-primary" />
120+
)}
121+
</ModelSelectorItem>
122+
))}
123+
</ModelSelectorGroup>
124+
)
125+
})}
126+
</ModelSelectorList>
127+
</ModelSelectorContent>
128+
</ModelSelector>
129+
130+
{messages.length > 0 && (
131+
<Button
132+
variant="ghost"
133+
size="icon"
134+
onClick={clearMessages}
135+
title="Clear conversation"
136+
>
137+
<Trash2Icon className="size-4" />
138+
</Button>
139+
)}
140+
</div>
141+
</header>
142+
)
143+
}

0 commit comments

Comments
 (0)