feat: comprehensive UI improvements - dark/light mode, markdown support, diff tool, and enhanced chat interface
This commit is contained in:
parent
0e0d59f13f
commit
ee74ad8485
161
inspiration-repo-agent/AGENT_DIFF_TOOL_SETUP.md
Normal file
161
inspiration-repo-agent/AGENT_DIFF_TOOL_SETUP.md
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
# Agent Diff Tool Setup Guide
|
||||||
|
|
||||||
|
This guide explains how to configure your agent (n8n workflow) to use the diff tool functionality.
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
The chat interface now supports diff tool calls that get converted into beautiful, interactive diff displays. When the agent wants to show code changes, it can call the `show_diff` tool, which will be automatically converted to the proper markdown format.
|
||||||
|
|
||||||
|
## Agent Tool Call Format
|
||||||
|
|
||||||
|
Your agent should return tool calls in this JSON format:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "tool_call",
|
||||||
|
"name": "show_diff",
|
||||||
|
"args": {
|
||||||
|
"oldCode": "function hello() {\n console.log('Hello, World!');\n}",
|
||||||
|
"newCode": "function hello(name = 'World') {\n console.log(`Hello, ${name}!`);\n}",
|
||||||
|
"title": "Updated hello function",
|
||||||
|
"language": "javascript"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
|
||||||
|
- **oldCode** (required): The original code as a string with `\n` for line breaks
|
||||||
|
- **newCode** (required): The modified code as a string with `\n` for line breaks
|
||||||
|
- **title** (optional): A descriptive title for the diff (defaults to "Code Changes")
|
||||||
|
- **language** (optional): The programming language for syntax highlighting (defaults to "text")
|
||||||
|
|
||||||
|
## n8n Workflow Configuration
|
||||||
|
|
||||||
|
### Option 1: Direct Tool Call Response
|
||||||
|
|
||||||
|
In your n8n workflow, when you want to show a diff, return:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "tool_call",
|
||||||
|
"name": "show_diff",
|
||||||
|
"args": {
|
||||||
|
"oldCode": "const x = 1;",
|
||||||
|
"newCode": "const x = 1;\nconst y = 2;",
|
||||||
|
"title": "Added new variable",
|
||||||
|
"language": "javascript"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 2: Mixed Response with Tool Calls
|
||||||
|
|
||||||
|
You can also mix regular text with tool calls:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"type": "item", "content": "Here are the changes I made:\n\n"}
|
||||||
|
|
||||||
|
{"type": "tool_call", "name": "show_diff", "args": {"oldCode": "old code", "newCode": "new code", "title": "Changes"}}
|
||||||
|
|
||||||
|
{"type": "item", "content": "\n\nThese changes improve the functionality by..."}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 3: Inline Tool Call
|
||||||
|
|
||||||
|
You can also return a single JSON object:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "tool_call",
|
||||||
|
"name": "show_diff",
|
||||||
|
"args": {
|
||||||
|
"oldCode": "function example() {\n return 'old';\n}",
|
||||||
|
"newCode": "function example() {\n return 'new and improved';\n}",
|
||||||
|
"title": "Function improvement",
|
||||||
|
"language": "javascript"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example Use Cases
|
||||||
|
|
||||||
|
### 1. Code Refactoring
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "tool_call",
|
||||||
|
"name": "show_diff",
|
||||||
|
"args": {
|
||||||
|
"oldCode": "if (user) {\n return user.name;\n}",
|
||||||
|
"newCode": "if (user && user.name) {\n return user.name;\n} else {\n return 'Anonymous';\n}",
|
||||||
|
"title": "Added null safety check",
|
||||||
|
"language": "javascript"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Bug Fix
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "tool_call",
|
||||||
|
"name": "show_diff",
|
||||||
|
"args": {
|
||||||
|
"oldCode": "const result = a + b;",
|
||||||
|
"newCode": "const result = Number(a) + Number(b);",
|
||||||
|
"title": "Fixed type coercion bug",
|
||||||
|
"language": "javascript"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Feature Addition
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "tool_call",
|
||||||
|
"name": "show_diff",
|
||||||
|
"args": {
|
||||||
|
"oldCode": "function calculate(x) {\n return x * 2;\n}",
|
||||||
|
"newCode": "function calculate(x, multiplier = 2) {\n return x * multiplier;\n}",
|
||||||
|
"title": "Added configurable multiplier",
|
||||||
|
"language": "javascript"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## n8n Node Configuration
|
||||||
|
|
||||||
|
In your n8n workflow, use a "Respond to Webhook" node with:
|
||||||
|
|
||||||
|
1. **Response Body**: Set to the JSON format above
|
||||||
|
2. **Response Headers**: Set `Content-Type` to `application/json`
|
||||||
|
3. **Response Code**: Set to `200`
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
To test the diff tool:
|
||||||
|
|
||||||
|
1. Send a message to your agent asking for code changes
|
||||||
|
2. Configure your agent to return a `show_diff` tool call
|
||||||
|
3. The chat interface will automatically render the diff
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
If the tool call is malformed, the API will return an error message instead of breaking. Make sure to:
|
||||||
|
|
||||||
|
- Include both `oldCode` and `newCode`
|
||||||
|
- Use proper JSON formatting
|
||||||
|
- Escape newlines as `\n`
|
||||||
|
- Use valid JSON structure
|
||||||
|
|
||||||
|
## Advanced Usage
|
||||||
|
|
||||||
|
You can also combine multiple tool calls in a single response by returning multiple JSON objects on separate lines:
|
||||||
|
|
||||||
|
```
|
||||||
|
{"type": "item", "content": "Here's the first change:\n\n"}
|
||||||
|
{"type": "tool_call", "name": "show_diff", "args": {...}}
|
||||||
|
{"type": "item", "content": "\n\nAnd here's the second change:\n\n"}
|
||||||
|
{"type": "tool_call", "name": "show_diff", "args": {...}}
|
||||||
|
```
|
||||||
|
|
||||||
|
This allows you to show multiple diffs in a single response.
|
||||||
14560
inspiration-repo-agent/package-lock.json
generated
14560
inspiration-repo-agent/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -10,6 +10,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^3.10.0",
|
"@hookform/resolvers": "^3.10.0",
|
||||||
|
"@opennextjs/cloudflare": "^1.9.1",
|
||||||
"@radix-ui/react-accordion": "1.2.2",
|
"@radix-ui/react-accordion": "1.2.2",
|
||||||
"@radix-ui/react-alert-dialog": "1.1.4",
|
"@radix-ui/react-alert-dialog": "1.1.4",
|
||||||
"@radix-ui/react-aspect-ratio": "1.1.1",
|
"@radix-ui/react-aspect-ratio": "1.1.1",
|
||||||
@ -43,6 +44,7 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "1.0.4",
|
"cmdk": "1.0.4",
|
||||||
"date-fns": "4.1.0",
|
"date-fns": "4.1.0",
|
||||||
|
"diff": "^8.0.2",
|
||||||
"embla-carousel-react": "8.5.1",
|
"embla-carousel-react": "8.5.1",
|
||||||
"geist": "^1.3.1",
|
"geist": "^1.3.1",
|
||||||
"input-otp": "1.4.1",
|
"input-otp": "1.4.1",
|
||||||
@ -53,8 +55,11 @@
|
|||||||
"react-day-picker": "9.8.0",
|
"react-day-picker": "9.8.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-hook-form": "^7.60.0",
|
"react-hook-form": "^7.60.0",
|
||||||
|
"react-markdown": "^10.1.0",
|
||||||
"react-resizable-panels": "^2.1.7",
|
"react-resizable-panels": "^2.1.7",
|
||||||
"recharts": "2.15.4",
|
"recharts": "2.15.4",
|
||||||
|
"rehype-highlight": "^7.0.2",
|
||||||
|
"remark-gfm": "^4.0.1",
|
||||||
"sonner": "^1.7.4",
|
"sonner": "^1.7.4",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
@ -69,6 +74,7 @@
|
|||||||
"postcss": "^8.5",
|
"postcss": "^8.5",
|
||||||
"tailwindcss": "^4.1.9",
|
"tailwindcss": "^4.1.9",
|
||||||
"tw-animate-css": "1.3.3",
|
"tw-animate-css": "1.3.3",
|
||||||
"typescript": "^5"
|
"typescript": "^5",
|
||||||
|
"wrangler": "^4.42.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,29 @@ import { type NextRequest, NextResponse } from "next/server"
|
|||||||
|
|
||||||
const WEBHOOK_URL = "https://n8n.biohazardvfx.com/webhook/d2ab4653-a107-412c-a905-ccd80e5b76cd"
|
const WEBHOOK_URL = "https://n8n.biohazardvfx.com/webhook/d2ab4653-a107-412c-a905-ccd80e5b76cd"
|
||||||
|
|
||||||
|
// Helper function to convert diff tool call to markdown format
|
||||||
|
function convertToDiffTool(args: any): string {
|
||||||
|
try {
|
||||||
|
const { oldCode, newCode, title, language } = args
|
||||||
|
|
||||||
|
if (!oldCode || !newCode) {
|
||||||
|
return "Error: Missing oldCode or newCode in diff tool call"
|
||||||
|
}
|
||||||
|
|
||||||
|
const diffToolCall = {
|
||||||
|
oldCode: String(oldCode).replace(/\n/g, '\\n'),
|
||||||
|
newCode: String(newCode).replace(/\n/g, '\\n'),
|
||||||
|
title: title || "Code Changes",
|
||||||
|
language: language || "text"
|
||||||
|
}
|
||||||
|
|
||||||
|
return `\`\`\`diff-tool\n${JSON.stringify(diffToolCall, null, 2)}\n\`\`\``
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[v0] Error converting diff tool:", error)
|
||||||
|
return "Error: Failed to process diff tool call"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
@ -85,6 +108,12 @@ export async function POST(request: NextRequest) {
|
|||||||
if (chunk.type === "item" && chunk.content) {
|
if (chunk.type === "item" && chunk.content) {
|
||||||
chunks.push(chunk.content)
|
chunks.push(chunk.content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle diff tool calls
|
||||||
|
if (chunk.type === "tool_call" && chunk.name === "show_diff") {
|
||||||
|
const diffTool = convertToDiffTool(chunk.args)
|
||||||
|
chunks.push(diffTool)
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
console.log("[v0] Failed to parse line:", line)
|
console.log("[v0] Failed to parse line:", line)
|
||||||
}
|
}
|
||||||
@ -101,6 +130,12 @@ export async function POST(request: NextRequest) {
|
|||||||
const data = JSON.parse(responseText)
|
const data = JSON.parse(responseText)
|
||||||
console.log("[v0] Parsed webhook data:", data)
|
console.log("[v0] Parsed webhook data:", data)
|
||||||
|
|
||||||
|
// Check if this is a diff tool call
|
||||||
|
if (data.type === "tool_call" && data.name === "show_diff") {
|
||||||
|
const diffTool = convertToDiffTool(data.args)
|
||||||
|
return NextResponse.json({ response: diffTool })
|
||||||
|
}
|
||||||
|
|
||||||
// Extract the response from various possible fields
|
// Extract the response from various possible fields
|
||||||
let responseMessage = data.response || data.message || data.output || data.text
|
let responseMessage = data.response || data.message || data.output || data.text
|
||||||
|
|
||||||
|
|||||||
@ -4,39 +4,40 @@
|
|||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: oklch(1 0 0);
|
/* Warm light mode with cream/beige tones */
|
||||||
--foreground: oklch(0.145 0 0);
|
--background: oklch(0.97 0.01 60);
|
||||||
--card: oklch(1 0 0);
|
--foreground: oklch(0.2 0.02 30);
|
||||||
--card-foreground: oklch(0.145 0 0);
|
--card: oklch(0.99 0.005 60);
|
||||||
--popover: oklch(1 0 0);
|
--card-foreground: oklch(0.2 0.02 30);
|
||||||
--popover-foreground: oklch(0.145 0 0);
|
--popover: oklch(0.99 0.005 60);
|
||||||
--primary: oklch(0.205 0 0);
|
--popover-foreground: oklch(0.2 0.02 30);
|
||||||
--primary-foreground: oklch(0.985 0 0);
|
--primary: oklch(0.68 0.19 45);
|
||||||
--secondary: oklch(0.97 0 0);
|
--primary-foreground: oklch(0.99 0.005 60);
|
||||||
--secondary-foreground: oklch(0.205 0 0);
|
--secondary: oklch(0.94 0.01 60);
|
||||||
--muted: oklch(0.97 0 0);
|
--secondary-foreground: oklch(0.2 0.02 30);
|
||||||
--muted-foreground: oklch(0.556 0 0);
|
--muted: oklch(0.94 0.01 60);
|
||||||
--accent: oklch(0.97 0 0);
|
--muted-foreground: oklch(0.5 0.015 40);
|
||||||
--accent-foreground: oklch(0.205 0 0);
|
--accent: oklch(0.94 0.015 50);
|
||||||
|
--accent-foreground: oklch(0.2 0.02 30);
|
||||||
--destructive: oklch(0.577 0.245 27.325);
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
--destructive-foreground: oklch(0.577 0.245 27.325);
|
--destructive-foreground: oklch(0.99 0.005 60);
|
||||||
--border: oklch(0.922 0 0);
|
--border: oklch(0.88 0.015 55);
|
||||||
--input: oklch(0.922 0 0);
|
--input: oklch(0.88 0.015 55);
|
||||||
--ring: oklch(0.708 0 0);
|
--ring: oklch(0.68 0.19 45);
|
||||||
--chart-1: oklch(0.646 0.222 41.116);
|
--chart-1: oklch(0.646 0.222 41.116);
|
||||||
--chart-2: oklch(0.6 0.118 184.704);
|
--chart-2: oklch(0.6 0.118 184.704);
|
||||||
--chart-3: oklch(0.398 0.07 227.392);
|
--chart-3: oklch(0.398 0.07 227.392);
|
||||||
--chart-4: oklch(0.828 0.189 84.429);
|
--chart-4: oklch(0.828 0.189 84.429);
|
||||||
--chart-5: oklch(0.769 0.188 70.08);
|
--chart-5: oklch(0.769 0.188 70.08);
|
||||||
--radius: 0.625rem;
|
--radius: 0.625rem;
|
||||||
--sidebar: oklch(0.985 0 0);
|
--sidebar: oklch(0.99 0.005 60);
|
||||||
--sidebar-foreground: oklch(0.145 0 0);
|
--sidebar-foreground: oklch(0.2 0.02 30);
|
||||||
--sidebar-primary: oklch(0.205 0 0);
|
--sidebar-primary: oklch(0.68 0.19 45);
|
||||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
--sidebar-primary-foreground: oklch(0.99 0.005 60);
|
||||||
--sidebar-accent: oklch(0.97 0 0);
|
--sidebar-accent: oklch(0.94 0.015 50);
|
||||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
--sidebar-accent-foreground: oklch(0.2 0.02 30);
|
||||||
--sidebar-border: oklch(0.922 0 0);
|
--sidebar-border: oklch(0.88 0.015 55);
|
||||||
--sidebar-ring: oklch(0.708 0 0);
|
--sidebar-ring: oklch(0.68 0.19 45);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
@ -118,19 +119,24 @@
|
|||||||
* {
|
* {
|
||||||
@apply border-border outline-ring/50;
|
@apply border-border outline-ring/50;
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: #ff8c00 #000000;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Light mode scrollbar */
|
||||||
|
* {
|
||||||
|
scrollbar-color: #ff8c00 oklch(0.94 0.01 60);
|
||||||
|
}
|
||||||
|
|
||||||
*::-webkit-scrollbar {
|
*::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
*::-webkit-scrollbar-track {
|
*::-webkit-scrollbar-track {
|
||||||
background: #000000;
|
background: oklch(0.94 0.01 60);
|
||||||
}
|
}
|
||||||
|
|
||||||
*::-webkit-scrollbar-thumb {
|
*::-webkit-scrollbar-thumb {
|
||||||
@ -141,5 +147,23 @@
|
|||||||
*::-webkit-scrollbar-thumb:hover {
|
*::-webkit-scrollbar-thumb:hover {
|
||||||
background: #ff9d1a;
|
background: #ff9d1a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Dark mode scrollbar */
|
||||||
|
.dark * {
|
||||||
|
scrollbar-color: #ff8c00 #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark *::-webkit-scrollbar-track {
|
||||||
|
background: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark *::-webkit-scrollbar-thumb {
|
||||||
|
background: #ff8c00;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark *::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #ff9d1a;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,12 +4,31 @@ import { GeistSans } from "geist/font/sans"
|
|||||||
import { GeistMono } from "geist/font/mono"
|
import { GeistMono } from "geist/font/mono"
|
||||||
import { Analytics } from "@vercel/analytics/next"
|
import { Analytics } from "@vercel/analytics/next"
|
||||||
import { Suspense } from "react"
|
import { Suspense } from "react"
|
||||||
|
import { ThemeProvider } from "@/components/theme-provider"
|
||||||
import "./globals.css"
|
import "./globals.css"
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "AI Chat Interface",
|
title: "Inspiration Repo Agent - AI-Powered Creative Assistant",
|
||||||
description: "Chat with AI Agent",
|
description: "Get inspired and generate creative content with our AI-powered assistant. Perfect for brainstorming, content creation, and creative problem-solving.",
|
||||||
generator: "v0.app",
|
keywords: "AI, creative assistant, inspiration, brainstorming, content creation",
|
||||||
|
authors: [{ name: "Inspiration Repo Team" }],
|
||||||
|
creator: "Inspiration Repo",
|
||||||
|
publisher: "Inspiration Repo",
|
||||||
|
robots: "index, follow",
|
||||||
|
openGraph: {
|
||||||
|
title: "Inspiration Repo Agent - AI-Powered Creative Assistant",
|
||||||
|
description: "Get inspired and generate creative content with our AI-powered assistant.",
|
||||||
|
type: "website",
|
||||||
|
locale: "en_US",
|
||||||
|
siteName: "Inspiration Repo Agent",
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary_large_image",
|
||||||
|
title: "Inspiration Repo Agent - AI-Powered Creative Assistant",
|
||||||
|
description: "Get inspired and generate creative content with our AI-powered assistant.",
|
||||||
|
},
|
||||||
|
viewport: "width=device-width, initial-scale=1",
|
||||||
|
canonical: "https://inspiration-repo-agent.com",
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
@ -18,10 +37,22 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en" className="dark">
|
<html lang="en" suppressHydrationWarning>
|
||||||
|
<head>
|
||||||
|
<link rel="canonical" href="https://inspiration-repo-agent.com" />
|
||||||
|
<link rel="preload" href="/fonts/geist-sans.woff2" as="font" type="font/woff2" crossOrigin="anonymous" />
|
||||||
|
<link rel="preload" href="/fonts/geist-mono.woff2" as="font" type="font/woff2" crossOrigin="anonymous" />
|
||||||
|
</head>
|
||||||
<body className={`font-sans ${GeistSans.variable} ${GeistMono.variable}`}>
|
<body className={`font-sans ${GeistSans.variable} ${GeistMono.variable}`}>
|
||||||
<Suspense fallback={null}>{children}</Suspense>
|
<ThemeProvider
|
||||||
<Analytics />
|
attribute="class"
|
||||||
|
defaultTheme="system"
|
||||||
|
enableSystem
|
||||||
|
disableTransitionOnChange
|
||||||
|
>
|
||||||
|
<Suspense fallback={null}>{children}</Suspense>
|
||||||
|
<Analytics />
|
||||||
|
</ThemeProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,9 +1,13 @@
|
|||||||
import { ChatInterface } from "@/components/chat-interface"
|
import { ChatInterface } from "@/components/chat-interface"
|
||||||
|
import { Header } from "@/components/header"
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<main className="bg-black">
|
<div className="flex h-screen flex-col bg-background">
|
||||||
<ChatInterface />
|
<Header />
|
||||||
</main>
|
<main className="flex-1 overflow-hidden">
|
||||||
|
<ChatInterface />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
69
inspiration-repo-agent/src/components/DIFF_TOOL_USAGE.md
Normal file
69
inspiration-repo-agent/src/components/DIFF_TOOL_USAGE.md
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
# Diff Tool Usage Guide
|
||||||
|
|
||||||
|
The diff tool allows the AI model to display code differences in a beautiful, interactive format within chat messages.
|
||||||
|
|
||||||
|
## How to Use
|
||||||
|
|
||||||
|
The model can include diff displays in its responses by using the following markdown syntax:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
```diff-tool
|
||||||
|
{
|
||||||
|
"oldCode": "function hello() {\n console.log('Hello, World!');\n}",
|
||||||
|
"newCode": "function hello(name = 'World') {\n console.log(\`Hello, \${name}!\`);\n}",
|
||||||
|
"title": "Updated hello function",
|
||||||
|
"language": "javascript"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
|
||||||
|
- **oldCode** (required): The original code as a string with `\n` for line breaks
|
||||||
|
- **newCode** (required): The modified code as a string with `\n` for line breaks
|
||||||
|
- **title** (optional): A descriptive title for the diff (defaults to "Code Changes")
|
||||||
|
- **language** (optional): The programming language for syntax highlighting (defaults to "text")
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Interactive**: Click to expand/collapse the diff
|
||||||
|
- **Copy to Clipboard**: Copy button to copy the full diff
|
||||||
|
- **Line Numbers**: Shows line numbers for both old and new code
|
||||||
|
- **Color Coding**:
|
||||||
|
- Green for additions (+)
|
||||||
|
- Red for deletions (-)
|
||||||
|
- Neutral for unchanged lines
|
||||||
|
- **Syntax Highlighting**: Supports various programming languages
|
||||||
|
- **Responsive**: Works well on different screen sizes
|
||||||
|
|
||||||
|
## Example Usage Scenarios
|
||||||
|
|
||||||
|
1. **Code Refactoring**: Show before/after code improvements
|
||||||
|
2. **Bug Fixes**: Highlight what was changed to fix an issue
|
||||||
|
3. **Feature Additions**: Display new functionality being added
|
||||||
|
4. **Configuration Changes**: Show config file modifications
|
||||||
|
5. **API Changes**: Demonstrate API signature changes
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Keep it focused**: Show only relevant changes, not entire files
|
||||||
|
2. **Add context**: Use descriptive titles and surrounding text
|
||||||
|
3. **Escape properly**: Make sure to escape quotes and newlines in JSON
|
||||||
|
4. **Reasonable size**: Avoid extremely large diffs that are hard to read
|
||||||
|
|
||||||
|
## Example Response Format
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
Here are the changes I made to fix the bug:
|
||||||
|
|
||||||
|
```diff-tool
|
||||||
|
{
|
||||||
|
"oldCode": "if (user) {\n return user.name;\n}",
|
||||||
|
"newCode": "if (user && user.name) {\n return user.name;\n} else {\n return 'Anonymous';\n}",
|
||||||
|
"title": "Fixed null reference bug",
|
||||||
|
"language": "javascript"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The fix adds proper null checking and provides a fallback value.
|
||||||
|
```
|
||||||
@ -5,7 +5,8 @@ import type React from "react"
|
|||||||
import { useState, useRef, useEffect } from "react"
|
import { useState, useRef, useEffect } from "react"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Send, Bot, Loader2, SquarePen } from "lucide-react"
|
import { Send, Bot, Loader2, SquarePen, Sparkles, Paperclip, Mic } from "lucide-react"
|
||||||
|
import { MarkdownRenderer } from "./markdown-renderer"
|
||||||
|
|
||||||
interface Message {
|
interface Message {
|
||||||
id: string
|
id: string
|
||||||
@ -22,7 +23,7 @@ export function ChatInterface() {
|
|||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const [sessionId, setSessionId] = useState<string>("")
|
const [sessionId, setSessionId] = useState<string>("")
|
||||||
const messagesContainerRef = useRef<HTMLDivElement>(null)
|
const messagesContainerRef = useRef<HTMLDivElement>(null)
|
||||||
const inputRef = useRef<HTMLInputElement>(null)
|
const inputRef = useRef<HTMLTextAreaElement>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Try to get existing sessionID from localStorage
|
// Try to get existing sessionID from localStorage
|
||||||
@ -43,8 +44,10 @@ export function ChatInterface() {
|
|||||||
}
|
}
|
||||||
}, [messages, isLoading])
|
}, [messages, isLoading])
|
||||||
|
|
||||||
const sendMessage = async (e: React.FormEvent) => {
|
const sendMessage = async (e?: React.FormEvent) => {
|
||||||
e.preventDefault()
|
if (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
if (!input.trim() || isLoading) return
|
if (!input.trim() || isLoading) return
|
||||||
|
|
||||||
@ -129,8 +132,15 @@ export function ChatInterface() {
|
|||||||
inputRef.current?.focus()
|
inputRef.current?.focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault()
|
||||||
|
sendMessage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen w-full flex-col">
|
<div className="flex h-full w-full flex-col">
|
||||||
{messages.length > 0 && (
|
{messages.length > 0 && (
|
||||||
<div className="absolute right-4 top-4 z-10">
|
<div className="absolute right-4 top-4 z-10">
|
||||||
<Button
|
<Button
|
||||||
@ -148,46 +158,118 @@ export function ChatInterface() {
|
|||||||
<div ref={messagesContainerRef} className="flex-1 overflow-y-auto px-4 py-8 pb-32">
|
<div ref={messagesContainerRef} className="flex-1 overflow-y-auto px-4 py-8 pb-32">
|
||||||
<div className="mx-auto max-w-3xl">
|
<div className="mx-auto max-w-3xl">
|
||||||
{messages.length === 0 ? (
|
{messages.length === 0 ? (
|
||||||
<div className="flex h-full min-h-[60vh] flex-col items-center justify-center gap-3 text-center">
|
<div className="flex h-full min-h-[60vh] flex-col items-center justify-center gap-8 text-center">
|
||||||
<h1 className="text-2xl font-semibold text-white">Hello there!</h1>
|
<div className="space-y-4">
|
||||||
<p className="text-base text-neutral-400">How can I help you today?</p>
|
<div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-orange-500">
|
||||||
|
<Sparkles className="h-8 w-8 text-white" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h1 className="text-3xl font-bold text-white">Welcome to Inspiration Repo</h1>
|
||||||
|
<p className="text-lg text-neutral-400">Your AI-powered creative assistant is ready to help you brainstorm, create, and innovate.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid w-full max-w-2xl gap-3 sm:grid-cols-2">
|
||||||
|
{[
|
||||||
|
"Help me brainstorm ideas for a new mobile app",
|
||||||
|
"Generate creative writing prompts for a fantasy novel",
|
||||||
|
"Suggest innovative marketing strategies for a startup",
|
||||||
|
"Create a list of unique product names for a tech company"
|
||||||
|
].map((prompt, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
onClick={() => setInput(prompt)}
|
||||||
|
className="rounded-xl border border-neutral-800 bg-neutral-900/50 p-4 text-left text-sm text-neutral-300 transition-all hover:border-orange-500/50 hover:bg-neutral-800/50 hover:text-white"
|
||||||
|
>
|
||||||
|
{prompt}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 text-xs text-neutral-500">
|
||||||
|
<div className="h-1 w-1 rounded-full bg-green-500"></div>
|
||||||
|
<span>AI Assistant Online</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-6">
|
<div className="space-y-8">
|
||||||
{messages.map((message) => (
|
{messages.map((message) => (
|
||||||
<div key={message.id} className="flex gap-3">
|
<div key={message.id} className={`flex gap-4 ${message.role === "user" ? "flex-row-reverse" : ""}`}>
|
||||||
{message.role === "assistant" && (
|
{message.role === "assistant" && (
|
||||||
<div className="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-neutral-800">
|
<div className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-orange-500">
|
||||||
<Bot className="h-4 w-4 text-neutral-400" />
|
<Bot className="h-4 w-4 text-white" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={`flex-1 ${message.role === "user" ? "flex justify-end" : ""}`}>
|
{message.role === "user" && (
|
||||||
<div
|
<div className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-gradient-to-br from-blue-500 to-purple-500">
|
||||||
className={`inline-block max-w-[85%] rounded-2xl px-4 py-2.5 ${
|
<div className="h-4 w-4 rounded-full bg-white"></div>
|
||||||
message.role === "user"
|
|
||||||
? "bg-primary text-white"
|
|
||||||
: message.isError
|
|
||||||
? "bg-red-500/10 text-red-400"
|
|
||||||
: "bg-neutral-800/50 text-neutral-100"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<p className="whitespace-pre-wrap break-words text-sm leading-relaxed">{message.content}</p>
|
|
||||||
</div>
|
</div>
|
||||||
{message.hint && <p className="mt-1 text-xs text-neutral-500">{message.hint}</p>}
|
)}
|
||||||
|
|
||||||
|
<div className="flex-1 space-y-2">
|
||||||
|
{message.role === "user" && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs font-medium text-neutral-400">You</span>
|
||||||
|
<span className="text-xs text-neutral-600">
|
||||||
|
{message.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{message.role === "user" ? (
|
||||||
|
<div className="rounded-2xl px-5 py-4 shadow-sm bg-orange-500 text-white">
|
||||||
|
<p className="whitespace-pre-wrap break-words text-sm leading-relaxed">{message.content}</p>
|
||||||
|
</div>
|
||||||
|
) : message.isError ? (
|
||||||
|
<div className="rounded-2xl px-5 py-4 shadow-sm bg-red-500/10 text-red-400 border border-red-500/20">
|
||||||
|
<p className="whitespace-pre-wrap break-words text-sm leading-relaxed">{message.content}</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs font-medium text-neutral-400">AI Assistant</span>
|
||||||
|
<span className="text-xs text-neutral-600">
|
||||||
|
{message.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-neutral-100">
|
||||||
|
<MarkdownRenderer content={message.content} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{message.hint && (
|
||||||
|
<div className="rounded-lg bg-amber-500/10 border border-amber-500/20 px-3 py-2">
|
||||||
|
<p className="text-xs text-amber-400">{message.hint}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-4">
|
||||||
<div className="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-neutral-800">
|
<div className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-orange-500">
|
||||||
<Bot className="h-4 w-4 text-neutral-400" />
|
<Bot className="h-4 w-4 text-white" />
|
||||||
</div>
|
</div>
|
||||||
<div className="inline-block rounded-2xl bg-neutral-800/50 px-4 py-2.5">
|
<div className="flex-1 space-y-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Loader2 className="h-4 w-4 animate-spin text-neutral-400" />
|
<span className="text-xs font-medium text-neutral-400">AI Assistant</span>
|
||||||
<p className="text-sm text-neutral-400">Thinking...</p>
|
<span className="text-xs text-neutral-600">
|
||||||
|
{new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-neutral-100">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin text-orange-500" />
|
||||||
|
<p className="text-sm text-neutral-400">Thinking...</p>
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<div className="h-1 w-1 rounded-full bg-orange-500 animate-bounce"></div>
|
||||||
|
<div className="h-1 w-1 rounded-full bg-orange-500 animate-bounce" style={{ animationDelay: '0.1s' }}></div>
|
||||||
|
<div className="h-1 w-1 rounded-full bg-orange-500 animate-bounce" style={{ animationDelay: '0.2s' }}></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -199,23 +281,69 @@ export function ChatInterface() {
|
|||||||
|
|
||||||
<div className="fixed bottom-6 left-1/2 z-20 w-full max-w-3xl -translate-x-1/2 px-4">
|
<div className="fixed bottom-6 left-1/2 z-20 w-full max-w-3xl -translate-x-1/2 px-4">
|
||||||
<form onSubmit={sendMessage} className="relative">
|
<form onSubmit={sendMessage} className="relative">
|
||||||
<div className="flex items-center gap-2 rounded-2xl border border-neutral-800 bg-neutral-900/95 p-2 shadow-2xl backdrop-blur-md">
|
<div className="rounded-2xl border border-neutral-800 bg-neutral-900/95 p-3 shadow-2xl backdrop-blur-md">
|
||||||
<Input
|
<div className="flex items-end gap-3">
|
||||||
ref={inputRef}
|
<div className="flex flex-1 flex-col gap-2">
|
||||||
value={input}
|
<textarea
|
||||||
onChange={(e) => setInput(e.target.value)}
|
ref={inputRef}
|
||||||
placeholder="Ask a follow-up..."
|
value={input}
|
||||||
disabled={isLoading}
|
onChange={(e) => setInput(e.target.value)}
|
||||||
className="flex-1 border-0 bg-transparent text-white placeholder:text-neutral-500 focus-visible:ring-0 focus-visible:ring-offset-0"
|
onKeyDown={handleKeyDown}
|
||||||
/>
|
placeholder="Ask me anything or share your ideas... (Enter to send, Shift+Enter for new line)"
|
||||||
<Button
|
disabled={isLoading}
|
||||||
type="submit"
|
rows={1}
|
||||||
disabled={!input.trim() || isLoading}
|
className="min-h-[20px] max-h-32 resize-none border-0 bg-transparent text-white placeholder:text-neutral-500 focus:outline-none focus:ring-0"
|
||||||
size="icon"
|
style={{
|
||||||
className="h-9 w-9 flex-shrink-0 rounded-xl bg-primary text-white hover:bg-primary/90"
|
overflow: 'hidden',
|
||||||
>
|
height: 'auto'
|
||||||
{isLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Send className="h-4 w-4" />}
|
}}
|
||||||
</Button>
|
onInput={(e) => {
|
||||||
|
const target = e.target as HTMLTextAreaElement;
|
||||||
|
target.style.height = 'auto';
|
||||||
|
target.style.height = Math.min(target.scrollHeight, 128) + 'px';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8 text-neutral-400 hover:text-white hover:bg-neutral-800"
|
||||||
|
title="Attach file"
|
||||||
|
>
|
||||||
|
<Paperclip className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8 text-neutral-400 hover:text-white hover:bg-neutral-800"
|
||||||
|
title="Voice input"
|
||||||
|
>
|
||||||
|
<Mic className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{input.trim() && (
|
||||||
|
<span className="text-xs text-neutral-500">
|
||||||
|
{input.length}/2000
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={!input.trim() || isLoading}
|
||||||
|
size="icon"
|
||||||
|
className="h-9 w-9 flex-shrink-0 rounded-xl bg-orange-500 text-white hover:bg-orange-600 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Send className="h-4 w-4" />}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
222
inspiration-repo-agent/src/components/diff-display.tsx
Normal file
222
inspiration-repo-agent/src/components/diff-display.tsx
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useState } from "react"
|
||||||
|
import { ChevronDown, ChevronRight, Copy, Check } from "lucide-react"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
|
||||||
|
interface DiffDisplayProps {
|
||||||
|
oldText: string
|
||||||
|
newText: string
|
||||||
|
title?: string
|
||||||
|
language?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DiffLine {
|
||||||
|
type: 'added' | 'removed' | 'unchanged'
|
||||||
|
content: string
|
||||||
|
oldLineNumber?: number
|
||||||
|
newLineNumber?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DiffDisplay({ oldText, newText, title = "Code Diff", language = "text" }: DiffDisplayProps) {
|
||||||
|
const [isExpanded, setIsExpanded] = useState(false)
|
||||||
|
const [copied, setCopied] = useState(false)
|
||||||
|
|
||||||
|
// Simple diff algorithm - split into lines and compare
|
||||||
|
const generateDiff = (): DiffLine[] => {
|
||||||
|
const oldLines = oldText.split('\n')
|
||||||
|
const newLines = newText.split('\n')
|
||||||
|
const diff: DiffLine[] = []
|
||||||
|
|
||||||
|
let oldIndex = 0
|
||||||
|
let newIndex = 0
|
||||||
|
let oldLineNum = 1
|
||||||
|
let newLineNum = 1
|
||||||
|
|
||||||
|
while (oldIndex < oldLines.length || newIndex < newLines.length) {
|
||||||
|
const oldLine = oldLines[oldIndex]
|
||||||
|
const newLine = newLines[newIndex]
|
||||||
|
|
||||||
|
if (oldIndex >= oldLines.length) {
|
||||||
|
// Only new lines left
|
||||||
|
diff.push({
|
||||||
|
type: 'added',
|
||||||
|
content: newLine,
|
||||||
|
newLineNumber: newLineNum
|
||||||
|
})
|
||||||
|
newIndex++
|
||||||
|
newLineNum++
|
||||||
|
} else if (newIndex >= newLines.length) {
|
||||||
|
// Only old lines left
|
||||||
|
diff.push({
|
||||||
|
type: 'removed',
|
||||||
|
content: oldLine,
|
||||||
|
oldLineNumber: oldLineNum
|
||||||
|
})
|
||||||
|
oldIndex++
|
||||||
|
oldLineNum++
|
||||||
|
} else if (oldLine === newLine) {
|
||||||
|
// Lines are the same
|
||||||
|
diff.push({
|
||||||
|
type: 'unchanged',
|
||||||
|
content: oldLine,
|
||||||
|
oldLineNumber: oldLineNum,
|
||||||
|
newLineNumber: newLineNum
|
||||||
|
})
|
||||||
|
oldIndex++
|
||||||
|
newIndex++
|
||||||
|
oldLineNum++
|
||||||
|
newLineNum++
|
||||||
|
} else {
|
||||||
|
// Lines are different - check if it's an addition or removal
|
||||||
|
const oldLineNext = oldLines[oldIndex + 1]
|
||||||
|
const newLineNext = newLines[newIndex + 1]
|
||||||
|
|
||||||
|
if (oldLineNext === newLine) {
|
||||||
|
// Old line was removed
|
||||||
|
diff.push({
|
||||||
|
type: 'removed',
|
||||||
|
content: oldLine,
|
||||||
|
oldLineNumber: oldLineNum
|
||||||
|
})
|
||||||
|
oldIndex++
|
||||||
|
oldLineNum++
|
||||||
|
} else if (newLineNext === oldLine) {
|
||||||
|
// New line was added
|
||||||
|
diff.push({
|
||||||
|
type: 'added',
|
||||||
|
content: newLine,
|
||||||
|
newLineNumber: newLineNum
|
||||||
|
})
|
||||||
|
newIndex++
|
||||||
|
newLineNum++
|
||||||
|
} else {
|
||||||
|
// Both lines changed
|
||||||
|
diff.push({
|
||||||
|
type: 'removed',
|
||||||
|
content: oldLine,
|
||||||
|
oldLineNumber: oldLineNum
|
||||||
|
})
|
||||||
|
diff.push({
|
||||||
|
type: 'added',
|
||||||
|
content: newLine,
|
||||||
|
newLineNumber: newLineNum
|
||||||
|
})
|
||||||
|
oldIndex++
|
||||||
|
newIndex++
|
||||||
|
oldLineNum++
|
||||||
|
newLineNum++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return diff
|
||||||
|
}
|
||||||
|
|
||||||
|
const diff = generateDiff()
|
||||||
|
const hasChanges = diff.some(line => line.type !== 'unchanged')
|
||||||
|
|
||||||
|
const copyToClipboard = async () => {
|
||||||
|
const fullDiff = diff.map(line => {
|
||||||
|
const prefix = line.type === 'added' ? '+' : line.type === 'removed' ? '-' : ' '
|
||||||
|
const oldNum = line.oldLineNumber ? line.oldLineNumber.toString().padStart(3) : ' '
|
||||||
|
const newNum = line.newLineNumber ? line.newLineNumber.toString().padStart(3) : ' '
|
||||||
|
return `${prefix} ${oldNum}|${newNum} ${line.content}`
|
||||||
|
}).join('\n')
|
||||||
|
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(fullDiff)
|
||||||
|
setCopied(true)
|
||||||
|
setTimeout(() => setCopied(false), 2000)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to copy diff:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasChanges) {
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg border border-neutral-800 bg-neutral-900/50 p-4">
|
||||||
|
<div className="flex items-center gap-2 text-sm text-neutral-400">
|
||||||
|
<Check className="h-4 w-4 text-green-500" />
|
||||||
|
<span>No changes detected</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg border border-neutral-800 bg-neutral-900/50 overflow-hidden">
|
||||||
|
<div className="flex items-center justify-between px-4 py-3 bg-neutral-800/50 border-b border-neutral-800">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setIsExpanded(!isExpanded)}
|
||||||
|
className="h-6 w-6 p-0 text-neutral-400 hover:text-white"
|
||||||
|
>
|
||||||
|
{isExpanded ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
|
||||||
|
</Button>
|
||||||
|
<span className="text-sm font-medium text-white">{title}</span>
|
||||||
|
<span className="text-xs text-neutral-500">({language})</span>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={copyToClipboard}
|
||||||
|
className="h-6 w-6 p-0 text-neutral-400 hover:text-white"
|
||||||
|
>
|
||||||
|
{copied ? <Check className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isExpanded && (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<div className="font-mono text-sm">
|
||||||
|
{diff.map((line, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={`flex items-start gap-2 px-4 py-1 ${
|
||||||
|
line.type === 'added'
|
||||||
|
? 'bg-green-500/10 border-l-4 border-green-500'
|
||||||
|
: line.type === 'removed'
|
||||||
|
? 'bg-red-500/10 border-l-4 border-red-500'
|
||||||
|
: 'bg-transparent'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex-shrink-0 w-12 text-xs text-neutral-500 select-none">
|
||||||
|
{line.oldLineNumber && line.newLineNumber ? (
|
||||||
|
`${line.oldLineNumber}|${line.newLineNumber}`
|
||||||
|
) : line.oldLineNumber ? (
|
||||||
|
`${line.oldLineNumber}| `
|
||||||
|
) : line.newLineNumber ? (
|
||||||
|
` |${line.newLineNumber}`
|
||||||
|
) : (
|
||||||
|
' | '
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex-shrink-0 w-4 text-center text-xs select-none">
|
||||||
|
{line.type === 'added' ? (
|
||||||
|
<span className="text-green-500">+</span>
|
||||||
|
) : line.type === 'removed' ? (
|
||||||
|
<span className="text-red-500">-</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-neutral-500"> </span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={`flex-1 ${
|
||||||
|
line.type === 'added'
|
||||||
|
? 'text-green-400'
|
||||||
|
: line.type === 'removed'
|
||||||
|
? 'text-red-400'
|
||||||
|
: 'text-neutral-300'
|
||||||
|
}`}>
|
||||||
|
<code>{line.content || ' '}</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
36
inspiration-repo-agent/src/components/diff-tool.tsx
Normal file
36
inspiration-repo-agent/src/components/diff-tool.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { DiffDisplay } from "./diff-display"
|
||||||
|
|
||||||
|
interface DiffToolProps {
|
||||||
|
oldCode: string
|
||||||
|
newCode: string
|
||||||
|
title?: string
|
||||||
|
language?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DiffTool({ oldCode, newCode, title, language }: DiffToolProps) {
|
||||||
|
return (
|
||||||
|
<div className="my-4">
|
||||||
|
<DiffDisplay
|
||||||
|
oldText={oldCode}
|
||||||
|
newText={newCode}
|
||||||
|
title={title || "Code Changes"}
|
||||||
|
language={language || "text"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to create a diff tool call
|
||||||
|
export function createDiffToolCall(oldCode: string, newCode: string, title?: string, language?: string) {
|
||||||
|
return {
|
||||||
|
type: "diff_tool",
|
||||||
|
props: {
|
||||||
|
oldCode,
|
||||||
|
newCode,
|
||||||
|
title,
|
||||||
|
language
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
34
inspiration-repo-agent/src/components/header.tsx
Normal file
34
inspiration-repo-agent/src/components/header.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { Sparkles, Settings } from "lucide-react"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { ModeToggle } from "@/components/mode-toggle"
|
||||||
|
|
||||||
|
export function Header() {
|
||||||
|
return (
|
||||||
|
<header className="sticky top-0 z-50 w-full border-b border-border bg-background/80 backdrop-blur-md">
|
||||||
|
<div className="flex h-16 items-center justify-between px-4">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-orange-500">
|
||||||
|
<Sparkles className="h-5 w-5 text-white" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-lg font-semibold text-foreground">Inspiration Repo</h1>
|
||||||
|
<p className="text-xs text-muted-foreground">AI Creative Assistant</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<ModeToggle />
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-9 w-9 text-muted-foreground hover:text-foreground hover:bg-accent"
|
||||||
|
>
|
||||||
|
<Settings className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
)
|
||||||
|
}
|
||||||
188
inspiration-repo-agent/src/components/markdown-renderer.tsx
Normal file
188
inspiration-repo-agent/src/components/markdown-renderer.tsx
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import ReactMarkdown from 'react-markdown'
|
||||||
|
import remarkGfm from 'remark-gfm'
|
||||||
|
import rehypeHighlight from 'rehype-highlight'
|
||||||
|
import 'highlight.js/styles/github-dark.css'
|
||||||
|
import { DiffTool } from './diff-tool'
|
||||||
|
|
||||||
|
interface MarkdownRendererProps {
|
||||||
|
content: string
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse diff tool calls from markdown content
|
||||||
|
function parseDiffTools(content: string) {
|
||||||
|
const diffToolRegex = /```diff-tool\n([\s\S]*?)\n```/g
|
||||||
|
const tools: Array<{ match: string; props: any }> = []
|
||||||
|
let match
|
||||||
|
|
||||||
|
while ((match = diffToolRegex.exec(content)) !== null) {
|
||||||
|
try {
|
||||||
|
const props = JSON.parse(match[1])
|
||||||
|
tools.push({ match: match[0], props })
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to parse diff tool:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tools
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MarkdownRenderer({ content, className = "" }: MarkdownRendererProps) {
|
||||||
|
// Parse diff tools from content
|
||||||
|
const diffTools = parseDiffTools(content)
|
||||||
|
let processedContent = content
|
||||||
|
|
||||||
|
// Replace diff tool calls with placeholders
|
||||||
|
diffTools.forEach((tool, index) => {
|
||||||
|
processedContent = processedContent.replace(tool.match, `__DIFF_TOOL_${index}__`)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`prose prose-invert max-w-none ${className}`}>
|
||||||
|
<ReactMarkdown
|
||||||
|
remarkPlugins={[remarkGfm]}
|
||||||
|
rehypePlugins={[rehypeHighlight]}
|
||||||
|
components={{
|
||||||
|
// Custom component for diff tool placeholders
|
||||||
|
p: ({ children }) => {
|
||||||
|
const text = typeof children === 'string' ? children : children?.toString() || ''
|
||||||
|
const diffToolMatch = text.match(/^__DIFF_TOOL_(\d+)__$/)
|
||||||
|
|
||||||
|
if (diffToolMatch) {
|
||||||
|
const index = parseInt(diffToolMatch[1])
|
||||||
|
const tool = diffTools[index]
|
||||||
|
if (tool) {
|
||||||
|
return (
|
||||||
|
<DiffTool
|
||||||
|
oldCode={tool.props.oldCode}
|
||||||
|
newCode={tool.props.newCode}
|
||||||
|
title={tool.props.title}
|
||||||
|
language={tool.props.language}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p className="text-sm leading-relaxed text-neutral-100 mb-2 last:mb-0">
|
||||||
|
{children}
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
// Custom styling for different elements
|
||||||
|
h1: ({ children }) => (
|
||||||
|
<h1 className="text-xl font-bold text-white mb-3 mt-4 first:mt-0">
|
||||||
|
{children}
|
||||||
|
</h1>
|
||||||
|
),
|
||||||
|
h2: ({ children }) => (
|
||||||
|
<h2 className="text-lg font-semibold text-white mb-2 mt-3 first:mt-0">
|
||||||
|
{children}
|
||||||
|
</h2>
|
||||||
|
),
|
||||||
|
h3: ({ children }) => (
|
||||||
|
<h3 className="text-base font-semibold text-white mb-2 mt-3 first:mt-0">
|
||||||
|
{children}
|
||||||
|
</h3>
|
||||||
|
),
|
||||||
|
ul: ({ children }) => (
|
||||||
|
<ul className="list-disc list-inside text-sm text-neutral-100 mb-2 space-y-1">
|
||||||
|
{children}
|
||||||
|
</ul>
|
||||||
|
),
|
||||||
|
ol: ({ children }) => (
|
||||||
|
<ol className="list-decimal list-inside text-sm text-neutral-100 mb-2 space-y-1">
|
||||||
|
{children}
|
||||||
|
</ol>
|
||||||
|
),
|
||||||
|
li: ({ children }) => (
|
||||||
|
<li className="text-sm text-neutral-100">
|
||||||
|
{children}
|
||||||
|
</li>
|
||||||
|
),
|
||||||
|
code: ({ children, className }) => {
|
||||||
|
const isInline = !className
|
||||||
|
if (isInline) {
|
||||||
|
return (
|
||||||
|
<code className="bg-neutral-800 text-orange-400 px-1.5 py-0.5 rounded text-xs font-mono">
|
||||||
|
{children}
|
||||||
|
</code>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<code className={className}>
|
||||||
|
{children}
|
||||||
|
</code>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
pre: ({ children }) => (
|
||||||
|
<pre className="bg-neutral-900 border border-neutral-800 rounded-lg p-4 overflow-x-auto mb-3">
|
||||||
|
{children}
|
||||||
|
</pre>
|
||||||
|
),
|
||||||
|
blockquote: ({ children }) => (
|
||||||
|
<blockquote className="border-l-4 border-orange-500 pl-4 italic text-neutral-300 mb-3">
|
||||||
|
{children}
|
||||||
|
</blockquote>
|
||||||
|
),
|
||||||
|
a: ({ children, href }) => (
|
||||||
|
<a
|
||||||
|
href={href}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-orange-400 hover:text-orange-300 underline"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
),
|
||||||
|
strong: ({ children }) => (
|
||||||
|
<strong className="font-semibold text-white">
|
||||||
|
{children}
|
||||||
|
</strong>
|
||||||
|
),
|
||||||
|
em: ({ children }) => (
|
||||||
|
<em className="italic text-neutral-200">
|
||||||
|
{children}
|
||||||
|
</em>
|
||||||
|
),
|
||||||
|
table: ({ children }) => (
|
||||||
|
<div className="overflow-x-auto mb-3">
|
||||||
|
<table className="min-w-full border border-neutral-800 rounded-lg">
|
||||||
|
{children}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
thead: ({ children }) => (
|
||||||
|
<thead className="bg-neutral-800">
|
||||||
|
{children}
|
||||||
|
</thead>
|
||||||
|
),
|
||||||
|
tbody: ({ children }) => (
|
||||||
|
<tbody className="bg-neutral-900">
|
||||||
|
{children}
|
||||||
|
</tbody>
|
||||||
|
),
|
||||||
|
tr: ({ children }) => (
|
||||||
|
<tr className="border-b border-neutral-800">
|
||||||
|
{children}
|
||||||
|
</tr>
|
||||||
|
),
|
||||||
|
th: ({ children }) => (
|
||||||
|
<th className="px-4 py-2 text-left text-sm font-semibold text-white">
|
||||||
|
{children}
|
||||||
|
</th>
|
||||||
|
),
|
||||||
|
td: ({ children }) => (
|
||||||
|
<td className="px-4 py-2 text-sm text-neutral-100">
|
||||||
|
{children}
|
||||||
|
</td>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
41
inspiration-repo-agent/src/components/mode-toggle.tsx
Normal file
41
inspiration-repo-agent/src/components/mode-toggle.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { Moon, Sun } from "lucide-react"
|
||||||
|
import { useTheme } from "next-themes"
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu"
|
||||||
|
|
||||||
|
export function ModeToggle() {
|
||||||
|
const { setTheme } = useTheme()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" size="icon">
|
||||||
|
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||||
|
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||||
|
<span className="sr-only">Toggle theme</span>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem onClick={() => setTheme("light")}>
|
||||||
|
Light
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => setTheme("dark")}>
|
||||||
|
Dark
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => setTheme("system")}>
|
||||||
|
System
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
12
inspiration-repo-agent/src/components/theme-provider.tsx
Normal file
12
inspiration-repo-agent/src/components/theme-provider.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { ThemeProvider as NextThemesProvider } from "next-themes"
|
||||||
|
|
||||||
|
export function ThemeProvider({
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof NextThemesProvider>) {
|
||||||
|
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
||||||
|
}
|
||||||
|
|
||||||
201
inspiration-repo-agent/src/components/ui/dropdown-menu.tsx
Normal file
201
inspiration-repo-agent/src/components/ui/dropdown-menu.tsx
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||||
|
import { Check, ChevronRight, Circle } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const DropdownMenu = DropdownMenuPrimitive.Root
|
||||||
|
|
||||||
|
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
||||||
|
|
||||||
|
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
||||||
|
|
||||||
|
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
||||||
|
|
||||||
|
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
||||||
|
|
||||||
|
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
||||||
|
|
||||||
|
const DropdownMenuSubTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||||
|
inset?: boolean
|
||||||
|
}
|
||||||
|
>(({ className, inset, children, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||||
|
inset && "pl-8",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronRight className="ml-auto" />
|
||||||
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
|
))
|
||||||
|
DropdownMenuSubTrigger.displayName =
|
||||||
|
DropdownMenuPrimitive.SubTrigger.displayName
|
||||||
|
|
||||||
|
const DropdownMenuSubContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.SubContent
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DropdownMenuSubContent.displayName =
|
||||||
|
DropdownMenuPrimitive.SubContent.displayName
|
||||||
|
|
||||||
|
const DropdownMenuContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||||
|
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Portal>
|
||||||
|
<DropdownMenuPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</DropdownMenuPrimitive.Portal>
|
||||||
|
))
|
||||||
|
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const DropdownMenuItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||||
|
inset?: boolean
|
||||||
|
}
|
||||||
|
>(({ className, inset, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||||
|
inset && "pl-8",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
||||||
|
|
||||||
|
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||||
|
>(({ className, children, checked, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
checked={checked}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.CheckboxItem>
|
||||||
|
))
|
||||||
|
DropdownMenuCheckboxItem.displayName =
|
||||||
|
DropdownMenuPrimitive.CheckboxItem.displayName
|
||||||
|
|
||||||
|
const DropdownMenuRadioItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.RadioItem
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<Circle className="h-2 w-2 fill-current" />
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.RadioItem>
|
||||||
|
))
|
||||||
|
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
||||||
|
|
||||||
|
const DropdownMenuLabel = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||||
|
inset?: boolean
|
||||||
|
}
|
||||||
|
>(({ className, inset, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Label
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"px-2 py-1.5 text-sm font-semibold",
|
||||||
|
inset && "pl-8",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
||||||
|
|
||||||
|
const DropdownMenuSeparator = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Separator
|
||||||
|
ref={ref}
|
||||||
|
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
||||||
|
|
||||||
|
const DropdownMenuShortcut = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
||||||
|
|
||||||
|
export {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuRadioItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuShortcut,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuPortal,
|
||||||
|
DropdownMenuSub,
|
||||||
|
DropdownMenuSubContent,
|
||||||
|
DropdownMenuSubTrigger,
|
||||||
|
DropdownMenuRadioGroup,
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
x
Reference in New Issue
Block a user