mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-01-02 22:32:27 +08:00
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 <jdy.toh@gmail.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -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)。
|
||||
|
||||
|
||||
@@ -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)を参照してください。
|
||||
|
||||
|
||||
@@ -62,6 +62,18 @@ function createCachedStreamResponse(xml: string): Response {
|
||||
|
||||
// Inner handler function
|
||||
async function handleChatRequest(req: Request): Promise<Response> {
|
||||
// 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
|
||||
|
||||
9
app/api/config/route.ts
Normal file
9
app/api/config/route.ts
Normal file
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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({
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<div className="mx-4 mb-4 p-4 rounded-xl bg-red-50 border border-red-200 text-red-600 text-sm">
|
||||
<span className="font-medium">Error:</span> {error.message}
|
||||
</div>
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</ScrollArea>
|
||||
);
|
||||
|
||||
@@ -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<File[]>([]);
|
||||
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
|
||||
>
|
||||
<FaGithub className="w-5 h-5" />
|
||||
</a>
|
||||
{accessCodeRequired && (
|
||||
<ButtonWithTooltip
|
||||
tooltipContent="Settings"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setShowSettingsDialog(true)}
|
||||
className="hover:bg-accent"
|
||||
>
|
||||
<Settings className="h-5 w-5 text-muted-foreground" />
|
||||
</ButtonWithTooltip>
|
||||
)}
|
||||
<ButtonWithTooltip
|
||||
tooltipContent="Hide chat panel (Ctrl+B)"
|
||||
variant="ghost"
|
||||
@@ -443,7 +489,6 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
||||
<main className="flex-1 overflow-hidden">
|
||||
<ChatMessageDisplay
|
||||
messages={messages}
|
||||
error={error}
|
||||
setInput={setInput}
|
||||
setFiles={handleFileChange}
|
||||
sessionId={sessionId}
|
||||
@@ -473,6 +518,11 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
||||
error={error}
|
||||
/>
|
||||
</footer>
|
||||
|
||||
<SettingsDialog
|
||||
open={showSettingsDialog}
|
||||
onOpenChange={setShowSettingsDialog}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
83
components/settings-dialog.tsx
Normal file
83
components/settings-dialog.tsx
Normal file
@@ -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 (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Settings</DialogTitle>
|
||||
<DialogDescription>
|
||||
Configure your access settings.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-2">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
||||
Access Code
|
||||
</label>
|
||||
<Input
|
||||
type="password"
|
||||
value={accessCode}
|
||||
onChange={(e) => setAccessCode(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Enter access code"
|
||||
autoComplete="off"
|
||||
/>
|
||||
<p className="text-[0.8rem] text-muted-foreground">
|
||||
Required if the server has enabled access control.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSave}>Save</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user