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:
Twelveeee
2025-12-05 21:09:34 +08:00
committed by GitHub
parent ed29e32ba3
commit 3fb349fb3e
9 changed files with 171 additions and 10 deletions

View File

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

View File

@@ -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>
);
}

View 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>
);
}