mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-01-03 14:52:28 +08:00
Compare commits
6 Commits
feat/tool-
...
test/repli
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
73b1f6e8be | ||
|
|
1b7414d7a1 | ||
|
|
2f01f016d9 | ||
|
|
28258d19ca | ||
|
|
fc8c1b64c8 | ||
|
|
b396f07254 |
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"extends": [
|
||||
"next/core-web-vitals",
|
||||
"next/typescript"
|
||||
]
|
||||
}
|
||||
6
.github/workflows/claude-code.yml
vendored
6
.github/workflows/claude-code.yml
vendored
@@ -74,7 +74,11 @@ jobs:
|
||||
|
||||
This is a personal project - an AI-powered draw.io diagram generator built with:
|
||||
- Next.js 15 with React 19
|
||||
- Vercel AI SDK (streamText, useChat, tool calling)
|
||||
- Multiple AI providers: Bedrock, Anthropic, OpenAI, Google, Azure, OpenRouter, Ollama
|
||||
|
||||
First, check previous review comments from github-actions bot using `gh api repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/comments`.
|
||||
For each previous comment:
|
||||
- If the issue is fixed in the current code, resolve the comment thread using:
|
||||
- Multiple AI providers: Bedrock, Anthropic, OpenAI, Google, Azure, OpenRouter, Ollama
|
||||
|
||||
STEP 1: Check existing comments to avoid duplicates.
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -41,3 +41,8 @@ yarn-error.log*
|
||||
next-env.d.ts
|
||||
push-via-ec2.sh
|
||||
.claude/settings.local.json
|
||||
|
||||
next
|
||||
next-ai-draw-io@0.2.0
|
||||
object
|
||||
starting
|
||||
|
||||
@@ -120,14 +120,13 @@ ${lastMessageText}
|
||||
console.log("Enhanced messages:", enhancedMessages);
|
||||
|
||||
// Get AI model from environment configuration
|
||||
const { model, providerOptions, headers } = getAIModel();
|
||||
const { model, providerOptions } = getAIModel();
|
||||
|
||||
const result = streamText({
|
||||
model,
|
||||
system: systemMessage,
|
||||
messages: enhancedMessages,
|
||||
...(providerOptions && { providerOptions }),
|
||||
...(headers && { headers }),
|
||||
tools: {
|
||||
// Client-side tool that will be executed on the client
|
||||
display_diagram: {
|
||||
|
||||
@@ -1,23 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import type React from "react";
|
||||
import { useRef, useEffect, useState, useCallback } from "react";
|
||||
import Image from "next/image";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import ExamplePanel from "./chat-example-panel";
|
||||
import { UIMessage } from "ai";
|
||||
import { convertToLegalXml, replaceNodes } from "@/lib/utils";
|
||||
import { Copy, Check, X } from "lucide-react";
|
||||
import { Copy, Check } from "lucide-react";
|
||||
|
||||
import { useDiagram } from "@/contexts/diagram-context";
|
||||
|
||||
const getMessageTextContent = (message: UIMessage): string => {
|
||||
if (!message.parts) return "";
|
||||
return message.parts
|
||||
.filter((part: any) => part.type === "text")
|
||||
.map((part: any) => part.text)
|
||||
.join("\n");
|
||||
};
|
||||
|
||||
interface ChatMessageDisplayProps {
|
||||
messages: UIMessage[];
|
||||
error?: Error | null;
|
||||
@@ -39,20 +32,50 @@ export function ChatMessageDisplay({
|
||||
{}
|
||||
);
|
||||
const [copiedMessageId, setCopiedMessageId] = useState<string | null>(null);
|
||||
const [copyFailedMessageId, setCopyFailedMessageId] = useState<string | null>(null);
|
||||
|
||||
// 复制消息到剪贴板,支持非 HTTPS 环境的降级处理
|
||||
const copyMessageToClipboard = async (messageId: string, text: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
// 优先使用 Clipboard API(需要 HTTPS 或 localhost)
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
} else {
|
||||
// 降级方案:使用传统的 execCommand 方法(兼容 HTTP 环境)
|
||||
const textArea = document.createElement("textarea");
|
||||
textArea.value = text;
|
||||
// 设置样式避免影响页面布局
|
||||
textArea.style.position = "fixed";
|
||||
// 降级方案:使用传统的 execCommand 方法(兼容 HTTP 环境)
|
||||
// Fallback: Use textarea selection (works in most browsers)
|
||||
textArea.style.top = "-9999px";
|
||||
textArea.style.opacity = "0";
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
|
||||
const successful = document.execCommand("copy");
|
||||
document.body.removeChild(textArea);
|
||||
|
||||
if (!successful) {
|
||||
throw new Error("execCommand copy failed");
|
||||
}
|
||||
}
|
||||
setCopiedMessageId(messageId);
|
||||
setTimeout(() => setCopiedMessageId(null), 2000);
|
||||
setTimeout(() => {
|
||||
setCopiedMessageId(null);
|
||||
}, 2000);
|
||||
} catch (err) {
|
||||
console.error("Failed to copy message:", err);
|
||||
setCopyFailedMessageId(messageId);
|
||||
setTimeout(() => setCopyFailedMessageId(null), 2000);
|
||||
}
|
||||
};
|
||||
|
||||
const getMessageTextContent = (message: UIMessage): string => {
|
||||
if (!message.parts) return "";
|
||||
return message.parts
|
||||
.filter((part: any) => part.type === "text")
|
||||
.map((part: any) => part.text)
|
||||
.join("\n");
|
||||
};
|
||||
const handleDisplayChart = useCallback(
|
||||
(xml: string) => {
|
||||
const currentXml = xml || "";
|
||||
@@ -160,16 +183,16 @@ export function ChatMessageDisplay({
|
||||
{output || (toolName === "display_diagram"
|
||||
? "Diagram generated"
|
||||
: toolName === "edit_diagram"
|
||||
? "Diagram edited"
|
||||
: "Tool executed")}
|
||||
? "Diagram edited"
|
||||
: "Tool executed")}
|
||||
</div>
|
||||
) : state === "output-error" ? (
|
||||
<div className="text-red-600">
|
||||
{output || (toolName === "display_diagram"
|
||||
? "Error generating diagram"
|
||||
: toolName === "edit_diagram"
|
||||
? "Error editing diagram"
|
||||
: "Tool error")}
|
||||
? "Error editing diagram"
|
||||
: "Tool error")}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -188,25 +211,11 @@ export function ChatMessageDisplay({
|
||||
return (
|
||||
<div
|
||||
key={message.id}
|
||||
className={`mb-4 flex ${message.role === "user" ? "justify-end" : "justify-start"}`}
|
||||
className={`mb-4 ${message.role === "user" ? "text-right" : "text-left"
|
||||
}`}
|
||||
>
|
||||
{message.role === "user" && userMessageText && (
|
||||
<button
|
||||
onClick={() => copyMessageToClipboard(message.id, userMessageText)}
|
||||
className="p-1 text-gray-400 hover:text-gray-600 transition-colors self-center mr-1"
|
||||
title={copiedMessageId === message.id ? "Copied!" : copyFailedMessageId === message.id ? "Failed to copy" : "Copy message"}
|
||||
>
|
||||
{copiedMessageId === message.id ? (
|
||||
<Check className="h-3.5 w-3.5 text-green-500" />
|
||||
) : copyFailedMessageId === message.id ? (
|
||||
<X className="h-3.5 w-3.5 text-red-500" />
|
||||
) : (
|
||||
<Copy className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
<div
|
||||
className={`px-4 py-2 whitespace-pre-wrap text-sm rounded-lg max-w-[85%] break-words ${message.role === "user"
|
||||
className={`inline-block px-4 py-2 whitespace-pre-wrap text-sm rounded-lg max-w-[85%] break-words ${message.role === "user"
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-muted text-muted-foreground"
|
||||
}`}
|
||||
@@ -240,8 +249,23 @@ export function ChatMessageDisplay({
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
{userMessageText && (
|
||||
<div className="flex justify-start mt-1">
|
||||
<button
|
||||
onClick={() => copyMessageToClipboard(message.id, userMessageText)}
|
||||
className="p-1 text-gray-400 hover:text-gray-600 transition-colors"
|
||||
title={copiedMessageId === message.id ? "Copied!" : "Copy message"}
|
||||
>
|
||||
{copiedMessageId === message.id ? (
|
||||
<Check className="h-3.5 w-3.5 text-green-500" />
|
||||
) : (
|
||||
<Copy className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
})
|
||||
)}
|
||||
{error && (
|
||||
|
||||
@@ -18,19 +18,15 @@ export type ProviderName =
|
||||
interface ModelConfig {
|
||||
model: any;
|
||||
providerOptions?: any;
|
||||
headers?: Record<string, string>;
|
||||
}
|
||||
|
||||
// Bedrock provider options for Anthropic beta features
|
||||
const BEDROCK_ANTHROPIC_BETA = {
|
||||
bedrock: {
|
||||
anthropicBeta: ['fine-grained-tool-streaming-2025-05-14'],
|
||||
},
|
||||
};
|
||||
|
||||
// Direct Anthropic API headers for beta features
|
||||
const ANTHROPIC_BETA_HEADERS = {
|
||||
'anthropic-beta': 'fine-grained-tool-streaming-2025-05-14',
|
||||
// Anthropic beta headers for fine-grained tool streaming
|
||||
const ANTHROPIC_BETA_OPTIONS = {
|
||||
anthropic: {
|
||||
additionalModelRequestFields: {
|
||||
anthropic_beta: ['fine-grained-tool-streaming-2025-05-14']
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -91,14 +87,13 @@ export function getAIModel(): ModelConfig {
|
||||
|
||||
let model: any;
|
||||
let providerOptions: any = undefined;
|
||||
let headers: Record<string, string> | undefined = undefined;
|
||||
|
||||
switch (provider) {
|
||||
case 'bedrock':
|
||||
model = bedrock(modelId);
|
||||
// Add Anthropic beta options if using Claude models via Bedrock
|
||||
// Add Anthropic beta headers if using Claude models via Bedrock
|
||||
if (modelId.includes('anthropic.claude')) {
|
||||
providerOptions = BEDROCK_ANTHROPIC_BETA;
|
||||
providerOptions = ANTHROPIC_BETA_OPTIONS;
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -117,7 +112,7 @@ export function getAIModel(): ModelConfig {
|
||||
case 'anthropic':
|
||||
model = anthropic(modelId);
|
||||
// Add beta headers for fine-grained tool streaming
|
||||
headers = ANTHROPIC_BETA_HEADERS;
|
||||
providerOptions = ANTHROPIC_BETA_OPTIONS;
|
||||
break;
|
||||
|
||||
case 'google':
|
||||
@@ -145,10 +140,10 @@ export function getAIModel(): ModelConfig {
|
||||
);
|
||||
}
|
||||
|
||||
// Log if provider options or headers are being applied
|
||||
if (providerOptions || headers) {
|
||||
console.log('[AI Provider] Applying provider-specific options/headers');
|
||||
// Log if provider options are being applied
|
||||
if (providerOptions) {
|
||||
console.log('[AI Provider] Applying provider-specific options');
|
||||
}
|
||||
|
||||
return { model, providerOptions, headers };
|
||||
return { model, providerOptions };
|
||||
}
|
||||
|
||||
4753
package-lock.json
generated
4753
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,7 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/amazon-bedrock": "^3.0.62",
|
||||
"@ai-sdk/amazon-bedrock": "^3.0.52",
|
||||
"@ai-sdk/anthropic": "^2.0.44",
|
||||
"@ai-sdk/azure": "^2.0.69",
|
||||
"@ai-sdk/google": "^2.0.0",
|
||||
@@ -46,8 +46,6 @@
|
||||
"@types/pako": "^2.0.3",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "9.39.1",
|
||||
"eslint-config-next": "16.0.5",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user