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
3 changed files with 131 additions and 55 deletions

View File

@@ -74,19 +74,24 @@ jobs:
This is a personal project - an AI-powered draw.io diagram generator built with: This is a personal project - an AI-powered draw.io diagram generator built with:
- Next.js 15 with React 19 - Next.js 15 with React 19
- Vercel AI SDK (streamText, useChat, tool calling)
- Multiple AI providers: Bedrock, Anthropic, OpenAI, Google, Azure, OpenRouter, Ollama - 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`. 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: For each previous comment:
- If the issue is fixed in the current code, resolve the comment thread using: - 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.
Run: `gh api repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/comments`
Build a list of files and line numbers that already have comments. For each existing comment:
- If the issue is FIXED in current code, resolve the thread using:
`gh api graphql -f query='mutation { resolveReviewThread(input: {threadId: "THREAD_ID"}) { thread { isResolved } } }'` `gh api graphql -f query='mutation { resolveReviewThread(input: {threadId: "THREAD_ID"}) { thread { isResolved } } }'`
Get the thread ID from the comment's node_id field. - If the issue still exists, remember this file:line - DO NOT create a new comment for it
- If the issue still exists, leave it alone
Then review the current diff for NEW issues only: STEP 2: Review the diff for issues, but SKIP any file:line that already has a comment.
Review this PR for ONLY these issues: Review this PR for these issues (report ALL that apply):
1. Bugs that would cause runtime errors or broken functionality 1. Bugs that would cause runtime errors or broken functionality
2. Security issues (exposed secrets, API key leaks) 2. Security issues (exposed secrets, API key leaks)
3. AI SDK misuse - specifically check for: 3. AI SDK misuse - specifically check for:
@@ -96,18 +101,22 @@ jobs:
- Tool definitions: Must use Zod schemas for inputSchema - Tool definitions: Must use Zod schemas for inputSchema
- Status handling: Check status (submitted/streaming/ready/error) before actions - Status handling: Check status (submitted/streaming/ready/error) before actions
- Stream cleanup: Call stop() when aborting streams - Stream cleanup: Call stop() when aborting streams
4. Unrelated changes that should be in separate PRs (scope creep)
5. Suspicious .gitignore additions or accidentally committed files
6. UI/UX inconsistencies (e.g., alignment issues)
When reviewing AI SDK usage, fetch https://ai-sdk.dev/docs/ to verify correct patterns. When reviewing AI SDK usage, fetch https://ai-sdk.dev/docs/ to verify correct patterns.
Key doc pages: /docs/ai-sdk-ui/chatbot, /docs/ai-sdk-core/generating-text, /docs/ai-sdk-core/tools-and-tool-calling Key doc pages: /docs/ai-sdk-ui/chatbot, /docs/ai-sdk-core/generating-text, /docs/ai-sdk-core/tools-and-tool-calling
DO NOT comment on: DO NOT comment on:
- Performance optimizations - Minor performance optimizations
- Code style or formatting - Code style preferences (unless clearly wrong)
- "Best practices" that don't affect functionality - Type annotations that don't affect functionality
- Type safety improvements
- Error handling additions
Use `mcp__github_inline_comment__create_inline_comment` for inline comments. IMPORTANT:
Be very selective - if there are no real bugs, just say "LGTM" in a PR comment. - NEVER create a comment on a file:line that already has a comment - this causes duplicates
- For each NEW issue, use `mcp__github_inline_comment__create_inline_comment` to comment on the specific line
- ALWAYS include a suggested fix using GitHub's suggestion syntax: ```suggestion\n<fixed code>\n```
- Only say "LGTM" if there are truly ZERO new issues to report
claude_args: | claude_args: |
--allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh api:*),WebFetch(domain:ai-sdk.dev)" --allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh api:*),WebFetch(domain:ai-sdk.dev)"

5
.gitignore vendored
View File

@@ -41,3 +41,8 @@ yarn-error.log*
next-env.d.ts next-env.d.ts
push-via-ec2.sh push-via-ec2.sh
.claude/settings.local.json .claude/settings.local.json
next
next-ai-draw-io@0.2.0
object
starting

View File

@@ -7,6 +7,7 @@ import { ScrollArea } from "@/components/ui/scroll-area";
import ExamplePanel from "./chat-example-panel"; import ExamplePanel from "./chat-example-panel";
import { UIMessage } from "ai"; import { UIMessage } from "ai";
import { convertToLegalXml, replaceNodes } from "@/lib/utils"; import { convertToLegalXml, replaceNodes } from "@/lib/utils";
import { Copy, Check } from "lucide-react";
import { useDiagram } from "@/contexts/diagram-context"; import { useDiagram } from "@/contexts/diagram-context";
@@ -30,6 +31,51 @@ export function ChatMessageDisplay({
const [expandedTools, setExpandedTools] = useState<Record<string, boolean>>( const [expandedTools, setExpandedTools] = useState<Record<string, boolean>>(
{} {}
); );
const [copiedMessageId, setCopiedMessageId] = useState<string | null>(null);
// 复制消息到剪贴板,支持非 HTTPS 环境的降级处理
const copyMessageToClipboard = async (messageId: string, text: string) => {
try {
// 优先使用 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);
} catch (err) {
console.error("Failed to copy message:", err);
}
};
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( const handleDisplayChart = useCallback(
(xml: string) => { (xml: string) => {
const currentXml = xml || ""; const currentXml = xml || "";
@@ -160,16 +206,16 @@ export function ChatMessageDisplay({
{messages.length === 0 ? ( {messages.length === 0 ? (
<ExamplePanel setInput={setInput} setFiles={setFiles} /> <ExamplePanel setInput={setInput} setFiles={setFiles} />
) : ( ) : (
messages.map((message) => ( messages.map((message) => {
const userMessageText = message.role === "user" ? getMessageTextContent(message) : "";
return (
<div <div
key={message.id} key={message.id}
className={`mb-4 ${ className={`mb-4 ${message.role === "user" ? "text-right" : "text-left"
message.role === "user" ? "text-right" : "text-left"
}`} }`}
> >
<div <div
className={`inline-block px-4 py-2 whitespace-pre-wrap text-sm rounded-lg max-w-[85%] break-words ${ className={`inline-block px-4 py-2 whitespace-pre-wrap text-sm rounded-lg max-w-[85%] break-words ${message.role === "user"
message.role === "user"
? "bg-primary text-primary-foreground" ? "bg-primary text-primary-foreground"
: "bg-muted text-muted-foreground" : "bg-muted text-muted-foreground"
}`} }`}
@@ -203,8 +249,24 @@ export function ChatMessageDisplay({
} }
})} })}
</div> </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>
)) )}
</div>
)
})
)} )}
{error && ( {error && (
<div className="text-red-500 text-sm mt-2"> <div className="text-red-500 text-sm mt-2">