Compare commits

..

6 Commits

Author SHA1 Message Date
Dayuan Jiang
73b1f6e8be Update components/chat-message-display.tsx
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-11-29 13:03:46 +09:00
Dayuan Jiang
1b7414d7a1 Update .github/workflows/claude-code.yml
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-11-29 13:03:27 +09:00
dayuan.jiang
2f01f016d9 fix: prevent duplicate PR review comments by tracking existing comments 2025-11-29 12:51:34 +09:00
dayuan.jiang
28258d19ca chore: trigger workflow re-run for suggestion syntax 2025-11-29 12:46:42 +09:00
dayuan.jiang
fc8c1b64c8 chore: trigger workflow re-run 2025-11-29 12:39:59 +09:00
dayuan.jiang
b396f07254 feat: add copy button to user messages in chat sidebar
- Add copy-to-clipboard functionality for user messages
- Support non-HTTPS environments with execCommand fallback
- Add visual feedback with check icon on successful copy
2025-11-29 12:19:14 +09:00
8 changed files with 116 additions and 4790 deletions

View File

@@ -1,6 +0,0 @@
{
"extends": [
"next/core-web-vitals",
"next/typescript"
]
}

View File

@@ -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
View File

@@ -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

View File

@@ -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: {

View File

@@ -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 && (

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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"
}