mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-01-02 14:22:28 +08:00
feat: add WebSearchToolUI component and input UI, integrate Radix UI scroll area
This commit is contained in:
71
app/page.tsx
71
app/page.tsx
@@ -1,43 +1,38 @@
|
||||
"use client";
|
||||
import { AssistantRuntimeProvider } from "@assistant-ui/react";
|
||||
import { useChatRuntime } from "@assistant-ui/react-ai-sdk";
|
||||
import { DrawIoEmbed, DrawIoEmbedRef } from "react-drawio";
|
||||
|
||||
import { Thread } from "@/components/assistant-ui/thread";
|
||||
import { useRef, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { extractDiagramXML } from "./extract_xml"
|
||||
export default function Home() {
|
||||
const runtime = useChatRuntime({
|
||||
api: "/api/chat",
|
||||
});
|
||||
import { extractDiagramXML } from "./extract_xml";
|
||||
import ChatPanel from "@/components/chatPanel";
|
||||
|
||||
export default function Home() {
|
||||
const drawioRef = useRef<DrawIoEmbedRef>(null);
|
||||
const [imgData, setImgData] = useState<string | null>(null);
|
||||
const [chartXML, setChartXML] = useState<string>("");
|
||||
const [diagram, setDiagram] = useState<string>("");
|
||||
// const handleExport = () => {};
|
||||
const handleExport = () => {
|
||||
// use this function to export the diagramxml from the drawio editor
|
||||
if (drawioRef.current) {
|
||||
drawioRef.current.exportDiagram({
|
||||
format: "xmlsvg",
|
||||
});
|
||||
}
|
||||
};
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const loadDiagram = (chart: string) => {
|
||||
// use this function to display the diagramxml in the drawio editor
|
||||
if (drawioRef.current) {
|
||||
drawioRef.current.load({
|
||||
xml: diagram,
|
||||
xml: chart,
|
||||
});
|
||||
}
|
||||
};
|
||||
console.log("imgData", imgData);
|
||||
return (
|
||||
<AssistantRuntimeProvider runtime={runtime}>
|
||||
<div className="grid h-dvh grid-cols-[1fr_400px] gap-x-2 px-4 py-4">
|
||||
<div className="flex h-screen bg-gray-100">
|
||||
<div className="w-2/3 p-1">
|
||||
<DrawIoEmbed
|
||||
ref={drawioRef}
|
||||
onExport={(data) => setImgData(data.data)}
|
||||
onExport={(data) => setChartXML(extractDiagramXML(data.data))}
|
||||
urlParameters={{
|
||||
// ui: "kennedy",
|
||||
spin: true,
|
||||
@@ -46,48 +41,10 @@ export default function Home() {
|
||||
noExitBtn: true,
|
||||
}}
|
||||
/>
|
||||
{/* <Thread /> */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<form>
|
||||
<div className="mb-4">
|
||||
<label htmlFor="diagramXml" className="block text-sm font-medium mb-1">
|
||||
Diagram XML
|
||||
</label>
|
||||
<textarea
|
||||
id="diagramXml"
|
||||
className="w-full p-2 border border-gray-300 rounded-md min-h-[200px]"
|
||||
placeholder="Paste your diagram XML here..."
|
||||
value={diagram || ''}
|
||||
onChange={(e) => setDiagram(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button onClick={handleSubmit}>submit</Button>
|
||||
</form>
|
||||
<div>
|
||||
<div className="bg-amber-300 w-full h-[400px]">{imgData &&
|
||||
<>
|
||||
{/* <img src={imgData} /> */}
|
||||
<div className="bg-blue-100 h-[400px] p-4 overflow-auto">
|
||||
<h1 className="font-semibold mb-2">Extracted XML</h1>
|
||||
{(() => {
|
||||
try {
|
||||
const extractedXml = extractDiagramXML(imgData);
|
||||
return <pre className="whitespace-pre-wrap text-sm">{extractedXml}</pre>;
|
||||
} catch (error) {
|
||||
return (
|
||||
<div className="text-red-600">
|
||||
Error extracting XML: {error instanceof Error ? error.message : 'Unknown error'}
|
||||
<div className="w-1/3 p-1 border-gray-300 ">
|
||||
<ChatPanel />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})()}
|
||||
</div>
|
||||
</>}
|
||||
<Button onClick={handleExport}>Export</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AssistantRuntimeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
87
components/chatPanel.tsx
Normal file
87
components/chatPanel.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
|
||||
import { useRef, useEffect } from "react"
|
||||
import { useChat } from "@ai-sdk/react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||
import { Loader2, Send } from "lucide-react"
|
||||
|
||||
|
||||
export default function ChatPanel() {
|
||||
const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({
|
||||
api: "/api/chat",
|
||||
maxSteps: 5, // Allow multiple steps for complex diagram generation
|
||||
})
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Scroll to bottom when messages change
|
||||
useEffect(() => {
|
||||
if (messagesEndRef.current) {
|
||||
messagesEndRef.current.scrollIntoView({ behavior: "smooth" })
|
||||
}
|
||||
}, [messages])
|
||||
|
||||
|
||||
const onFormSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
if (input.trim()) {
|
||||
handleSubmit(e)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="h-full flex flex-col">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle>Chat with Diagram Generator</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-grow overflow-hidden p-4">
|
||||
<ScrollArea className="h-full pr-4">
|
||||
{messages.length === 0 ? (
|
||||
<div className="text-center text-gray-500 mt-8">
|
||||
<p>Start a conversation to generate a diagram.</p>
|
||||
<p className="text-sm mt-2">Try: "Create a flowchart for user authentication"</p>
|
||||
</div>
|
||||
) : (
|
||||
messages.map((message) => (
|
||||
<div key={message.id} className={`mb-4 ${message.role === "user" ? "text-right" : "text-left"}`}>
|
||||
<div
|
||||
className={`inline-block px-4 py-2 rounded-lg max-w-[85%] break-words ${message.role === "user" ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
{message.content}
|
||||
</div>
|
||||
{message.toolInvocations?.map((tool, index) => (
|
||||
<div key={index} className="mt-2 text-left">
|
||||
<div className="text-xs text-gray-500">
|
||||
{tool.state === "call" ? "Generating diagram..." : "Diagram generated"}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
<CardFooter className="pt-2">
|
||||
<form onSubmit={onFormSubmit} className="w-full flex space-x-2">
|
||||
<Input
|
||||
value={input}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Describe the diagram you want to create..."
|
||||
disabled={isLoading}
|
||||
className="flex-grow"
|
||||
/>
|
||||
<Button type="submit" disabled={isLoading || !input.trim()}>
|
||||
{isLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Send className="h-4 w-4" />}
|
||||
</Button>
|
||||
</form>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
92
components/ui/card.tsx
Normal file
92
components/ui/card.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-[data-slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn("leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
}
|
||||
21
components/ui/input.tsx
Normal file
21
components/ui/input.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Input }
|
||||
58
components/ui/scroll-area.tsx
Normal file
58
components/ui/scroll-area.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function ScrollArea({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.Root
|
||||
data-slot="scroll-area"
|
||||
className={cn("relative", className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport
|
||||
data-slot="scroll-area-viewport"
|
||||
className="ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] focus-visible:ring-4 focus-visible:outline-1"
|
||||
>
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
function ScrollBar({
|
||||
className,
|
||||
orientation = "vertical",
|
||||
...props
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
data-slot="scroll-area-scrollbar"
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"flex touch-none p-px transition-colors select-none",
|
||||
orientation === "vertical" &&
|
||||
"h-full w-2.5 border-l border-l-transparent",
|
||||
orientation === "horizontal" &&
|
||||
"h-2.5 flex-col border-t border-t-transparent",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb
|
||||
data-slot="scroll-area-thumb"
|
||||
className="bg-border relative flex-1 rounded-full"
|
||||
/>
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
)
|
||||
}
|
||||
|
||||
export { ScrollArea, ScrollBar }
|
||||
21
components/webSearchToolUI.tsx
Normal file
21
components/webSearchToolUI.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { makeAssistantToolUI } from "@assistant-ui/react";
|
||||
|
||||
type WebSearchArgs = {
|
||||
query: string;
|
||||
};
|
||||
|
||||
type WebSearchResult = {
|
||||
title: string;
|
||||
description: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export const WebSearchToolUI = makeAssistantToolUI<
|
||||
WebSearchArgs,
|
||||
WebSearchResult
|
||||
>({
|
||||
toolName: "web_search",
|
||||
render: ({ args, status }) => {
|
||||
return <p>web_search({args.query}) </p>;
|
||||
},
|
||||
});
|
||||
53
package-lock.json
generated
53
package-lock.json
generated
@@ -13,6 +13,7 @@
|
||||
"@assistant-ui/react": "^0.8.6",
|
||||
"@assistant-ui/react-ai-sdk": "^0.8.0",
|
||||
"@assistant-ui/react-markdown": "^0.8.0",
|
||||
"@radix-ui/react-scroll-area": "^1.2.3",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"@xmldom/xmldom": "^0.9.8",
|
||||
@@ -532,6 +533,12 @@
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/number": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz",
|
||||
"integrity": "sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@radix-ui/primitive": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz",
|
||||
@@ -591,6 +598,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-direction": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz",
|
||||
"integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dismissable-layer": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz",
|
||||
@@ -816,6 +838,37 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-scroll-area": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.3.tgz",
|
||||
"integrity": "sha512-l7+NNBfBYYJa9tNqVcP2AGvxdE3lmE6kFTBXdvHgUaZuy+4wGCL1Cl2AfaR7RKyimj7lZURGLwFO59k4eBnDJQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/number": "1.1.0",
|
||||
"@radix-ui/primitive": "1.1.1",
|
||||
"@radix-ui/react-compose-refs": "1.1.1",
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-direction": "1.1.0",
|
||||
"@radix-ui/react-presence": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.0.2",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"@assistant-ui/react": "^0.8.6",
|
||||
"@assistant-ui/react-ai-sdk": "^0.8.0",
|
||||
"@assistant-ui/react-markdown": "^0.8.0",
|
||||
"@radix-ui/react-scroll-area": "^1.2.3",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"@xmldom/xmldom": "^0.9.8",
|
||||
|
||||
Reference in New Issue
Block a user