From 226c33667125df309432f62b3c1d597a4efef3e4 Mon Sep 17 00:00:00 2001 From: Biki Kalita <86558912+Biki-dev@users.noreply.github.com> Date: Sun, 28 Dec 2025 18:46:10 +0530 Subject: [PATCH] feat: move History and Download buttons to Settings dialog for cleaner chat interface (#442) * fix: move History and Download buttons to Settings dialog for cleaner chat interface * fix: cleanup unused imports/props, add i18n for diagram style * fix: use npx directly to avoid package-lock.json changes in CI --------- Co-authored-by: dayuan.jiang --- .github/workflows/auto-format.yml | 5 +- components/chat-input.tsx | 175 ++++++-------- components/chat-panel.tsx | 372 +++++++++++++----------------- components/settings-dialog.tsx | 60 ++++- lib/i18n/dictionaries/en.json | 8 +- lib/i18n/dictionaries/ja.json | 8 +- lib/i18n/dictionaries/zh.json | 8 +- 7 files changed, 307 insertions(+), 329 deletions(-) diff --git a/.github/workflows/auto-format.yml b/.github/workflows/auto-format.yml index 7c05657..03a1ca9 100644 --- a/.github/workflows/auto-format.yml +++ b/.github/workflows/auto-format.yml @@ -22,11 +22,8 @@ jobs: with: node-version: '24' - - name: Install Biome - run: npm install --save-dev @biomejs/biome - - name: Run Biome format - run: npx @biomejs/biome check --write --no-errors-on-unmatched . + run: npx @biomejs/biome@latest check --write --no-errors-on-unmatched . - name: Check for changes id: changes diff --git a/components/chat-input.tsx b/components/chat-input.tsx index 774b6f2..3c45d17 100644 --- a/components/chat-input.tsx +++ b/components/chat-input.tsx @@ -17,14 +17,9 @@ import { HistoryDialog } from "@/components/history-dialog" import { ModelSelector } from "@/components/model-selector" import { ResetWarningModal } from "@/components/reset-warning-modal" import { SaveDialog } from "@/components/save-dialog" + import { Button } from "@/components/ui/button" -import { Switch } from "@/components/ui/switch" import { Textarea } from "@/components/ui/textarea" -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/ui/tooltip" import { useDiagram } from "@/contexts/diagram-context" import { useDictionary } from "@/hooks/use-dictionary" import { formatMessage } from "@/lib/i18n/utils" @@ -152,12 +147,9 @@ interface ChatInputProps { File, { text: string; charCount: number; isExtracting: boolean } > - showHistory?: boolean - onToggleHistory?: (show: boolean) => void + sessionId?: string error?: Error | null - minimalStyle?: boolean - onMinimalStyleChange?: (value: boolean) => void // Model selector props models?: FlattenedModel[] selectedModelId?: string @@ -175,12 +167,8 @@ export function ChatInput({ files = [], onFileChange = () => {}, pdfData = new Map(), - showHistory = false, - onToggleHistory = () => {}, sessionId, error = null, - minimalStyle = false, - onMinimalStyleChange = () => {}, models = [], selectedModelId, onModelSelect = () => {}, @@ -188,16 +176,14 @@ export function ChatInput({ onConfigureModels = () => {}, }: ChatInputProps) { const dict = useDictionary() - const { - diagramHistory, - saveDiagramToFile, - showSaveDialog, - setShowSaveDialog, - } = useDiagram() + const { diagramHistory, saveDiagramToFile } = useDiagram() + const textareaRef = useRef(null) const fileInputRef = useRef(null) const [isDragging, setIsDragging] = useState(false) const [showClearDialog, setShowClearDialog] = useState(false) + const [showHistory, setShowHistory] = useState(false) + const [showSaveDialog, setShowSaveDialog] = useState(false) // Allow retry when there's an error (even if status is still "streaming" or "submitted") const isDisabled = (status === "streaming" || status === "submitted") && !error @@ -385,99 +371,58 @@ export function ChatInput({ onOpenChange={setShowClearDialog} onClear={handleClear} /> - - - - - -
- - -
-
- - {dict.chat.minimalTooltip} - -
- onToggleHistory(true)} - disabled={isDisabled || diagramHistory.length === 0} - tooltipContent={dict.chat.diagramHistory} - className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground" - > - - +
+ setShowHistory(true)} + disabled={ + isDisabled || diagramHistory.length === 0 + } + tooltipContent={dict.chat.diagramHistory} + className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground" + > + + - setShowSaveDialog(true)} - disabled={isDisabled} - tooltipContent={dict.chat.saveDiagram} - className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground" - > - - + setShowSaveDialog(true)} + disabled={isDisabled} + tooltipContent={dict.chat.saveDiagram} + className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground" + > + + - - saveDiagramToFile(filename, format, sessionId) - } - defaultFilename={`diagram-${new Date() - .toISOString() - .slice(0, 10)}`} - /> - - - - - - + + + + +
-
-
+ + + saveDiagramToFile(filename, format, sessionId) + } + defaultFilename={`diagram-${new Date() + .toISOString() + .slice(0, 10)}`} + /> ) } diff --git a/components/chat-panel.tsx b/components/chat-panel.tsx index dec0eb3..60d8e00 100644 --- a/components/chat-panel.tsx +++ b/components/chat-panel.tsx @@ -3,29 +3,21 @@ import { useChat } from "@ai-sdk/react" import { DefaultChatTransport } from "ai" import { - AlertTriangle, MessageSquarePlus, PanelRightClose, PanelRightOpen, Settings, } from "lucide-react" import Image from "next/image" -import Link from "next/link" import type React from "react" import { useCallback, useEffect, useRef, useState } from "react" import { flushSync } from "react-dom" -import { FaGithub } from "react-icons/fa" import { Toaster, toast } from "sonner" import { ButtonWithTooltip } from "@/components/button-with-tooltip" import { ChatInput } from "@/components/chat-input" import { ModelConfigDialog } from "@/components/model-config-dialog" import { ResetWarningModal } from "@/components/reset-warning-modal" import { SettingsDialog } from "@/components/settings-dialog" -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/ui/tooltip" import { useDiagram } from "@/contexts/diagram-context" import { useDiagramToolHandlers } from "@/hooks/use-diagram-tool-handlers" import { useDictionary } from "@/hooks/use-dictionary" @@ -154,7 +146,6 @@ export default function ChatPanel({ // File processing using extracted hook const { files, pdfData, handleFileChange, setFiles } = useFileProcessor() - const [showHistory, setShowHistory] = useState(false) const [showSettingsDialog, setShowSettingsDialog] = useState(false) const [showModelConfigDialog, setShowModelConfigDialog] = useState(false) @@ -247,182 +238,178 @@ export default function ChatPanel({ onExport, }) - const { - messages, - sendMessage, - addToolOutput, - stop, - status, - error, - setMessages, - } = useChat({ - transport: new DefaultChatTransport({ - api: getApiEndpoint("/api/chat"), - }), - onToolCall: async ({ toolCall }) => { - await handleToolCall({ toolCall }, addToolOutput) - }, - onError: (error) => { - // Handle server-side quota limit (429 response) - // AI SDK puts the full response body in error.message for non-OK responses - try { - const data = JSON.parse(error.message) - if (data.type === "request") { - quotaManager.showQuotaLimitToast(data.used, data.limit) - return - } - if (data.type === "token") { - quotaManager.showTokenLimitToast(data.used, data.limit) - return - } - if (data.type === "tpm") { - quotaManager.showTPMLimitToast(data.limit) - return - } - } catch { - // Not JSON, fall through to string matching for backwards compatibility - } - - // Fallback to string matching - if (error.message.includes("Daily request limit")) { - quotaManager.showQuotaLimitToast() - return - } - if (error.message.includes("Daily token limit")) { - quotaManager.showTokenLimitToast() - return - } - if ( - error.message.includes("Rate limit exceeded") || - error.message.includes("tokens per minute") - ) { - quotaManager.showTPMLimitToast() - return - } - - // 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) - // Debug: Log messages structure when error occurs - console.log("[onError] messages count:", messages.length) - messages.forEach((msg, idx) => { - console.log(`[onError] Message ${idx}:`, { - role: msg.role, - partsCount: msg.parts?.length, - }) - if (msg.parts) { - msg.parts.forEach((part: any, partIdx: number) => { - console.log( - `[onError] Part ${partIdx}:`, - JSON.stringify({ - type: part.type, - toolName: part.toolName, - hasInput: !!part.input, - inputType: typeof part.input, - inputKeys: - part.input && - typeof part.input === "object" - ? Object.keys(part.input) - : null, - }), - ) - }) + const { messages, sendMessage, addToolOutput, status, error, setMessages } = + useChat({ + transport: new DefaultChatTransport({ + api: getApiEndpoint("/api/chat"), + }), + onToolCall: async ({ toolCall }) => { + await handleToolCall({ toolCall }, addToolOutput) + }, + onError: (error) => { + // Handle server-side quota limit (429 response) + // AI SDK puts the full response body in error.message for non-OK responses + try { + const data = JSON.parse(error.message) + if (data.type === "request") { + quotaManager.showQuotaLimitToast(data.used, data.limit) + return } - }) - } - - // Translate technical errors into user-friendly messages - // The server now handles detailed error messages, so we can display them directly. - // But we still handle connection/network errors that happen before reaching the server. - let friendlyMessage = error.message - - // Simple check for network errors if message is generic - if (friendlyMessage === "Failed to fetch") { - friendlyMessage = "Network error. Please check your connection." - } - - // Truncated tool input error (model output limit too low) - if (friendlyMessage.includes("toolUse.input is invalid")) { - friendlyMessage = - "Output was truncated before the diagram could be generated. Try a simpler request or increase the maxOutputLength." - } - - // Translate image not supported error - if (friendlyMessage.includes("image content block")) { - friendlyMessage = "This model doesn't support image input." - } - - // Add system message for error so it can be cleared - setMessages((currentMessages) => { - const errorMessage = { - id: `error-${Date.now()}`, - role: "system" as const, - content: friendlyMessage, - parts: [{ type: "text" as const, text: friendlyMessage }], + if (data.type === "token") { + quotaManager.showTokenLimitToast(data.used, data.limit) + return + } + if (data.type === "tpm") { + quotaManager.showTPMLimitToast(data.limit) + return + } + } catch { + // Not JSON, fall through to string matching for backwards compatibility } - return [...currentMessages, errorMessage] - }) - if (error.message.includes("Invalid or missing access code")) { - // Show settings dialog to help user fix it - setShowSettingsDialog(true) - } - }, - onFinish: ({ message }) => { - // Track actual token usage from server metadata - const metadata = message?.metadata as - | Record - | undefined - - // DEBUG: Log finish reason to diagnose truncation - console.log("[onFinish] finishReason:", metadata?.finishReason) - }, - sendAutomaticallyWhen: ({ messages }) => { - const isInContinuationMode = partialXmlRef.current.length > 0 - - const shouldRetry = hasToolErrors( - messages as unknown as ChatMessage[], - ) - - if (!shouldRetry) { - // No error, reset retry count and clear state - autoRetryCountRef.current = 0 - continuationRetryCountRef.current = 0 - partialXmlRef.current = "" - return false - } - - // Continuation mode: limited retries for truncation handling - if (isInContinuationMode) { + // Fallback to string matching + if (error.message.includes("Daily request limit")) { + quotaManager.showQuotaLimitToast() + return + } + if (error.message.includes("Daily token limit")) { + quotaManager.showTokenLimitToast() + return + } if ( - continuationRetryCountRef.current >= - MAX_CONTINUATION_RETRY_COUNT + error.message.includes("Rate limit exceeded") || + error.message.includes("tokens per minute") ) { - toast.error( - `Continuation retry limit reached (${MAX_CONTINUATION_RETRY_COUNT}). The diagram may be too complex.`, - ) + quotaManager.showTPMLimitToast() + return + } + + // 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) + // Debug: Log messages structure when error occurs + console.log("[onError] messages count:", messages.length) + messages.forEach((msg, idx) => { + console.log(`[onError] Message ${idx}:`, { + role: msg.role, + partsCount: msg.parts?.length, + }) + if (msg.parts) { + msg.parts.forEach((part: any, partIdx: number) => { + console.log( + `[onError] Part ${partIdx}:`, + JSON.stringify({ + type: part.type, + toolName: part.toolName, + hasInput: !!part.input, + inputType: typeof part.input, + inputKeys: + part.input && + typeof part.input === "object" + ? Object.keys(part.input) + : null, + }), + ) + }) + } + }) + } + + // Translate technical errors into user-friendly messages + // The server now handles detailed error messages, so we can display them directly. + // But we still handle connection/network errors that happen before reaching the server. + let friendlyMessage = error.message + + // Simple check for network errors if message is generic + if (friendlyMessage === "Failed to fetch") { + friendlyMessage = + "Network error. Please check your connection." + } + + // Truncated tool input error (model output limit too low) + if (friendlyMessage.includes("toolUse.input is invalid")) { + friendlyMessage = + "Output was truncated before the diagram could be generated. Try a simpler request or increase the maxOutputLength." + } + + // Translate image not supported error + if (friendlyMessage.includes("image content block")) { + friendlyMessage = "This model doesn't support image input." + } + + // Add system message for error so it can be cleared + setMessages((currentMessages) => { + const errorMessage = { + id: `error-${Date.now()}`, + role: "system" as const, + content: friendlyMessage, + parts: [ + { type: "text" as const, text: friendlyMessage }, + ], + } + return [...currentMessages, errorMessage] + }) + + if (error.message.includes("Invalid or missing access code")) { + // Show settings dialog to help user fix it + setShowSettingsDialog(true) + } + }, + onFinish: ({ message }) => { + // Track actual token usage from server metadata + const metadata = message?.metadata as + | Record + | undefined + + // DEBUG: Log finish reason to diagnose truncation + console.log("[onFinish] finishReason:", metadata?.finishReason) + }, + sendAutomaticallyWhen: ({ messages }) => { + const isInContinuationMode = partialXmlRef.current.length > 0 + + const shouldRetry = hasToolErrors( + messages as unknown as ChatMessage[], + ) + + if (!shouldRetry) { + // No error, reset retry count and clear state + autoRetryCountRef.current = 0 continuationRetryCountRef.current = 0 partialXmlRef.current = "" return false } - continuationRetryCountRef.current++ - } else { - // Regular error: check retry count limit - if (autoRetryCountRef.current >= MAX_AUTO_RETRY_COUNT) { - toast.error( - `Auto-retry limit reached (${MAX_AUTO_RETRY_COUNT}). Please try again manually.`, - ) - autoRetryCountRef.current = 0 - partialXmlRef.current = "" - return false - } - // Increment retry count for actual errors - autoRetryCountRef.current++ - } - return true - }, - }) + // Continuation mode: limited retries for truncation handling + if (isInContinuationMode) { + if ( + continuationRetryCountRef.current >= + MAX_CONTINUATION_RETRY_COUNT + ) { + toast.error( + `Continuation retry limit reached (${MAX_CONTINUATION_RETRY_COUNT}). The diagram may be too complex.`, + ) + continuationRetryCountRef.current = 0 + partialXmlRef.current = "" + return false + } + continuationRetryCountRef.current++ + } else { + // Regular error: check retry count limit + if (autoRetryCountRef.current >= MAX_AUTO_RETRY_COUNT) { + toast.error( + `Auto-retry limit reached (${MAX_AUTO_RETRY_COUNT}). Please try again manually.`, + ) + autoRetryCountRef.current = 0 + partialXmlRef.current = "" + return false + } + // Increment retry count for actual errors + autoRetryCountRef.current++ + } + + return true + }, + }) // Ref to track latest messages for unload persistence const messagesRef = useRef(messages) @@ -959,18 +946,6 @@ export default function ChatPanel({ Next AI Drawio - {!isMobile && - process.env.NEXT_PUBLIC_SHOW_ABOUT_AND_NOTICE === - "true" && ( - - About - - )}
-
- - - - - - - - {dict.nav.github} - void darkMode: boolean onToggleDarkMode: () => void + minimalStyle?: boolean + onMinimalStyleChange?: (value: boolean) => void } export const STORAGE_ACCESS_CODE_KEY = "next-ai-draw-io-access-code" @@ -86,6 +87,8 @@ function SettingsContent({ onToggleDrawioUi, darkMode, onToggleDarkMode, + minimalStyle = false, + onMinimalStyleChange = () => {}, }: SettingsDialogProps) { const dict = useDictionary() const router = useRouter() @@ -348,14 +351,61 @@ function SettingsContent({ }} /> + + {/* Diagram Style */} + +
+ + + {minimalStyle + ? dict.chat.minimalStyle + : dict.chat.styledMode} + +
+
{/* Footer */}
-

- Version {process.env.APP_VERSION} -

+
+ + + {process.env.APP_VERSION} + + · + + + GitHub + + {process.env.NEXT_PUBLIC_SHOW_ABOUT_AND_NOTICE === + "true" && ( + <> + · + + + About + + + )} +
) diff --git a/lib/i18n/dictionaries/en.json b/lib/i18n/dictionaries/en.json index 26557fe..b19f1b6 100644 --- a/lib/i18n/dictionaries/en.json +++ b/lib/i18n/dictionaries/en.json @@ -98,7 +98,13 @@ "minimal": "Minimal", "sketch": "Sketch", "closeProtection": "Close Protection", - "closeProtectionDescription": "Show confirmation when leaving the page." + "closeProtectionDescription": "Show confirmation when leaving the page.", + "diagramStyle": "Diagram Style", + "diagramStyleDescription": "Toggle between minimal and styled diagram output.", + "diagramActions": "Diagram Actions", + "diagramActionsDescription": "Manage diagram history and exports", + "history": "History", + "download": "Download" }, "save": { "title": "Save Diagram", diff --git a/lib/i18n/dictionaries/ja.json b/lib/i18n/dictionaries/ja.json index 39bdac0..b5d82af 100644 --- a/lib/i18n/dictionaries/ja.json +++ b/lib/i18n/dictionaries/ja.json @@ -98,7 +98,13 @@ "minimal": "ミニマル", "sketch": "スケッチ", "closeProtection": "ページ離脱確認", - "closeProtectionDescription": "ページを離れる際に確認を表示します。" + "closeProtectionDescription": "ページを離れる際に確認を表示します。", + "diagramStyle": "ダイアグラムスタイル", + "diagramStyleDescription": "ミニマルとスタイル付きの出力を切り替えます。", + "diagramActions": "ダイアグラム操作", + "diagramActionsDescription": "ダイアグラムの履歴とエクスポートを管理", + "history": "履歴", + "download": "ダウンロード" }, "save": { "title": "ダイアグラムを保存", diff --git a/lib/i18n/dictionaries/zh.json b/lib/i18n/dictionaries/zh.json index b7868c4..51b6b64 100644 --- a/lib/i18n/dictionaries/zh.json +++ b/lib/i18n/dictionaries/zh.json @@ -98,7 +98,13 @@ "minimal": "简约", "sketch": "草图", "closeProtection": "关闭确认", - "closeProtectionDescription": "离开页面时显示确认。" + "closeProtectionDescription": "离开页面时显示确认。", + "diagramStyle": "图表样式", + "diagramStyleDescription": "切换简约与精致图表输出模式。", + "diagramActions": "图表操作", + "diagramActionsDescription": "管理图表历史记录和导出", + "history": "历史记录", + "download": "下载" }, "save": { "title": "保存图表",