From 3fb349fb3e113263097d5b73a7e86dc2c7816366 Mon Sep 17 00:00:00 2001 From: Twelveeee <48245733+Twelveeee@users.noreply.github.com> Date: Fri, 5 Dec 2025 21:09:34 +0800 Subject: [PATCH] clear button cant clear error msg & feat: add setting dialog and add accesscode (#77) * fix: clear button cant clear error msg * new: add setting dialog and add accesscode * fix: address review feedback - dark mode, types, formatting * feat: only show Settings button when access code is required * refactor: rename ACCESS_CODES to ACCESS_CODE_LIST --------- Co-authored-by: dayuan.jiang --- README.md | 3 ++ README_CN.md | 3 ++ README_JA.md | 3 ++ app/api/chat/route.ts | 12 +++++ app/api/config/route.ts | 9 ++++ components/chat-message-display.tsx | 9 +--- components/chat-panel.tsx | 56 +++++++++++++++++-- components/settings-dialog.tsx | 83 +++++++++++++++++++++++++++++ env.example | 3 ++ 9 files changed, 171 insertions(+), 10 deletions(-) create mode 100644 app/api/config/route.ts create mode 100644 components/settings-dialog.tsx diff --git a/README.md b/README.md index 99b0186..dc95e78 100644 --- a/README.md +++ b/README.md @@ -149,6 +149,9 @@ Edit `.env.local` and configure your chosen provider: - Set `AI_PROVIDER` to your chosen provider (bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek) - Set `AI_MODEL` to the specific model you want to use - Add the required API keys for your provider +- `ACCESS_CODE_LIST`: Optional access password(s), can be comma-separated for multiple passwords. + +> Warning: If you do not set `ACCESS_CODE_LIST`, anyone can access your deployed site directly, which may lead to rapid depletion of your token. It is recommended to set this option. See the [Provider Configuration Guide](./docs/ai-providers.md) for detailed setup instructions for each provider. diff --git a/README_CN.md b/README_CN.md index c7f9914..5ba3033 100644 --- a/README_CN.md +++ b/README_CN.md @@ -149,6 +149,9 @@ cp env.example .env.local - 将 `AI_PROVIDER` 设置为您选择的提供商(bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek) - 将 `AI_MODEL` 设置为您要使用的特定模型 - 添加您的提供商所需的API密钥 +- `ACCESS_CODE_LIST` 访问密码,可选,可以使用逗号隔开多个密码。 + +> 警告:如果不填写 `ACCESS_CODE_LIST`,则任何人都可以直接使用你部署后的网站,可能会导致你的 token 被急速消耗完毕,建议填写此选项。 详细设置说明请参阅[提供商配置指南](./docs/ai-providers.md)。 diff --git a/README_JA.md b/README_JA.md index ac83cdc..a80dd6f 100644 --- a/README_JA.md +++ b/README_JA.md @@ -149,6 +149,9 @@ cp env.example .env.local - `AI_PROVIDER`を選択したプロバイダーに設定(bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek) - `AI_MODEL`を使用する特定のモデルに設定 - プロバイダーに必要なAPIキーを追加 +- `ACCESS_CODE_LIST` アクセスパスワード(オプション)。カンマ区切りで複数のパスワードを指定できます。 + +> 警告:`ACCESS_CODE_LIST`を設定しない場合、誰でもデプロイされたサイトに直接アクセスできるため、トークンが急速に消費される可能性があります。このオプションを設定することをお勧めします。 詳細な設定手順については[プロバイダー設定ガイド](./docs/ai-providers.md)を参照してください。 diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index 4b3a279..15440f9 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -62,6 +62,18 @@ function createCachedStreamResponse(xml: string): Response { // Inner handler function async function handleChatRequest(req: Request): Promise { + // Check for access code + const accessCodes = process.env.ACCESS_CODE_LIST?.split(',').map(code => code.trim()).filter(Boolean) || []; + if (accessCodes.length > 0) { + const accessCodeHeader = req.headers.get('x-access-code'); + if (!accessCodeHeader || !accessCodes.includes(accessCodeHeader)) { + return Response.json( + { error: 'Invalid or missing access code. Please configure it in Settings.' }, + { status: 401 } + ); + } + } + const { messages, xml, sessionId } = await req.json(); // Get user IP for Langfuse tracking diff --git a/app/api/config/route.ts b/app/api/config/route.ts new file mode 100644 index 0000000..9d92b90 --- /dev/null +++ b/app/api/config/route.ts @@ -0,0 +1,9 @@ +import { NextResponse } from "next/server"; + +export async function GET() { + const accessCodes = process.env.ACCESS_CODE_LIST?.split(',').map(code => code.trim()).filter(Boolean) || []; + + return NextResponse.json({ + accessCodeRequired: accessCodes.length > 0, + }); +} diff --git a/components/chat-message-display.tsx b/components/chat-message-display.tsx index d461569..643513f 100644 --- a/components/chat-message-display.tsx +++ b/components/chat-message-display.tsx @@ -64,7 +64,6 @@ const getMessageTextContent = (message: UIMessage): string => { interface ChatMessageDisplayProps { messages: UIMessage[]; - error?: Error | null; setInput: (input: string) => void; setFiles: (files: File[]) => void; sessionId?: string; @@ -74,7 +73,6 @@ interface ChatMessageDisplayProps { export function ChatMessageDisplay({ messages, - error, setInput, setFiles, sessionId, @@ -391,6 +389,8 @@ export function ChatMessageDisplay({ className={`px-4 py-3 text-sm leading-relaxed ${ message.role === "user" ? "bg-primary text-primary-foreground rounded-2xl rounded-br-md shadow-sm" + : message.role === "system" + ? "bg-destructive/10 text-destructive border border-destructive/20 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" : ""}`} onClick={() => { @@ -501,11 +501,6 @@ export function ChatMessageDisplay({ })} )} - {error && ( -
- Error: {error.message} -
- )}
); diff --git a/components/chat-panel.tsx b/components/chat-panel.tsx index 3db4703..0ff4184 100644 --- a/components/chat-panel.tsx +++ b/components/chat-panel.tsx @@ -4,7 +4,7 @@ import type React from "react"; import { useRef, useEffect, useState } from "react"; import { flushSync } from "react-dom"; import { FaGithub } from "react-icons/fa"; -import { PanelRightClose, PanelRightOpen } from "lucide-react"; +import { PanelRightClose, PanelRightOpen, Settings } from "lucide-react"; import Link from "next/link"; import Image from "next/image"; @@ -16,6 +16,7 @@ import { useDiagram } from "@/contexts/diagram-context"; import { replaceNodes, formatXML, validateMxCellStructure } from "@/lib/utils"; import { ButtonWithTooltip } from "@/components/button-with-tooltip"; import { Toaster } from "sonner"; +import { SettingsDialog, STORAGE_ACCESS_CODE_KEY } from "@/components/settings-dialog"; interface ChatPanelProps { isVisible: boolean; @@ -61,8 +62,18 @@ export default function ChatPanel({ const [files, setFiles] = useState([]); const [showHistory, setShowHistory] = useState(false); + const [showSettingsDialog, setShowSettingsDialog] = useState(false); + const [accessCodeRequired, setAccessCodeRequired] = useState(false); const [input, setInput] = useState(""); + // Check if access code is required on mount + useEffect(() => { + fetch("/api/config") + .then((res) => res.json()) + .then((data) => setAccessCodeRequired(data.accessCodeRequired)) + .catch(() => setAccessCodeRequired(false)); + }, []); + // Generate a unique session ID for Langfuse tracing const [sessionId, setSessionId] = useState(() => `session-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`); @@ -168,7 +179,27 @@ Please retry with an adjusted search pattern or use display_diagram if retries a } }, onError: (error) => { - console.error("Chat error:", error); + // Silence access code error in console since it's handled by UI + if (!error.message.includes("Invalid or missing access code")) { + console.error("Chat error:", error); + } + + // Add system message for error so it can be cleared + setMessages((currentMessages) => { + const errorMessage = { + id: `error-${Date.now()}`, + role: 'system' as const, + content: error.message, + parts: [{ type: 'text' as const, text: error.message }] + }; + return [...currentMessages, errorMessage]; + }); + + if (error.message.includes("Invalid or missing access code")) { + // Show settings button and open dialog to help user fix it + setAccessCodeRequired(true); + setShowSettingsDialog(true); + } }, }); @@ -215,6 +246,7 @@ Please retry with an adjusted search pattern or use display_diagram if retries a const messageIndex = messages.length; xmlSnapshotsRef.current.set(messageIndex, chartXml); + const accessCode = localStorage.getItem(STORAGE_ACCESS_CODE_KEY) || ""; sendMessage( { parts }, { @@ -222,6 +254,9 @@ Please retry with an adjusted search pattern or use display_diagram if retries a xml: chartXml, sessionId, }, + headers: { + "x-access-code": accessCode, + }, } ); @@ -426,6 +461,17 @@ Please retry with an adjusted search pattern or use display_diagram if retries a > + {accessCodeRequired && ( + setShowSettingsDialog(true)} + className="hover:bg-accent" + > + + + )} + +
); } diff --git a/components/settings-dialog.tsx b/components/settings-dialog.tsx new file mode 100644 index 0000000..9d6808e --- /dev/null +++ b/components/settings-dialog.tsx @@ -0,0 +1,83 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, + DialogDescription, +} from "@/components/ui/dialog"; + +interface SettingsDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +export const STORAGE_ACCESS_CODE_KEY = "next-ai-draw-io-access-code"; + +export function SettingsDialog({ + open, + onOpenChange, +}: SettingsDialogProps) { + const [accessCode, setAccessCode] = useState(""); + + useEffect(() => { + if (open) { + const storedCode = localStorage.getItem(STORAGE_ACCESS_CODE_KEY) || ""; + setAccessCode(storedCode); + } + }, [open]); + + const handleSave = () => { + localStorage.setItem(STORAGE_ACCESS_CODE_KEY, accessCode.trim()); + onOpenChange(false); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + e.preventDefault(); + handleSave(); + } + }; + + return ( + + + + Settings + + Configure your access settings. + + +
+
+ + setAccessCode(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="Enter access code" + autoComplete="off" + /> +

+ Required if the server has enabled access control. +

+
+
+ + + + +
+
+ ); +} diff --git a/env.example b/env.example index e654d91..7514843 100644 --- a/env.example +++ b/env.example @@ -47,3 +47,6 @@ AI_MODEL=global.anthropic.claude-sonnet-4-5-20250929-v1:0 # LANGFUSE_PUBLIC_KEY=pk-lf-... # LANGFUSE_SECRET_KEY=sk-lf-... # LANGFUSE_BASEURL=https://cloud.langfuse.com # EU region, use https://us.cloud.langfuse.com for US + +# Access Control (Optional) +# ACCESS_CODE_LIST=your-secret-code,another-code