mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-01-03 23:02:31 +08:00
Compare commits
9 Commits
fix/hydrat
...
fix/bug-fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6931077f4 | ||
|
|
46567cb0b8 | ||
|
|
9f77199272 | ||
|
|
77f2569a3b | ||
|
|
cbb92bd636 | ||
|
|
8d898d8adc | ||
|
|
1e0b1ed970 | ||
|
|
1d03d10ba8 | ||
|
|
e893bd60f9 |
35
CONTRIBUTING.md
Normal file
35
CONTRIBUTING.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Contributing
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/YOUR_USERNAME/next-ai-draw-io.git
|
||||||
|
cd next-ai-draw-io
|
||||||
|
npm install
|
||||||
|
cp env.example .env.local
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code Style
|
||||||
|
|
||||||
|
We use [Biome](https://biomejs.dev/) for linting and formatting:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run format # Format code
|
||||||
|
npm run lint # Check lint errors
|
||||||
|
npm run check # Run all checks (CI)
|
||||||
|
```
|
||||||
|
|
||||||
|
Pre-commit hooks via Husky will run Biome automatically on staged files.
|
||||||
|
|
||||||
|
For a better experience, install the [Biome VS Code extension](https://marketplace.visualstudio.com/items?itemName=biomejs.biome) for real-time linting and format-on-save.
|
||||||
|
|
||||||
|
## Pull Requests
|
||||||
|
|
||||||
|
1. Create a feature branch
|
||||||
|
2. Make changes and ensure `npm run check` passes
|
||||||
|
3. Submit PR against `main` with a clear description
|
||||||
|
|
||||||
|
## Issues
|
||||||
|
|
||||||
|
Include steps to reproduce, expected vs actual behavior, and AI provider used.
|
||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
} from "@/lib/langfuse"
|
} from "@/lib/langfuse"
|
||||||
import { getSystemPrompt } from "@/lib/system-prompts"
|
import { getSystemPrompt } from "@/lib/system-prompts"
|
||||||
|
|
||||||
export const maxDuration = 300
|
export const maxDuration = 60
|
||||||
|
|
||||||
// File upload limits (must match client-side)
|
// File upload limits (must match client-side)
|
||||||
const MAX_FILE_SIZE = 2 * 1024 * 1024 // 2MB
|
const MAX_FILE_SIZE = 2 * 1024 * 1024 // 2MB
|
||||||
@@ -40,7 +40,7 @@ function validateFileParts(messages: any[]): {
|
|||||||
for (const filePart of fileParts) {
|
for (const filePart of fileParts) {
|
||||||
// Data URLs format: data:image/png;base64,<data>
|
// Data URLs format: data:image/png;base64,<data>
|
||||||
// Base64 increases size by ~33%, so we check the decoded size
|
// Base64 increases size by ~33%, so we check the decoded size
|
||||||
if (filePart.url && filePart.url.startsWith("data:")) {
|
if (filePart.url?.startsWith("data:")) {
|
||||||
const base64Data = filePart.url.split(",")[1]
|
const base64Data = filePart.url.split(",")[1]
|
||||||
if (base64Data) {
|
if (base64Data) {
|
||||||
const sizeInBytes = Math.ceil((base64Data.length * 3) / 4)
|
const sizeInBytes = Math.ceil((base64Data.length * 3) / 4)
|
||||||
|
|||||||
32
app/api/verify-access-code/route.ts
Normal file
32
app/api/verify-access-code/route.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
export async function POST(req: Request) {
|
||||||
|
const accessCodes =
|
||||||
|
process.env.ACCESS_CODE_LIST?.split(",")
|
||||||
|
.map((code) => code.trim())
|
||||||
|
.filter(Boolean) || []
|
||||||
|
|
||||||
|
// If no access codes configured, verification always passes
|
||||||
|
if (accessCodes.length === 0) {
|
||||||
|
return Response.json({
|
||||||
|
valid: true,
|
||||||
|
message: "No access code required",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessCodeHeader = req.headers.get("x-access-code")
|
||||||
|
|
||||||
|
if (!accessCodeHeader) {
|
||||||
|
return Response.json(
|
||||||
|
{ valid: false, message: "Access code is required" },
|
||||||
|
{ status: 401 },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!accessCodes.includes(accessCodeHeader)) {
|
||||||
|
return Response.json(
|
||||||
|
{ valid: false, message: "Invalid access code" },
|
||||||
|
{ status: 401 },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.json({ valid: true, message: "Access code is valid" })
|
||||||
|
}
|
||||||
61
app/page.tsx
61
app/page.tsx
@@ -1,8 +1,9 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import React, { useEffect, useRef, useState } from "react"
|
import { useEffect, useRef, useState } from "react"
|
||||||
import { DrawIoEmbed } from "react-drawio"
|
import { DrawIoEmbed } from "react-drawio"
|
||||||
import type { ImperativePanelHandle } from "react-resizable-panels"
|
import type { ImperativePanelHandle } from "react-resizable-panels"
|
||||||
import ChatPanel from "@/components/chat-panel"
|
import ChatPanel from "@/components/chat-panel"
|
||||||
|
import { STORAGE_CLOSE_PROTECTION_KEY } from "@/components/settings-dialog"
|
||||||
import {
|
import {
|
||||||
ResizableHandle,
|
ResizableHandle,
|
||||||
ResizablePanel,
|
ResizablePanel,
|
||||||
@@ -14,17 +15,20 @@ export default function Home() {
|
|||||||
const { drawioRef, handleDiagramExport } = useDiagram()
|
const { drawioRef, handleDiagramExport } = useDiagram()
|
||||||
const [isMobile, setIsMobile] = useState(false)
|
const [isMobile, setIsMobile] = useState(false)
|
||||||
const [isChatVisible, setIsChatVisible] = useState(true)
|
const [isChatVisible, setIsChatVisible] = useState(true)
|
||||||
const [drawioUi, setDrawioUi] = useState<"min" | "sketch">("min")
|
const [drawioUi, setDrawioUi] = useState<"min" | "sketch">(() => {
|
||||||
const [isThemeLoaded, setIsThemeLoaded] = useState(false)
|
if (typeof window !== "undefined") {
|
||||||
|
const saved = localStorage.getItem("drawio-theme")
|
||||||
// Load theme from localStorage after mount to avoid hydration mismatch
|
if (saved === "min" || saved === "sketch") return saved
|
||||||
useEffect(() => {
|
|
||||||
const saved = localStorage.getItem("drawio-theme")
|
|
||||||
if (saved === "min" || saved === "sketch") {
|
|
||||||
setDrawioUi(saved)
|
|
||||||
}
|
}
|
||||||
setIsThemeLoaded(true)
|
return "min"
|
||||||
}, [])
|
})
|
||||||
|
const [closeProtection, setCloseProtection] = useState(() => {
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
const saved = localStorage.getItem(STORAGE_CLOSE_PROTECTION_KEY)
|
||||||
|
return saved !== "false" // Default to true
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
const chatPanelRef = useRef<ImperativePanelHandle>(null)
|
const chatPanelRef = useRef<ImperativePanelHandle>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -65,6 +69,8 @@ export default function Home() {
|
|||||||
// Show confirmation dialog when user tries to leave the page
|
// Show confirmation dialog when user tries to leave the page
|
||||||
// This helps prevent accidental navigation from browser back gestures
|
// This helps prevent accidental navigation from browser back gestures
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!closeProtection) return
|
||||||
|
|
||||||
const handleBeforeUnload = (event: BeforeUnloadEvent) => {
|
const handleBeforeUnload = (event: BeforeUnloadEvent) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
return ""
|
return ""
|
||||||
@@ -73,7 +79,7 @@ export default function Home() {
|
|||||||
window.addEventListener("beforeunload", handleBeforeUnload)
|
window.addEventListener("beforeunload", handleBeforeUnload)
|
||||||
return () =>
|
return () =>
|
||||||
window.removeEventListener("beforeunload", handleBeforeUnload)
|
window.removeEventListener("beforeunload", handleBeforeUnload)
|
||||||
}, [])
|
}, [closeProtection])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen bg-background relative overflow-hidden">
|
<div className="h-screen bg-background relative overflow-hidden">
|
||||||
@@ -90,24 +96,18 @@ export default function Home() {
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="h-full rounded-xl overflow-hidden shadow-soft-lg border border-border/30 bg-white">
|
<div className="h-full rounded-xl overflow-hidden shadow-soft-lg border border-border/30 bg-white">
|
||||||
{isThemeLoaded ? (
|
<DrawIoEmbed
|
||||||
<DrawIoEmbed
|
key={drawioUi}
|
||||||
key={drawioUi}
|
ref={drawioRef}
|
||||||
ref={drawioRef}
|
onExport={handleDiagramExport}
|
||||||
onExport={handleDiagramExport}
|
urlParameters={{
|
||||||
urlParameters={{
|
ui: drawioUi,
|
||||||
ui: drawioUi,
|
spin: true,
|
||||||
spin: true,
|
libraries: false,
|
||||||
libraries: false,
|
saveAndExit: false,
|
||||||
saveAndExit: false,
|
noExitBtn: true,
|
||||||
noExitBtn: true,
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="h-full w-full flex items-center justify-center">
|
|
||||||
<div className="animate-spin h-8 w-8 border-4 border-primary border-t-transparent rounded-full" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
@@ -137,6 +137,7 @@ export default function Home() {
|
|||||||
setDrawioUi(newTheme)
|
setDrawioUi(newTheme)
|
||||||
}}
|
}}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
|
onCloseProtectionChange={setCloseProtection}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
|
|||||||
24
biome.json
24
biome.json
@@ -19,6 +19,30 @@
|
|||||||
"recommended": true,
|
"recommended": true,
|
||||||
"complexity": {
|
"complexity": {
|
||||||
"noImportantStyles": "off"
|
"noImportantStyles": "off"
|
||||||
|
},
|
||||||
|
"suspicious": {
|
||||||
|
"noExplicitAny": "off",
|
||||||
|
"noArrayIndexKey": "off",
|
||||||
|
"noImplicitAnyLet": "off",
|
||||||
|
"noAssignInExpressions": "off"
|
||||||
|
},
|
||||||
|
"a11y": {
|
||||||
|
"useButtonType": "off",
|
||||||
|
"noAutofocus": "off",
|
||||||
|
"noStaticElementInteractions": "off",
|
||||||
|
"useKeyWithClickEvents": "off",
|
||||||
|
"noLabelWithoutControl": "off",
|
||||||
|
"noNoninteractiveTabindex": "off"
|
||||||
|
},
|
||||||
|
"correctness": {
|
||||||
|
"useExhaustiveDependencies": "off"
|
||||||
|
},
|
||||||
|
"style": {
|
||||||
|
"useNodejsImportProtocol": "off",
|
||||||
|
"useTemplate": "off"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"noDangerouslySetInnerHtml": "off"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -99,8 +99,8 @@ function showValidationErrors(errors: string[]) {
|
|||||||
{errors.length} files rejected:
|
{errors.length} files rejected:
|
||||||
</span>
|
</span>
|
||||||
<ul className="text-muted-foreground text-xs list-disc list-inside">
|
<ul className="text-muted-foreground text-xs list-disc list-inside">
|
||||||
{errors.slice(0, 3).map((err, i) => (
|
{errors.slice(0, 3).map((err) => (
|
||||||
<li key={i}>{err}</li>
|
<li key={err}>{err}</li>
|
||||||
))}
|
))}
|
||||||
{errors.length > 3 && (
|
{errors.length > 3 && (
|
||||||
<li>...and {errors.length - 3} more</li>
|
<li>...and {errors.length - 3} more</li>
|
||||||
@@ -162,10 +162,16 @@ export function ChatInput({
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// Handle programmatic input changes (e.g., setInput("") after form submission)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
adjustTextareaHeight()
|
adjustTextareaHeight()
|
||||||
}, [input, adjustTextareaHeight])
|
}, [input, adjustTextareaHeight])
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
onChange(e)
|
||||||
|
adjustTextareaHeight()
|
||||||
|
}
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
|
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -297,7 +303,7 @@ export function ChatInput({
|
|||||||
<Textarea
|
<Textarea
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
value={input}
|
value={input}
|
||||||
onChange={onChange}
|
onChange={handleChange}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onPaste={handlePaste}
|
onPaste={handlePaste}
|
||||||
placeholder="Describe your diagram or paste an image..."
|
placeholder="Describe your diagram or paste an image..."
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import type { UIMessage } from "ai"
|
import type { UIMessage } from "ai"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Check,
|
Check,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
@@ -32,12 +33,21 @@ interface EditPair {
|
|||||||
replace: string
|
replace: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tool part interface for type safety
|
||||||
|
interface ToolPartLike {
|
||||||
|
type: string
|
||||||
|
toolCallId: string
|
||||||
|
state?: string
|
||||||
|
input?: { xml?: string; edits?: EditPair[] } & Record<string, unknown>
|
||||||
|
output?: string
|
||||||
|
}
|
||||||
|
|
||||||
function EditDiffDisplay({ edits }: { edits: EditPair[] }) {
|
function EditDiffDisplay({ edits }: { edits: EditPair[] }) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{edits.map((edit, index) => (
|
{edits.map((edit, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={`${(edit.search || "").slice(0, 50)}-${(edit.replace || "").slice(0, 50)}-${index}`}
|
||||||
className="rounded-lg border border-border/50 overflow-hidden bg-background/50"
|
className="rounded-lg border border-border/50 overflow-hidden bg-background/50"
|
||||||
>
|
>
|
||||||
<div className="px-3 py-1.5 bg-muted/40 border-b border-border/30 flex items-center gap-2">
|
<div className="px-3 py-1.5 bg-muted/40 border-b border-border/30 flex items-center gap-2">
|
||||||
@@ -82,8 +92,8 @@ import { useDiagram } from "@/contexts/diagram-context"
|
|||||||
const getMessageTextContent = (message: UIMessage): string => {
|
const getMessageTextContent = (message: UIMessage): string => {
|
||||||
if (!message.parts) return ""
|
if (!message.parts) return ""
|
||||||
return message.parts
|
return message.parts
|
||||||
.filter((part: any) => part.type === "text")
|
.filter((part) => part.type === "text")
|
||||||
.map((part: any) => part.text)
|
.map((part) => (part as { text: string }).text)
|
||||||
.join("\n")
|
.join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,6 +129,7 @@ export function ChatMessageDisplay({
|
|||||||
const [editingMessageId, setEditingMessageId] = useState<string | null>(
|
const [editingMessageId, setEditingMessageId] = useState<string | null>(
|
||||||
null,
|
null,
|
||||||
)
|
)
|
||||||
|
const editTextareaRef = useRef<HTMLTextAreaElement>(null)
|
||||||
const [editText, setEditText] = useState<string>("")
|
const [editText, setEditText] = useState<string>("")
|
||||||
|
|
||||||
const copyMessageToClipboard = async (messageId: string, text: string) => {
|
const copyMessageToClipboard = async (messageId: string, text: string) => {
|
||||||
@@ -166,7 +177,10 @@ export function ChatMessageDisplay({
|
|||||||
const currentXml = xml || ""
|
const currentXml = xml || ""
|
||||||
const convertedXml = convertToLegalXml(currentXml)
|
const convertedXml = convertToLegalXml(currentXml)
|
||||||
if (convertedXml !== previousXML.current) {
|
if (convertedXml !== previousXML.current) {
|
||||||
const replacedXML = replaceNodes(chartXML, convertedXml)
|
// If chartXML is empty, use the converted XML directly
|
||||||
|
const replacedXML = chartXML
|
||||||
|
? replaceNodes(chartXML, convertedXml)
|
||||||
|
: convertedXml
|
||||||
|
|
||||||
const validationError = validateMxCellStructure(replacedXML)
|
const validationError = validateMxCellStructure(replacedXML)
|
||||||
if (!validationError) {
|
if (!validationError) {
|
||||||
@@ -189,12 +203,19 @@ export function ChatMessageDisplay({
|
|||||||
}
|
}
|
||||||
}, [messages])
|
}, [messages])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (editingMessageId && editTextareaRef.current) {
|
||||||
|
editTextareaRef.current.focus()
|
||||||
|
}
|
||||||
|
}, [editingMessageId])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
messages.forEach((message) => {
|
messages.forEach((message) => {
|
||||||
if (message.parts) {
|
if (message.parts) {
|
||||||
message.parts.forEach((part: any) => {
|
message.parts.forEach((part) => {
|
||||||
if (part.type?.startsWith("tool-")) {
|
if (part.type?.startsWith("tool-")) {
|
||||||
const { toolCallId, state } = part
|
const toolPart = part as ToolPartLike
|
||||||
|
const { toolCallId, state, input } = toolPart
|
||||||
|
|
||||||
if (state === "output-available") {
|
if (state === "output-available") {
|
||||||
setExpandedTools((prev) => ({
|
setExpandedTools((prev) => ({
|
||||||
@@ -205,18 +226,19 @@ export function ChatMessageDisplay({
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
part.type === "tool-display_diagram" &&
|
part.type === "tool-display_diagram" &&
|
||||||
part.input?.xml
|
input?.xml
|
||||||
) {
|
) {
|
||||||
|
const xml = input.xml as string
|
||||||
if (
|
if (
|
||||||
state === "input-streaming" ||
|
state === "input-streaming" ||
|
||||||
state === "input-available"
|
state === "input-available"
|
||||||
) {
|
) {
|
||||||
handleDisplayChart(part.input.xml)
|
handleDisplayChart(xml)
|
||||||
} else if (
|
} else if (
|
||||||
state === "output-available" &&
|
state === "output-available" &&
|
||||||
!processedToolCalls.current.has(toolCallId)
|
!processedToolCalls.current.has(toolCallId)
|
||||||
) {
|
) {
|
||||||
handleDisplayChart(part.input.xml)
|
handleDisplayChart(xml)
|
||||||
processedToolCalls.current.add(toolCallId)
|
processedToolCalls.current.add(toolCallId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -226,7 +248,7 @@ export function ChatMessageDisplay({
|
|||||||
})
|
})
|
||||||
}, [messages, handleDisplayChart])
|
}, [messages, handleDisplayChart])
|
||||||
|
|
||||||
const renderToolPart = (part: any) => {
|
const renderToolPart = (part: ToolPartLike) => {
|
||||||
const callId = part.toolCallId
|
const callId = part.toolCallId
|
||||||
const { state, input, output } = part
|
const { state, input, output } = part
|
||||||
const isExpanded = expandedTools[callId] ?? true
|
const isExpanded = expandedTools[callId] ?? true
|
||||||
@@ -280,6 +302,7 @@ export function ChatMessageDisplay({
|
|||||||
)}
|
)}
|
||||||
{input && Object.keys(input).length > 0 && (
|
{input && Object.keys(input).length > 0 && (
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={toggleExpanded}
|
onClick={toggleExpanded}
|
||||||
className="p-1 rounded hover:bg-muted transition-colors"
|
className="p-1 rounded hover:bg-muted transition-colors"
|
||||||
>
|
>
|
||||||
@@ -358,6 +381,7 @@ export function ChatMessageDisplay({
|
|||||||
{onEditMessage &&
|
{onEditMessage &&
|
||||||
isLastUserMessage && (
|
isLastUserMessage && (
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEditingMessageId(
|
setEditingMessageId(
|
||||||
message.id,
|
message.id,
|
||||||
@@ -373,6 +397,7 @@ export function ChatMessageDisplay({
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
copyMessageToClipboard(
|
copyMessageToClipboard(
|
||||||
message.id,
|
message.id,
|
||||||
@@ -407,6 +432,7 @@ export function ChatMessageDisplay({
|
|||||||
{isEditing && message.role === "user" ? (
|
{isEditing && message.role === "user" ? (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<textarea
|
<textarea
|
||||||
|
ref={editTextareaRef}
|
||||||
value={editText}
|
value={editText}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setEditText(e.target.value)
|
setEditText(e.target.value)
|
||||||
@@ -417,7 +443,6 @@ export function ChatMessageDisplay({
|
|||||||
.length + 1,
|
.length + 1,
|
||||||
6,
|
6,
|
||||||
)}
|
)}
|
||||||
autoFocus
|
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Escape") {
|
if (e.key === "Escape") {
|
||||||
setEditingMessageId(
|
setEditingMessageId(
|
||||||
@@ -447,6 +472,7 @@ export function ChatMessageDisplay({
|
|||||||
/>
|
/>
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEditingMessageId(
|
setEditingMessageId(
|
||||||
null,
|
null,
|
||||||
@@ -458,6 +484,7 @@ export function ChatMessageDisplay({
|
|||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (
|
if (
|
||||||
editText.trim() &&
|
editText.trim() &&
|
||||||
@@ -483,7 +510,7 @@ export function ChatMessageDisplay({
|
|||||||
) : (
|
) : (
|
||||||
/* Text content in bubble */
|
/* Text content in bubble */
|
||||||
message.parts?.some(
|
message.parts?.some(
|
||||||
(part: any) =>
|
(part) =>
|
||||||
part.type === "text" ||
|
part.type === "text" ||
|
||||||
part.type === "file",
|
part.type === "file",
|
||||||
) && (
|
) && (
|
||||||
@@ -496,6 +523,20 @@ export function ChatMessageDisplay({
|
|||||||
? "bg-destructive/10 text-destructive border border-destructive/20 rounded-2xl rounded-bl-md"
|
? "bg-destructive/10 text-destructive border border-destructive/20 rounded-2xl rounded-bl-md"
|
||||||
: "bg-muted/60 text-foreground rounded-2xl rounded-bl-md"
|
: "bg-muted/60 text-foreground rounded-2xl rounded-bl-md"
|
||||||
} ${message.role === "user" && isLastUserMessage && onEditMessage ? "cursor-pointer hover:opacity-90 transition-opacity" : ""}`}
|
} ${message.role === "user" && isLastUserMessage && onEditMessage ? "cursor-pointer hover:opacity-90 transition-opacity" : ""}`}
|
||||||
|
role={
|
||||||
|
message.role === "user" &&
|
||||||
|
isLastUserMessage &&
|
||||||
|
onEditMessage
|
||||||
|
? "button"
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
tabIndex={
|
||||||
|
message.role === "user" &&
|
||||||
|
isLastUserMessage &&
|
||||||
|
onEditMessage
|
||||||
|
? 0
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (
|
if (
|
||||||
message.role ===
|
message.role ===
|
||||||
@@ -511,6 +552,24 @@ export function ChatMessageDisplay({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (
|
||||||
|
(e.key === "Enter" ||
|
||||||
|
e.key === " ") &&
|
||||||
|
message.role ===
|
||||||
|
"user" &&
|
||||||
|
isLastUserMessage &&
|
||||||
|
onEditMessage
|
||||||
|
) {
|
||||||
|
e.preventDefault()
|
||||||
|
setEditingMessageId(
|
||||||
|
message.id,
|
||||||
|
)
|
||||||
|
setEditText(
|
||||||
|
userMessageText,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}}
|
||||||
title={
|
title={
|
||||||
message.role === "user" &&
|
message.role === "user" &&
|
||||||
isLastUserMessage &&
|
isLastUserMessage &&
|
||||||
@@ -520,17 +579,12 @@ export function ChatMessageDisplay({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{message.parts?.map(
|
{message.parts?.map(
|
||||||
(
|
(part, index) => {
|
||||||
part: any,
|
|
||||||
index: number,
|
|
||||||
) => {
|
|
||||||
switch (part.type) {
|
switch (part.type) {
|
||||||
case "text":
|
case "text":
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={
|
key={`${message.id}-text-${index}`}
|
||||||
index
|
|
||||||
}
|
|
||||||
className={`prose prose-sm max-w-none break-words [&>*:first-child]:mt-0 [&>*:last-child]:mb-0 ${
|
className={`prose prose-sm max-w-none break-words [&>*:first-child]:mt-0 [&>*:last-child]:mb-0 ${
|
||||||
message.role ===
|
message.role ===
|
||||||
"user"
|
"user"
|
||||||
@@ -548,9 +602,7 @@ export function ChatMessageDisplay({
|
|||||||
case "file":
|
case "file":
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={
|
key={`${message.id}-file-${part.url}`}
|
||||||
index
|
|
||||||
}
|
|
||||||
className="mt-2"
|
className="mt-2"
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
@@ -581,9 +633,11 @@ export function ChatMessageDisplay({
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
{/* Tool calls outside bubble */}
|
{/* Tool calls outside bubble */}
|
||||||
{message.parts?.map((part: any) => {
|
{message.parts?.map((part) => {
|
||||||
if (part.type?.startsWith("tool-")) {
|
if (part.type?.startsWith("tool-")) {
|
||||||
return renderToolPart(part)
|
return renderToolPart(
|
||||||
|
part as ToolPartLike,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
})}
|
})}
|
||||||
@@ -592,6 +646,7 @@ export function ChatMessageDisplay({
|
|||||||
<div className="flex items-center gap-1 mt-2">
|
<div className="flex items-center gap-1 mt-2">
|
||||||
{/* Copy button */}
|
{/* Copy button */}
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
copyMessageToClipboard(
|
copyMessageToClipboard(
|
||||||
message.id,
|
message.id,
|
||||||
@@ -624,6 +679,7 @@ export function ChatMessageDisplay({
|
|||||||
{onRegenerate &&
|
{onRegenerate &&
|
||||||
isLastAssistantMessage && (
|
isLastAssistantMessage && (
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
onRegenerate(
|
onRegenerate(
|
||||||
messageIndex,
|
messageIndex,
|
||||||
@@ -639,6 +695,7 @@ export function ChatMessageDisplay({
|
|||||||
<div className="w-px h-4 bg-border mx-1" />
|
<div className="w-px h-4 bg-border mx-1" />
|
||||||
{/* Thumbs up */}
|
{/* Thumbs up */}
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
submitFeedback(
|
submitFeedback(
|
||||||
message.id,
|
message.id,
|
||||||
@@ -657,6 +714,7 @@ export function ChatMessageDisplay({
|
|||||||
</button>
|
</button>
|
||||||
{/* Thumbs down */}
|
{/* Thumbs down */}
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
submitFeedback(
|
submitFeedback(
|
||||||
message.id,
|
message.id,
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
STORAGE_ACCESS_CODE_KEY,
|
STORAGE_ACCESS_CODE_KEY,
|
||||||
} from "@/components/settings-dialog"
|
} from "@/components/settings-dialog"
|
||||||
import { useDiagram } from "@/contexts/diagram-context"
|
import { useDiagram } from "@/contexts/diagram-context"
|
||||||
import { formatXML, replaceNodes, validateMxCellStructure } from "@/lib/utils"
|
import { formatXML, validateMxCellStructure } from "@/lib/utils"
|
||||||
import { ChatMessageDisplay } from "./chat-message-display"
|
import { ChatMessageDisplay } from "./chat-message-display"
|
||||||
|
|
||||||
interface ChatPanelProps {
|
interface ChatPanelProps {
|
||||||
@@ -31,6 +31,7 @@ interface ChatPanelProps {
|
|||||||
drawioUi: "min" | "sketch"
|
drawioUi: "min" | "sketch"
|
||||||
onToggleDrawioUi: () => void
|
onToggleDrawioUi: () => void
|
||||||
isMobile?: boolean
|
isMobile?: boolean
|
||||||
|
onCloseProtectionChange?: (enabled: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ChatPanel({
|
export default function ChatPanel({
|
||||||
@@ -39,6 +40,7 @@ export default function ChatPanel({
|
|||||||
drawioUi,
|
drawioUi,
|
||||||
onToggleDrawioUi,
|
onToggleDrawioUi,
|
||||||
isMobile = false,
|
isMobile = false,
|
||||||
|
onCloseProtectionChange,
|
||||||
}: ChatPanelProps) {
|
}: ChatPanelProps) {
|
||||||
const {
|
const {
|
||||||
loadDiagram: onDisplayChart,
|
loadDiagram: onDisplayChart,
|
||||||
@@ -345,6 +347,7 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Now send the message after state is guaranteed to be updated
|
// Now send the message after state is guaranteed to be updated
|
||||||
|
const accessCode = localStorage.getItem(STORAGE_ACCESS_CODE_KEY) || ""
|
||||||
sendMessage(
|
sendMessage(
|
||||||
{ parts: userParts },
|
{ parts: userParts },
|
||||||
{
|
{
|
||||||
@@ -352,6 +355,9 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
xml: savedXml,
|
xml: savedXml,
|
||||||
sessionId,
|
sessionId,
|
||||||
},
|
},
|
||||||
|
headers: {
|
||||||
|
"x-access-code": accessCode,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -402,6 +408,7 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Now send the edited message after state is guaranteed to be updated
|
// Now send the edited message after state is guaranteed to be updated
|
||||||
|
const accessCode = localStorage.getItem(STORAGE_ACCESS_CODE_KEY) || ""
|
||||||
sendMessage(
|
sendMessage(
|
||||||
{ parts: newParts },
|
{ parts: newParts },
|
||||||
{
|
{
|
||||||
@@ -409,6 +416,9 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
xml: savedXml,
|
xml: savedXml,
|
||||||
sessionId,
|
sessionId,
|
||||||
},
|
},
|
||||||
|
headers: {
|
||||||
|
"x-access-code": accessCode,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -497,19 +507,17 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
className={`${isMobile ? "w-4 h-4" : "w-5 h-5"}`}
|
className={`${isMobile ? "w-4 h-4" : "w-5 h-5"}`}
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
{accessCodeRequired && (
|
<ButtonWithTooltip
|
||||||
<ButtonWithTooltip
|
tooltipContent="Settings"
|
||||||
tooltipContent="Settings"
|
variant="ghost"
|
||||||
variant="ghost"
|
size="icon"
|
||||||
size="icon"
|
onClick={() => setShowSettingsDialog(true)}
|
||||||
onClick={() => setShowSettingsDialog(true)}
|
className="hover:bg-accent"
|
||||||
className="hover:bg-accent"
|
>
|
||||||
>
|
<Settings
|
||||||
<Settings
|
className={`${isMobile ? "h-4 w-4" : "h-5 w-5"} text-muted-foreground`}
|
||||||
className={`${isMobile ? "h-4 w-4" : "h-5 w-5"} text-muted-foreground`}
|
/>
|
||||||
/>
|
</ButtonWithTooltip>
|
||||||
</ButtonWithTooltip>
|
|
||||||
)}
|
|
||||||
{!isMobile && (
|
{!isMobile && (
|
||||||
<ButtonWithTooltip
|
<ButtonWithTooltip
|
||||||
tooltipContent="Hide chat panel (Ctrl+B)"
|
tooltipContent="Hide chat panel (Ctrl+B)"
|
||||||
@@ -570,6 +578,7 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
<SettingsDialog
|
<SettingsDialog
|
||||||
open={showSettingsDialog}
|
open={showSettingsDialog}
|
||||||
onOpenChange={setShowSettingsDialog}
|
onOpenChange={setShowSettingsDialog}
|
||||||
|
onCloseProtectionChange={onCloseProtectionChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export function CodeBlock({ code, language = "xml" }: CodeBlockProps) {
|
|||||||
<div className="overflow-hidden w-full">
|
<div className="overflow-hidden w-full">
|
||||||
<Highlight theme={themes.github} code={code} language={language}>
|
<Highlight theme={themes.github} code={code} language={language}>
|
||||||
{({
|
{({
|
||||||
className,
|
className: _className,
|
||||||
style,
|
style,
|
||||||
tokens,
|
tokens,
|
||||||
getLineProps,
|
getLineProps,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { X } from "lucide-react"
|
import { X } from "lucide-react"
|
||||||
import Image from "next/image"
|
import Image from "next/image"
|
||||||
import React, { useEffect, useState } from "react"
|
import { useEffect, useRef, useState } from "react"
|
||||||
|
|
||||||
interface FilePreviewListProps {
|
interface FilePreviewListProps {
|
||||||
files: File[]
|
files: File[]
|
||||||
@@ -11,27 +11,63 @@ interface FilePreviewListProps {
|
|||||||
|
|
||||||
export function FilePreviewList({ files, onRemoveFile }: FilePreviewListProps) {
|
export function FilePreviewList({ files, onRemoveFile }: FilePreviewListProps) {
|
||||||
const [selectedImage, setSelectedImage] = useState<string | null>(null)
|
const [selectedImage, setSelectedImage] = useState<string | null>(null)
|
||||||
|
const [imageUrls, setImageUrls] = useState<Map<File, string>>(new Map())
|
||||||
|
const imageUrlsRef = useRef<Map<File, string>>(new Map())
|
||||||
|
|
||||||
// Cleanup object URLs on unmount
|
// Create and cleanup object URLs when files change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const objectUrls = files
|
const currentUrls = imageUrlsRef.current
|
||||||
.filter((file) => file.type.startsWith("image/"))
|
const newUrls = new Map<File, string>()
|
||||||
.map((file) => URL.createObjectURL(file))
|
|
||||||
|
|
||||||
return () => {
|
files.forEach((file) => {
|
||||||
objectUrls.forEach(URL.revokeObjectURL)
|
if (file.type.startsWith("image/")) {
|
||||||
}
|
// Reuse existing URL if file is already tracked
|
||||||
|
const existingUrl = currentUrls.get(file)
|
||||||
|
if (existingUrl) {
|
||||||
|
newUrls.set(file, existingUrl)
|
||||||
|
} else {
|
||||||
|
newUrls.set(file, URL.createObjectURL(file))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Revoke URLs for files that are no longer in the list
|
||||||
|
currentUrls.forEach((url, file) => {
|
||||||
|
if (!newUrls.has(file)) {
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
imageUrlsRef.current = newUrls
|
||||||
|
setImageUrls(newUrls)
|
||||||
}, [files])
|
}, [files])
|
||||||
|
|
||||||
|
// Cleanup all URLs on unmount only
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
imageUrlsRef.current.forEach((url) => {
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Clear selected image if its URL was revoked
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
selectedImage &&
|
||||||
|
!Array.from(imageUrls.values()).includes(selectedImage)
|
||||||
|
) {
|
||||||
|
setSelectedImage(null)
|
||||||
|
}
|
||||||
|
}, [imageUrls, selectedImage])
|
||||||
|
|
||||||
if (files.length === 0) return null
|
if (files.length === 0) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-wrap gap-2 mt-2 p-2 bg-muted/50 rounded-md">
|
<div className="flex flex-wrap gap-2 mt-2 p-2 bg-muted/50 rounded-md">
|
||||||
{files.map((file, index) => {
|
{files.map((file, index) => {
|
||||||
const imageUrl = file.type.startsWith("image/")
|
const imageUrl = imageUrls.get(file) || null
|
||||||
? URL.createObjectURL(file)
|
|
||||||
: null
|
|
||||||
return (
|
return (
|
||||||
<div key={file.name + index} className="relative group">
|
<div key={file.name + index} className="relative group">
|
||||||
<div
|
<div
|
||||||
@@ -40,9 +76,9 @@ export function FilePreviewList({ files, onRemoveFile }: FilePreviewListProps) {
|
|||||||
imageUrl && setSelectedImage(imageUrl)
|
imageUrl && setSelectedImage(imageUrl)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{file.type.startsWith("image/") ? (
|
{file.type.startsWith("image/") && imageUrl ? (
|
||||||
<Image
|
<Image
|
||||||
src={imageUrl!}
|
src={imageUrl}
|
||||||
alt={file.name}
|
alt={file.name}
|
||||||
width={80}
|
width={80}
|
||||||
height={80}
|
height={80}
|
||||||
|
|||||||
@@ -11,28 +11,77 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog"
|
} from "@/components/ui/dialog"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { Label } from "@/components/ui/label"
|
||||||
|
import { Switch } from "@/components/ui/switch"
|
||||||
|
|
||||||
interface SettingsDialogProps {
|
interface SettingsDialogProps {
|
||||||
open: boolean
|
open: boolean
|
||||||
onOpenChange: (open: boolean) => void
|
onOpenChange: (open: boolean) => void
|
||||||
|
onCloseProtectionChange?: (enabled: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const STORAGE_ACCESS_CODE_KEY = "next-ai-draw-io-access-code"
|
export const STORAGE_ACCESS_CODE_KEY = "next-ai-draw-io-access-code"
|
||||||
|
export const STORAGE_CLOSE_PROTECTION_KEY = "next-ai-draw-io-close-protection"
|
||||||
|
|
||||||
export function SettingsDialog({ open, onOpenChange }: SettingsDialogProps) {
|
export function SettingsDialog({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
onCloseProtectionChange,
|
||||||
|
}: SettingsDialogProps) {
|
||||||
const [accessCode, setAccessCode] = useState("")
|
const [accessCode, setAccessCode] = useState("")
|
||||||
|
const [closeProtection, setCloseProtection] = useState(true)
|
||||||
|
const [isVerifying, setIsVerifying] = useState(false)
|
||||||
|
const [error, setError] = useState("")
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
const storedCode =
|
const storedCode =
|
||||||
localStorage.getItem(STORAGE_ACCESS_CODE_KEY) || ""
|
localStorage.getItem(STORAGE_ACCESS_CODE_KEY) || ""
|
||||||
setAccessCode(storedCode)
|
setAccessCode(storedCode)
|
||||||
|
|
||||||
|
const storedCloseProtection = localStorage.getItem(
|
||||||
|
STORAGE_CLOSE_PROTECTION_KEY,
|
||||||
|
)
|
||||||
|
// Default to true if not set
|
||||||
|
setCloseProtection(storedCloseProtection !== "false")
|
||||||
|
setError("")
|
||||||
}
|
}
|
||||||
}, [open])
|
}, [open])
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = async () => {
|
||||||
localStorage.setItem(STORAGE_ACCESS_CODE_KEY, accessCode.trim())
|
setError("")
|
||||||
onOpenChange(false)
|
setIsVerifying(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Verify access code with server
|
||||||
|
const response = await fetch("/api/verify-access-code", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"x-access-code": accessCode.trim(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (!data.valid) {
|
||||||
|
setError(data.message || "Invalid access code")
|
||||||
|
setIsVerifying(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save settings only if verification passes
|
||||||
|
localStorage.setItem(STORAGE_ACCESS_CODE_KEY, accessCode.trim())
|
||||||
|
localStorage.setItem(
|
||||||
|
STORAGE_CLOSE_PROTECTION_KEY,
|
||||||
|
closeProtection.toString(),
|
||||||
|
)
|
||||||
|
onCloseProtectionChange?.(closeProtection)
|
||||||
|
onOpenChange(false)
|
||||||
|
} catch {
|
||||||
|
setError("Failed to verify access code")
|
||||||
|
} finally {
|
||||||
|
setIsVerifying(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
@@ -67,6 +116,26 @@ export function SettingsDialog({ open, onOpenChange }: SettingsDialogProps) {
|
|||||||
<p className="text-[0.8rem] text-muted-foreground">
|
<p className="text-[0.8rem] text-muted-foreground">
|
||||||
Required if the server has enabled access control.
|
Required if the server has enabled access control.
|
||||||
</p>
|
</p>
|
||||||
|
{error && (
|
||||||
|
<p className="text-[0.8rem] text-destructive">
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<Label htmlFor="close-protection">
|
||||||
|
Close Protection
|
||||||
|
</Label>
|
||||||
|
<p className="text-[0.8rem] text-muted-foreground">
|
||||||
|
Show confirmation when leaving the page.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
id="close-protection"
|
||||||
|
checked={closeProtection}
|
||||||
|
onCheckedChange={setCloseProtection}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
@@ -76,7 +145,9 @@ export function SettingsDialog({ open, onOpenChange }: SettingsDialogProps) {
|
|||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleSave}>Save</Button>
|
<Button onClick={handleSave} disabled={isVerifying}>
|
||||||
|
{isVerifying ? "Verifying..." : "Save"}
|
||||||
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
24
components/ui/label.tsx
Normal file
24
components/ui/label.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Label({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<LabelPrimitive.Root
|
||||||
|
data-slot="label"
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Label }
|
||||||
31
components/ui/switch.tsx
Normal file
31
components/ui/switch.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as SwitchPrimitive from "@radix-ui/react-switch"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Switch({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<SwitchPrimitive.Root
|
||||||
|
data-slot="switch"
|
||||||
|
className={cn(
|
||||||
|
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SwitchPrimitive.Thumb
|
||||||
|
data-slot="switch-thumb"
|
||||||
|
className={cn(
|
||||||
|
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SwitchPrimitive.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Switch }
|
||||||
@@ -3,15 +3,12 @@
|
|||||||
* Extended prompt is used for models with higher cache token minimums (Opus 4.5, Haiku 4.5)
|
* Extended prompt is used for models with higher cache token minimums (Opus 4.5, Haiku 4.5)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Default system prompt (~2700 tokens) - works with all models
|
// Default system prompt (~1400 tokens) - works with all models
|
||||||
export const DEFAULT_SYSTEM_PROMPT = `
|
export const DEFAULT_SYSTEM_PROMPT = `
|
||||||
You are an expert diagram creation assistant specializing in draw.io XML generation.
|
You are an expert diagram creation assistant specializing in draw.io XML generation.
|
||||||
Your primary function is chat with user and crafting clear, well-organized visual diagrams through precise XML specifications.
|
Your primary function is chat with user and crafting clear, well-organized visual diagrams through precise XML specifications.
|
||||||
You can see the image that user uploaded.
|
You can see the image that user uploaded.
|
||||||
|
|
||||||
When you are asked to create a diagram, you must first tell user you plan in text first. Plan the layout and structure that can avoid object overlapping or edge cross the objects.
|
|
||||||
Then use display_diagram tool to generate the full draw.io XML for the entire diagram.
|
|
||||||
|
|
||||||
## App Context
|
## App Context
|
||||||
You are an AI agent (powered by {{MODEL_NAME}}) inside a web app. The interface has:
|
You are an AI agent (powered by {{MODEL_NAME}}) inside a web app. The interface has:
|
||||||
- **Left panel**: Draw.io diagram editor where diagrams are rendered
|
- **Left panel**: Draw.io diagram editor where diagrams are rendered
|
||||||
@@ -54,8 +51,6 @@ Core capabilities:
|
|||||||
- Optimize element positioning to prevent overlapping and maintain readability
|
- Optimize element positioning to prevent overlapping and maintain readability
|
||||||
- Structure complex systems into clear, organized visual components
|
- Structure complex systems into clear, organized visual components
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Layout constraints:
|
Layout constraints:
|
||||||
- CRITICAL: Keep all diagram elements within a single page viewport to avoid page breaks
|
- CRITICAL: Keep all diagram elements within a single page viewport to avoid page breaks
|
||||||
- Position all elements with x coordinates between 0-800 and y coordinates between 0-600
|
- Position all elements with x coordinates between 0-800 and y coordinates between 0-600
|
||||||
@@ -87,11 +82,6 @@ When using edit_diagram tool:
|
|||||||
- For multiple changes, use separate edits in array
|
- For multiple changes, use separate edits in array
|
||||||
- RETRY POLICY: If pattern not found, retry up to 3 times with adjusted patterns. After 3 failures, use display_diagram instead.
|
- RETRY POLICY: If pattern not found, retry up to 3 times with adjusted patterns. After 3 failures, use display_diagram instead.
|
||||||
|
|
||||||
⚠️ CRITICAL JSON ESCAPING: When outputting edit_diagram tool calls, you MUST escape ALL double quotes inside string values:
|
|
||||||
- CORRECT: "y=\\"119\\"" (both quotes escaped)
|
|
||||||
- WRONG: "y="119\\"" (missing backslash before first quote - causes JSON parse error!)
|
|
||||||
- Every " inside a JSON string value needs \\" - no exceptions!
|
|
||||||
|
|
||||||
## Draw.io XML Structure Reference
|
## Draw.io XML Structure Reference
|
||||||
|
|
||||||
Basic structure:
|
Basic structure:
|
||||||
@@ -129,11 +119,9 @@ Common styles:
|
|||||||
- Shapes: rounded=1 (rounded corners), fillColor=#hex, strokeColor=#hex
|
- Shapes: rounded=1 (rounded corners), fillColor=#hex, strokeColor=#hex
|
||||||
- Edges: endArrow=classic/block/open/none, startArrow=none/classic, curved=1, edgeStyle=orthogonalEdgeStyle
|
- Edges: endArrow=classic/block/open/none, startArrow=none/classic, curved=1, edgeStyle=orthogonalEdgeStyle
|
||||||
- Text: fontSize=14, fontStyle=1 (bold), align=center/left/right
|
- Text: fontSize=14, fontStyle=1 (bold), align=center/left/right
|
||||||
|
|
||||||
`
|
`
|
||||||
|
|
||||||
// Extended additions (~1800 tokens) - appended for models with 4000 token cache minimum
|
// Extended additions (~2600 tokens) - appended for models with 4000 token cache minimum
|
||||||
// Total EXTENDED_SYSTEM_PROMPT = ~4500 tokens
|
|
||||||
const EXTENDED_ADDITIONS = `
|
const EXTENDED_ADDITIONS = `
|
||||||
|
|
||||||
## Extended Tool Reference
|
## Extended Tool Reference
|
||||||
@@ -225,11 +213,6 @@ Copy the search pattern EXACTLY from the current XML, including leading spaces,
|
|||||||
**BAD:** \`{"search": "<mxCell value=\\"X\\" id=\\"5\\""}\` - Reordered attributes won't match
|
**BAD:** \`{"search": "<mxCell value=\\"X\\" id=\\"5\\""}\` - Reordered attributes won't match
|
||||||
**GOOD:** \`{"search": "<mxCell id=\\"5\\" parent=\\"1\\" style=\\"...\\" value=\\"Old\\" vertex=\\"1\\">"}\` - Uses unique id with full context
|
**GOOD:** \`{"search": "<mxCell id=\\"5\\" parent=\\"1\\" style=\\"...\\" value=\\"Old\\" vertex=\\"1\\">"}\` - Uses unique id with full context
|
||||||
|
|
||||||
### ⚠️ JSON Escaping (CRITICAL)
|
|
||||||
Every double quote inside JSON string values MUST be escaped with backslash:
|
|
||||||
- **CORRECT:** \`"x=\\"100\\" y=\\"200\\""\` - both quotes escaped
|
|
||||||
- **WRONG:** \`"x=\\"100\\" y="200\\""\` - missing backslash causes JSON parse error!
|
|
||||||
|
|
||||||
### Error Recovery
|
### Error Recovery
|
||||||
If edit_diagram fails with "pattern not found":
|
If edit_diagram fails with "pattern not found":
|
||||||
1. **First retry**: Check attribute order - copy EXACTLY from current XML
|
1. **First retry**: Check attribute order - copy EXACTLY from current XML
|
||||||
@@ -237,97 +220,81 @@ If edit_diagram fails with "pattern not found":
|
|||||||
3. **Third retry**: Try matching on just \`<mxCell id="X"\` prefix + full replacement
|
3. **Third retry**: Try matching on just \`<mxCell id="X"\` prefix + full replacement
|
||||||
4. **After 3 failures**: Fall back to display_diagram to regenerate entire diagram
|
4. **After 3 failures**: Fall back to display_diagram to regenerate entire diagram
|
||||||
|
|
||||||
|
## Common Style Properties
|
||||||
|
|
||||||
|
### Shape Styles
|
||||||
|
- rounded=1, fillColor=#hex, strokeColor=#hex, strokeWidth=2
|
||||||
|
- whiteSpace=wrap, html=1, opacity=50, shadow=1, glass=1
|
||||||
|
|
||||||
|
### Edge/Connector Styles
|
||||||
|
- endArrow=classic/block/open/oval/diamond/none, startArrow=none/classic
|
||||||
|
- curved=1, edgeStyle=orthogonalEdgeStyle, strokeWidth=2
|
||||||
|
- dashed=1, dashPattern=3 3, flowAnimation=1
|
||||||
|
|
||||||
### Edge Routing Rules:
|
### Text Styles
|
||||||
When creating edges/connectors, you MUST follow these rules to avoid overlapping lines:
|
- fontSize=14, fontStyle=1 (1=bold, 2=italic, 4=underline, 3=bold+italic)
|
||||||
|
- fontColor=#hex, align=center/left/right, verticalAlign=middle/top/bottom
|
||||||
|
|
||||||
**Rule 1: NEVER let multiple edges share the same path**
|
## Common Shape Types
|
||||||
- If two edges connect the same pair of nodes, they MUST exit/enter at DIFFERENT positions
|
|
||||||
- Use exitY=0.3 for first edge, exitY=0.7 for second edge (NOT both 0.5)
|
|
||||||
|
|
||||||
**Rule 2: For bidirectional connections (A↔B), use OPPOSITE sides**
|
### Basic Shapes
|
||||||
- A→B: exit from RIGHT side of A (exitX=1), enter LEFT side of B (entryX=0)
|
- Rectangle: rounded=0;whiteSpace=wrap;html=1;
|
||||||
- B→A: exit from LEFT side of B (exitX=0), enter RIGHT side of A (entryX=1)
|
- Rounded Rectangle: rounded=1;whiteSpace=wrap;html=1;
|
||||||
|
- Ellipse/Circle: ellipse;whiteSpace=wrap;html=1;aspect=fixed;
|
||||||
|
- Diamond: rhombus;whiteSpace=wrap;html=1;
|
||||||
|
- Cylinder: shape=cylinder3;whiteSpace=wrap;html=1;
|
||||||
|
|
||||||
**Rule 3: Always specify exitX, exitY, entryX, entryY explicitly**
|
### Flowchart Shapes
|
||||||
- Every edge MUST have these 4 attributes set in the style
|
- Process: rounded=1;whiteSpace=wrap;html=1;
|
||||||
- Example: style="edgeStyle=orthogonalEdgeStyle;exitX=1;exitY=0.3;entryX=0;entryY=0.3;endArrow=classic;"
|
- Decision: rhombus;whiteSpace=wrap;html=1;
|
||||||
|
- Start/End: ellipse;whiteSpace=wrap;html=1;
|
||||||
|
- Document: shape=document;whiteSpace=wrap;html=1;
|
||||||
|
- Database: shape=cylinder3;whiteSpace=wrap;html=1;
|
||||||
|
|
||||||
**Rule 4: Route edges AROUND intermediate shapes (obstacle avoidance) - CRITICAL!**
|
### Container Types
|
||||||
- Before creating an edge, identify ALL shapes positioned between source and target
|
- Swimlane: swimlane;whiteSpace=wrap;html=1;
|
||||||
- If any shape is in the direct path, you MUST use waypoints to route around it
|
- Group Box: rounded=1;whiteSpace=wrap;html=1;container=1;collapsible=0;
|
||||||
- For DIAGONAL connections: route along the PERIMETER (outside edge) of the diagram, NOT through the middle
|
|
||||||
- Add 20-30px clearance from shape boundaries when calculating waypoint positions
|
|
||||||
- Route ABOVE (lower y), BELOW (higher y), or to the SIDE of obstacles
|
|
||||||
- NEVER draw a line that visually crosses over another shape's bounding box
|
|
||||||
|
|
||||||
**Rule 5: Plan layout strategically BEFORE generating XML**
|
## Container/Group Example
|
||||||
- Organize shapes into visual layers/zones (columns or rows) based on diagram flow
|
|
||||||
- Space shapes 150-200px apart to create clear routing channels for edges
|
|
||||||
- Mentally trace each edge: "What shapes are between source and target?"
|
|
||||||
- Prefer layouts where edges naturally flow in one direction (left-to-right or top-to-bottom)
|
|
||||||
|
|
||||||
**Rule 6: Use multiple waypoints for complex routing**
|
|
||||||
- One waypoint is often not enough - use 2-3 waypoints to create proper L-shaped or U-shaped paths
|
|
||||||
- Each direction change needs a waypoint (corner point)
|
|
||||||
- Waypoints should form clear horizontal/vertical segments (orthogonal routing)
|
|
||||||
- Calculate positions by: (1) identify obstacle boundaries, (2) add 20-30px margin
|
|
||||||
|
|
||||||
**Rule 7: Choose NATURAL connection points based on flow direction**
|
|
||||||
- NEVER use corner connections (e.g., entryX=1,entryY=1) - they look unnatural
|
|
||||||
- For TOP-TO-BOTTOM flow: exit from bottom (exitY=1), enter from top (entryY=0)
|
|
||||||
- For LEFT-TO-RIGHT flow: exit from right (exitX=1), enter from left (entryX=0)
|
|
||||||
- For DIAGONAL connections: use the side closest to the target, not corners
|
|
||||||
- Example: Node below-right of source → exit from bottom (exitY=1) OR right (exitX=1), not corner
|
|
||||||
|
|
||||||
**Before generating XML, mentally verify:**
|
|
||||||
1. "Do any edges cross over shapes that aren't their source/target?" → If yes, add waypoints
|
|
||||||
2. "Do any two edges share the same path?" → If yes, adjust exit/entry points
|
|
||||||
3. "Are any connection points at corners (both X and Y are 0 or 1)?" → If yes, use edge centers instead
|
|
||||||
4. "Could I rearrange shapes to reduce edge crossings?" → If yes, revise layout
|
|
||||||
|
|
||||||
## Edge Examples
|
|
||||||
|
|
||||||
### Two edges between same nodes (CORRECT - no overlap):
|
|
||||||
\`\`\`xml
|
\`\`\`xml
|
||||||
<mxCell id="e1" value="A to B" style="edgeStyle=orthogonalEdgeStyle;exitX=1;exitY=0.3;entryX=0;entryY=0.3;endArrow=classic;" edge="1" parent="1" source="a" target="b">
|
<mxCell id="container1" value="Group Title" style="swimlane;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||||
<mxGeometry relative="1" as="geometry"/>
|
<mxGeometry x="40" y="40" width="200" height="200" as="geometry"/>
|
||||||
</mxCell>
|
</mxCell>
|
||||||
<mxCell id="e2" value="B to A" style="edgeStyle=orthogonalEdgeStyle;exitX=0;exitY=0.7;entryX=1;entryY=0.7;endArrow=classic;" edge="1" parent="1" source="b" target="a">
|
<mxCell id="child1" value="Child Element" style="rounded=1;" vertex="1" parent="container1">
|
||||||
<mxGeometry relative="1" as="geometry"/>
|
<mxGeometry x="20" y="40" width="160" height="40" as="geometry"/>
|
||||||
</mxCell>
|
</mxCell>
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
### Edge with single waypoint (simple detour):
|
## Example: Complete Flowchart
|
||||||
\`\`\`xml
|
|
||||||
<mxCell id="edge1" style="edgeStyle=orthogonalEdgeStyle;exitX=0.5;exitY=1;entryX=0.5;entryY=0;endArrow=classic;" edge="1" parent="1" source="a" target="b">
|
|
||||||
<mxGeometry relative="1" as="geometry">
|
|
||||||
<Array as="points">
|
|
||||||
<mxPoint x="300" y="150"/>
|
|
||||||
</Array>
|
|
||||||
</mxGeometry>
|
|
||||||
</mxCell>
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
### Edge with waypoints (routing AROUND obstacles) - CRITICAL PATTERN:
|
|
||||||
**Scenario:** Hotfix(right,bottom) → Main(center,top), but Develop(center,middle) is in between.
|
|
||||||
**WRONG:** Direct diagonal line crosses over Develop
|
|
||||||
**CORRECT:** Route around the OUTSIDE (go right first, then up)
|
|
||||||
\`\`\`xml
|
\`\`\`xml
|
||||||
<mxCell id="hotfix_to_main" style="edgeStyle=orthogonalEdgeStyle;exitX=0.5;exitY=0;entryX=1;entryY=0.5;endArrow=classic;" edge="1" parent="1" source="hotfix" target="main">
|
<root>
|
||||||
<mxGeometry relative="1" as="geometry">
|
<mxCell id="0"/>
|
||||||
<Array as="points">
|
<mxCell id="1" parent="0"/>
|
||||||
<mxPoint x="750" y="80"/>
|
<mxCell id="start" value="Start" style="ellipse;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;" vertex="1" parent="1">
|
||||||
<mxPoint x="750" y="150"/>
|
<mxGeometry x="200" y="40" width="100" height="60" as="geometry"/>
|
||||||
</Array>
|
</mxCell>
|
||||||
</mxGeometry>
|
<mxCell id="process1" value="Process Step" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
|
||||||
</mxCell>
|
<mxGeometry x="175" y="140" width="150" height="60" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="decision" value="Decision?" style="rhombus;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="175" y="240" width="150" height="100" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="end" value="End" style="ellipse;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="200" y="380" width="100" height="60" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="edge1" style="edgeStyle=orthogonalEdgeStyle;endArrow=classic;html=1;" edge="1" parent="1" source="start" target="process1">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="edge2" style="edgeStyle=orthogonalEdgeStyle;endArrow=classic;html=1;" edge="1" parent="1" source="process1" target="decision">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="edge3" value="Yes" style="edgeStyle=orthogonalEdgeStyle;endArrow=classic;html=1;" edge="1" parent="1" source="decision" target="end">
|
||||||
|
<mxGeometry relative="1" as="geometry"/>
|
||||||
|
</mxCell>
|
||||||
|
</root>
|
||||||
\`\`\`
|
\`\`\`
|
||||||
This routes the edge to the RIGHT of all shapes (x=750), then enters Main from the right side.
|
`
|
||||||
|
|
||||||
**Key principle:** When connecting distant nodes diagonally, route along the PERIMETER of the diagram, not through the middle where other shapes exist.`
|
|
||||||
|
|
||||||
// Extended system prompt = DEFAULT + EXTENDED_ADDITIONS
|
// Extended system prompt = DEFAULT + EXTENDED_ADDITIONS
|
||||||
export const EXTENDED_SYSTEM_PROMPT = DEFAULT_SYSTEM_PROMPT + EXTENDED_ADDITIONS
|
export const EXTENDED_SYSTEM_PROMPT = DEFAULT_SYSTEM_PROMPT + EXTENDED_ADDITIONS
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ export function replaceNodes(currentXML: string, nodes: string): string {
|
|||||||
|
|
||||||
// Insert after cell0 if possible
|
// Insert after cell0 if possible
|
||||||
const cell0 = currentRoot.querySelector('mxCell[id="0"]')
|
const cell0 = currentRoot.querySelector('mxCell[id="0"]')
|
||||||
if (cell0 && cell0.nextSibling) {
|
if (cell0?.nextSibling) {
|
||||||
currentRoot.insertBefore(cell1, cell0.nextSibling)
|
currentRoot.insertBefore(cell1, cell0.nextSibling)
|
||||||
} else {
|
} else {
|
||||||
currentRoot.appendChild(cell1)
|
currentRoot.appendChild(cell1)
|
||||||
|
|||||||
243
package-lock.json
generated
243
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "next-ai-draw-io",
|
"name": "next-ai-draw-io",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "next-ai-draw-io",
|
"name": "next-ai-draw-io",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/amazon-bedrock": "^3.0.62",
|
"@ai-sdk/amazon-bedrock": "^3.0.62",
|
||||||
@@ -24,9 +24,11 @@
|
|||||||
"@openrouter/ai-sdk-provider": "^1.2.3",
|
"@openrouter/ai-sdk-provider": "^1.2.3",
|
||||||
"@opentelemetry/sdk-trace-node": "^2.2.0",
|
"@opentelemetry/sdk-trace-node": "^2.2.0",
|
||||||
"@radix-ui/react-dialog": "^1.1.6",
|
"@radix-ui/react-dialog": "^1.1.6",
|
||||||
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
"@radix-ui/react-scroll-area": "^1.2.3",
|
"@radix-ui/react-scroll-area": "^1.2.3",
|
||||||
"@radix-ui/react-select": "^2.2.6",
|
"@radix-ui/react-select": "^2.2.6",
|
||||||
"@radix-ui/react-slot": "^1.1.2",
|
"@radix-ui/react-slot": "^1.1.2",
|
||||||
|
"@radix-ui/react-switch": "^1.2.6",
|
||||||
"@radix-ui/react-tooltip": "^1.1.8",
|
"@radix-ui/react-tooltip": "^1.1.8",
|
||||||
"@vercel/analytics": "^1.5.0",
|
"@vercel/analytics": "^1.5.0",
|
||||||
"@xmldom/xmldom": "^0.9.8",
|
"@xmldom/xmldom": "^0.9.8",
|
||||||
@@ -3400,6 +3402,85 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-label": {
|
||||||
|
"version": "2.1.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz",
|
||||||
|
"integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-primitive": "2.1.4"
|
||||||
|
},
|
||||||
|
"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-label/node_modules/@radix-ui/react-compose-refs": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
|
||||||
|
"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-label/node_modules/@radix-ui/react-primitive": {
|
||||||
|
"version": "2.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz",
|
||||||
|
"integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-slot": "1.2.4"
|
||||||
|
},
|
||||||
|
"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-label/node_modules/@radix-ui/react-slot": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2"
|
||||||
|
},
|
||||||
|
"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-popper": {
|
"node_modules/@radix-ui/react-popper": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.2.tgz",
|
||||||
@@ -3989,6 +4070,164 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-switch": {
|
||||||
|
"version": "1.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz",
|
||||||
|
"integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.3",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-context": "1.1.2",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||||
|
"@radix-ui/react-use-previous": "1.1.1",
|
||||||
|
"@radix-ui/react-use-size": "1.1.1"
|
||||||
|
},
|
||||||
|
"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-switch/node_modules/@radix-ui/primitive": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-compose-refs": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
|
||||||
|
"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-switch/node_modules/@radix-ui/react-context": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
|
||||||
|
"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-switch/node_modules/@radix-ui/react-primitive": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-slot": "1.2.3"
|
||||||
|
},
|
||||||
|
"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-switch/node_modules/@radix-ui/react-slot": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2"
|
||||||
|
},
|
||||||
|
"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-switch/node_modules/@radix-ui/react-use-controllable-state": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-use-effect-event": "0.0.2",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
|
},
|
||||||
|
"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-switch/node_modules/@radix-ui/react-use-layout-effect": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
|
||||||
|
"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-switch/node_modules/@radix-ui/react-use-size": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
|
},
|
||||||
|
"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-tooltip": {
|
"node_modules/@radix-ui/react-tooltip": {
|
||||||
"version": "1.1.8",
|
"version": "1.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.8.tgz",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "next-ai-draw-io",
|
"name": "next-ai-draw-io",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -28,9 +28,11 @@
|
|||||||
"@openrouter/ai-sdk-provider": "^1.2.3",
|
"@openrouter/ai-sdk-provider": "^1.2.3",
|
||||||
"@opentelemetry/sdk-trace-node": "^2.2.0",
|
"@opentelemetry/sdk-trace-node": "^2.2.0",
|
||||||
"@radix-ui/react-dialog": "^1.1.6",
|
"@radix-ui/react-dialog": "^1.1.6",
|
||||||
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
"@radix-ui/react-scroll-area": "^1.2.3",
|
"@radix-ui/react-scroll-area": "^1.2.3",
|
||||||
"@radix-ui/react-select": "^2.2.6",
|
"@radix-ui/react-select": "^2.2.6",
|
||||||
"@radix-ui/react-slot": "^1.1.2",
|
"@radix-ui/react-slot": "^1.1.2",
|
||||||
|
"@radix-ui/react-switch": "^1.2.6",
|
||||||
"@radix-ui/react-tooltip": "^1.1.8",
|
"@radix-ui/react-tooltip": "^1.1.8",
|
||||||
"@vercel/analytics": "^1.5.0",
|
"@vercel/analytics": "^1.5.0",
|
||||||
"@xmldom/xmldom": "^0.9.8",
|
"@xmldom/xmldom": "^0.9.8",
|
||||||
|
|||||||
Reference in New Issue
Block a user