2025-12-06 12:46:40 +09:00
|
|
|
"use client"
|
2025-03-19 07:20:22 +00:00
|
|
|
|
2025-12-06 12:46:40 +09:00
|
|
|
import { useChat } from "@ai-sdk/react"
|
2025-12-11 09:47:30 +09:00
|
|
|
import { DefaultChatTransport } from "ai"
|
2025-12-05 22:42:39 +09:00
|
|
|
import {
|
2025-12-08 20:26:51 +09:00
|
|
|
AlertTriangle,
|
2025-12-12 06:38:18 +05:30
|
|
|
MessageSquarePlus,
|
2025-12-05 22:42:39 +09:00
|
|
|
PanelRightClose,
|
|
|
|
|
PanelRightOpen,
|
|
|
|
|
Settings,
|
2025-12-06 12:46:40 +09:00
|
|
|
} from "lucide-react"
|
|
|
|
|
import Image from "next/image"
|
|
|
|
|
import Link from "next/link"
|
|
|
|
|
import type React from "react"
|
2025-12-07 01:39:09 +09:00
|
|
|
import { useCallback, useEffect, useRef, useState } from "react"
|
2025-12-06 12:46:40 +09:00
|
|
|
import { flushSync } from "react-dom"
|
|
|
|
|
import { FaGithub } from "react-icons/fa"
|
2025-12-08 14:26:01 +09:00
|
|
|
import { Toaster, toast } from "sonner"
|
2025-12-06 12:46:40 +09:00
|
|
|
import { ButtonWithTooltip } from "@/components/button-with-tooltip"
|
|
|
|
|
import { ChatInput } from "@/components/chat-input"
|
feat: multi-provider model configuration with UI/UX improvements (#355)
* feat: add multi-provider model configuration
- Add model config dialog for managing multiple AI providers
- Support for OpenAI, Anthropic, Google, Azure, Bedrock, OpenRouter, DeepSeek, SiliconFlow, Ollama, and AI Gateway
- Add model selector dropdown in chat panel header
- Add API key validation endpoint
- Add custom model ID input with keyboard navigation
- Fix hover highlight in Command component
- Add suggested models for each provider including latest Claude 4.5 series
- Store configuration locally in browser
* feat: improve model config UI and move selector to chat input
- Move model selector from header to chat input (left of send button)
- Add per-model validation status (queued, running, valid, invalid)
- Filter model selector to only show verified models
- Add editable model IDs in config dialog
- Add custom model input field alongside suggested models dropdown
- Fix hover states on provider buttons and select triggers
- Update OpenAI suggested models with GPT-5 series
- Add alert-dialog component for delete confirmation
* refactor: revert shadcn component changes, apply hover fix at usage site
* feat: add AWS credentials support for Bedrock provider
- Add AWS Access Key ID, Secret Access Key, Region fields for Bedrock
- Show different credential fields based on provider type
- Update validation API to handle Bedrock with AWS credentials
- Add region selector with common AWS regions
* fix: reset Test button after validation completes
* fix: reset validation button to Test after success
* fix: complete bedrock support and UI/UX improvements
- Add bedrock to ALLOWED_CLIENT_PROVIDERS for client credentials
- Pass AWS credentials through full chain (headers → API → provider)
- Replace non-existent GPT-5 models with real ones (o1, o3-mini)
- Add accessibility: aria-labels, focus-visible rings, inline errors
- Add more AWS regions (Ohio, London, Paris, Mumbai, Seoul, São Paulo)
- Fix setTimeout cleanup with useRef on component unmount
- Fix TypeScript type consistency in getSelectedAIConfig fallback
* chore: remove unused code
- Remove unused setAccessCodeRequired state in chat-panel.tsx
- Remove unused getSelectedModel export in model-config.ts
* fix: UI/UX improvements for model configuration dialog
- Add gradient header styling with icon badge
- Change Configuration section icon from Key to Settings2
- Add duplicate model detection with warning banner and inline removal
- Filter out already-added models from suggestions dropdown
- Add type-to-confirm for deleting providers with 3+ models
- Enhance delete confirmation dialog with warning icon
- Improve model selector discoverability (show model name + chevron)
- Add truncation for long model names with title tooltip
- Remove AI provider settings from Settings dialog (now in Model Config)
- Extract ValidationButton into reusable component
* fix: prevent duplicate model IDs within same provider
- Block adding model if ID already exists in provider
- Block editing model ID to match existing model in provider
* fix: improve duplicate model ID notifications
- Add toast notification when trying to add duplicate model
- Allow free typing when editing model ID, validate on blur
- Show warning toast instead of blocking input
* fix: improve duplicate model validation UX in config dialog
- Add inline error display for duplicate model IDs
- Show red border on input when error exists
- Validate on blur with shake animation for edit errors
- Prevent saving empty model names
- Clear errors when user starts typing
- Simplify error styling (small red text, no heavy chips)
2025-12-22 22:36:36 +09:00
|
|
|
import { ModelConfigDialog } from "@/components/model-config-dialog"
|
2025-12-12 06:38:18 +05:30
|
|
|
import { ResetWarningModal } from "@/components/reset-warning-modal"
|
2025-12-11 14:28:02 +09:00
|
|
|
import { SettingsDialog } from "@/components/settings-dialog"
|
2025-12-22 21:54:25 +08:00
|
|
|
import {
|
|
|
|
|
Tooltip,
|
|
|
|
|
TooltipContent,
|
|
|
|
|
TooltipTrigger,
|
|
|
|
|
} from "@/components/ui/tooltip"
|
2025-12-11 14:28:02 +09:00
|
|
|
import { useDiagram } from "@/contexts/diagram-context"
|
2025-12-20 20:18:54 +05:30
|
|
|
import { useDictionary } from "@/hooks/use-dictionary"
|
feat: multi-provider model configuration with UI/UX improvements (#355)
* feat: add multi-provider model configuration
- Add model config dialog for managing multiple AI providers
- Support for OpenAI, Anthropic, Google, Azure, Bedrock, OpenRouter, DeepSeek, SiliconFlow, Ollama, and AI Gateway
- Add model selector dropdown in chat panel header
- Add API key validation endpoint
- Add custom model ID input with keyboard navigation
- Fix hover highlight in Command component
- Add suggested models for each provider including latest Claude 4.5 series
- Store configuration locally in browser
* feat: improve model config UI and move selector to chat input
- Move model selector from header to chat input (left of send button)
- Add per-model validation status (queued, running, valid, invalid)
- Filter model selector to only show verified models
- Add editable model IDs in config dialog
- Add custom model input field alongside suggested models dropdown
- Fix hover states on provider buttons and select triggers
- Update OpenAI suggested models with GPT-5 series
- Add alert-dialog component for delete confirmation
* refactor: revert shadcn component changes, apply hover fix at usage site
* feat: add AWS credentials support for Bedrock provider
- Add AWS Access Key ID, Secret Access Key, Region fields for Bedrock
- Show different credential fields based on provider type
- Update validation API to handle Bedrock with AWS credentials
- Add region selector with common AWS regions
* fix: reset Test button after validation completes
* fix: reset validation button to Test after success
* fix: complete bedrock support and UI/UX improvements
- Add bedrock to ALLOWED_CLIENT_PROVIDERS for client credentials
- Pass AWS credentials through full chain (headers → API → provider)
- Replace non-existent GPT-5 models with real ones (o1, o3-mini)
- Add accessibility: aria-labels, focus-visible rings, inline errors
- Add more AWS regions (Ohio, London, Paris, Mumbai, Seoul, São Paulo)
- Fix setTimeout cleanup with useRef on component unmount
- Fix TypeScript type consistency in getSelectedAIConfig fallback
* chore: remove unused code
- Remove unused setAccessCodeRequired state in chat-panel.tsx
- Remove unused getSelectedModel export in model-config.ts
* fix: UI/UX improvements for model configuration dialog
- Add gradient header styling with icon badge
- Change Configuration section icon from Key to Settings2
- Add duplicate model detection with warning banner and inline removal
- Filter out already-added models from suggestions dropdown
- Add type-to-confirm for deleting providers with 3+ models
- Enhance delete confirmation dialog with warning icon
- Improve model selector discoverability (show model name + chevron)
- Add truncation for long model names with title tooltip
- Remove AI provider settings from Settings dialog (now in Model Config)
- Extract ValidationButton into reusable component
* fix: prevent duplicate model IDs within same provider
- Block adding model if ID already exists in provider
- Block editing model ID to match existing model in provider
* fix: improve duplicate model ID notifications
- Add toast notification when trying to add duplicate model
- Allow free typing when editing model ID, validate on blur
- Show warning toast instead of blocking input
* fix: improve duplicate model validation UX in config dialog
- Add inline error display for duplicate model IDs
- Show red border on input when error exists
- Validate on blur with shake animation for edit errors
- Prevent saving empty model names
- Clear errors when user starts typing
- Simplify error styling (small red text, no heavy chips)
2025-12-22 22:36:36 +09:00
|
|
|
import { getSelectedAIConfig, useModelConfig } from "@/hooks/use-model-config"
|
2025-12-22 19:58:55 +05:30
|
|
|
import { getApiEndpoint } from "@/lib/base-path"
|
2025-12-11 14:28:02 +09:00
|
|
|
import { findCachedResponse } from "@/lib/cached-responses"
|
|
|
|
|
import { isPdfFile, isTextFile } from "@/lib/pdf-utils"
|
|
|
|
|
import { type FileData, useFileProcessor } from "@/lib/use-file-processor"
|
|
|
|
|
import { useQuotaManager } from "@/lib/use-quota-manager"
|
2025-12-14 14:04:44 +09:00
|
|
|
import { formatXML, isMxCellXmlComplete, wrapWithMxFile } from "@/lib/utils"
|
2025-12-11 14:28:02 +09:00
|
|
|
import { ChatMessageDisplay } from "./chat-message-display"
|
2025-12-07 01:39:09 +09:00
|
|
|
|
|
|
|
|
// localStorage keys for persistence
|
|
|
|
|
const STORAGE_MESSAGES_KEY = "next-ai-draw-io-messages"
|
|
|
|
|
const STORAGE_XML_SNAPSHOTS_KEY = "next-ai-draw-io-xml-snapshots"
|
|
|
|
|
const STORAGE_SESSION_ID_KEY = "next-ai-draw-io-session-id"
|
2025-12-10 08:21:15 +08:00
|
|
|
export const STORAGE_DIAGRAM_XML_KEY = "next-ai-draw-io-diagram-xml"
|
2025-03-26 00:30:00 +00:00
|
|
|
|
2025-12-18 20:14:10 +08:00
|
|
|
// sessionStorage keys
|
|
|
|
|
const SESSION_STORAGE_INPUT_KEY = "next-ai-draw-io-input"
|
|
|
|
|
|
2025-12-11 09:47:30 +09:00
|
|
|
// Type for message parts (tool calls and their states)
|
|
|
|
|
interface MessagePart {
|
|
|
|
|
type: string
|
|
|
|
|
state?: string
|
|
|
|
|
toolName?: string
|
2025-12-14 20:01:24 +09:00
|
|
|
input?: { xml?: string; [key: string]: unknown }
|
2025-12-11 09:47:30 +09:00
|
|
|
[key: string]: unknown
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface ChatMessage {
|
|
|
|
|
role: string
|
|
|
|
|
parts?: MessagePart[]
|
|
|
|
|
[key: string]: unknown
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-15 12:09:32 +09:00
|
|
|
interface ChatPanelProps {
|
2025-12-06 12:46:40 +09:00
|
|
|
isVisible: boolean
|
|
|
|
|
onToggleVisibility: () => void
|
|
|
|
|
drawioUi: "min" | "sketch"
|
|
|
|
|
onToggleDrawioUi: () => void
|
2025-12-10 08:21:15 +08:00
|
|
|
darkMode: boolean
|
|
|
|
|
onToggleDarkMode: () => void
|
2025-12-06 12:46:40 +09:00
|
|
|
isMobile?: boolean
|
2025-12-06 21:42:28 +09:00
|
|
|
onCloseProtectionChange?: (enabled: boolean) => void
|
2025-11-15 12:09:32 +09:00
|
|
|
}
|
|
|
|
|
|
2025-12-11 09:47:30 +09:00
|
|
|
// Constants for tool states
|
|
|
|
|
const TOOL_ERROR_STATE = "output-error" as const
|
|
|
|
|
const DEBUG = process.env.NODE_ENV === "development"
|
2025-12-13 23:28:41 +09:00
|
|
|
const MAX_AUTO_RETRY_COUNT = 1
|
2025-12-23 14:17:06 +09:00
|
|
|
const MAX_CONTINUATION_RETRY_COUNT = 2 // Limit for truncation continuation retries
|
2025-12-11 09:47:30 +09:00
|
|
|
|
|
|
|
|
/**
|
2025-12-11 17:56:40 +09:00
|
|
|
* Check if auto-resubmit should happen based on tool errors.
|
2025-12-14 12:34:34 +09:00
|
|
|
* Only checks the LAST tool part (most recent tool call), not all tool parts.
|
2025-12-11 09:47:30 +09:00
|
|
|
*/
|
2025-12-11 17:56:40 +09:00
|
|
|
function hasToolErrors(messages: ChatMessage[]): boolean {
|
2025-12-11 09:47:30 +09:00
|
|
|
const lastMessage = messages[messages.length - 1]
|
|
|
|
|
if (!lastMessage || lastMessage.role !== "assistant") {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const toolParts =
|
|
|
|
|
(lastMessage.parts as MessagePart[] | undefined)?.filter((part) =>
|
|
|
|
|
part.type?.startsWith("tool-"),
|
|
|
|
|
) || []
|
|
|
|
|
|
|
|
|
|
if (toolParts.length === 0) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-14 12:34:34 +09:00
|
|
|
const lastToolPart = toolParts[toolParts.length - 1]
|
|
|
|
|
return lastToolPart?.state === TOOL_ERROR_STATE
|
2025-12-11 09:47:30 +09:00
|
|
|
}
|
|
|
|
|
|
2025-12-03 16:47:45 +09:00
|
|
|
export default function ChatPanel({
|
|
|
|
|
isVisible,
|
|
|
|
|
onToggleVisibility,
|
2025-12-05 23:10:48 +09:00
|
|
|
drawioUi,
|
|
|
|
|
onToggleDrawioUi,
|
2025-12-10 08:21:15 +08:00
|
|
|
darkMode,
|
|
|
|
|
onToggleDarkMode,
|
2025-12-05 23:25:59 +09:00
|
|
|
isMobile = false,
|
2025-12-06 21:42:28 +09:00
|
|
|
onCloseProtectionChange,
|
2025-12-03 16:47:45 +09:00
|
|
|
}: ChatPanelProps) {
|
2025-03-26 00:30:00 +00:00
|
|
|
const {
|
|
|
|
|
loadDiagram: onDisplayChart,
|
|
|
|
|
handleExport: onExport,
|
2025-12-03 21:58:48 +09:00
|
|
|
handleExportWithoutHistory,
|
2025-03-26 00:30:00 +00:00
|
|
|
resolverRef,
|
2025-04-03 15:10:53 +00:00
|
|
|
chartXML,
|
2025-03-27 08:09:22 +00:00
|
|
|
clearDiagram,
|
2025-12-06 12:46:40 +09:00
|
|
|
} = useDiagram()
|
2025-03-19 08:16:44 +00:00
|
|
|
|
2025-12-20 20:18:54 +05:30
|
|
|
const dict = useDictionary()
|
|
|
|
|
|
2025-12-03 21:58:48 +09:00
|
|
|
const onFetchChart = (saveToHistory = true) => {
|
2025-11-10 10:28:37 +09:00
|
|
|
return Promise.race([
|
|
|
|
|
new Promise<string>((resolve) => {
|
|
|
|
|
if (resolverRef && "current" in resolverRef) {
|
2025-12-06 12:46:40 +09:00
|
|
|
resolverRef.current = resolve
|
2025-11-10 10:28:37 +09:00
|
|
|
}
|
2025-12-03 21:58:48 +09:00
|
|
|
if (saveToHistory) {
|
2025-12-06 12:46:40 +09:00
|
|
|
onExport()
|
2025-12-03 21:58:48 +09:00
|
|
|
} else {
|
2025-12-06 12:46:40 +09:00
|
|
|
handleExportWithoutHistory()
|
2025-12-03 21:58:48 +09:00
|
|
|
}
|
2025-11-10 10:28:37 +09:00
|
|
|
}),
|
|
|
|
|
new Promise<string>((_, reject) =>
|
2025-12-03 16:47:45 +09:00
|
|
|
setTimeout(
|
|
|
|
|
() =>
|
|
|
|
|
reject(
|
2025-12-06 12:46:40 +09:00
|
|
|
new Error(
|
|
|
|
|
"Chart export timed out after 10 seconds",
|
|
|
|
|
),
|
2025-12-03 16:47:45 +09:00
|
|
|
),
|
2025-12-06 12:46:40 +09:00
|
|
|
10000,
|
|
|
|
|
),
|
2025-12-03 16:47:45 +09:00
|
|
|
),
|
2025-12-06 12:46:40 +09:00
|
|
|
])
|
|
|
|
|
}
|
2025-03-25 04:23:38 +00:00
|
|
|
|
2025-12-11 14:28:02 +09:00
|
|
|
// File processing using extracted hook
|
|
|
|
|
const { files, pdfData, handleFileChange, setFiles } = useFileProcessor()
|
|
|
|
|
|
2025-12-06 12:46:40 +09:00
|
|
|
const [showHistory, setShowHistory] = useState(false)
|
|
|
|
|
const [showSettingsDialog, setShowSettingsDialog] = useState(false)
|
feat: multi-provider model configuration with UI/UX improvements (#355)
* feat: add multi-provider model configuration
- Add model config dialog for managing multiple AI providers
- Support for OpenAI, Anthropic, Google, Azure, Bedrock, OpenRouter, DeepSeek, SiliconFlow, Ollama, and AI Gateway
- Add model selector dropdown in chat panel header
- Add API key validation endpoint
- Add custom model ID input with keyboard navigation
- Fix hover highlight in Command component
- Add suggested models for each provider including latest Claude 4.5 series
- Store configuration locally in browser
* feat: improve model config UI and move selector to chat input
- Move model selector from header to chat input (left of send button)
- Add per-model validation status (queued, running, valid, invalid)
- Filter model selector to only show verified models
- Add editable model IDs in config dialog
- Add custom model input field alongside suggested models dropdown
- Fix hover states on provider buttons and select triggers
- Update OpenAI suggested models with GPT-5 series
- Add alert-dialog component for delete confirmation
* refactor: revert shadcn component changes, apply hover fix at usage site
* feat: add AWS credentials support for Bedrock provider
- Add AWS Access Key ID, Secret Access Key, Region fields for Bedrock
- Show different credential fields based on provider type
- Update validation API to handle Bedrock with AWS credentials
- Add region selector with common AWS regions
* fix: reset Test button after validation completes
* fix: reset validation button to Test after success
* fix: complete bedrock support and UI/UX improvements
- Add bedrock to ALLOWED_CLIENT_PROVIDERS for client credentials
- Pass AWS credentials through full chain (headers → API → provider)
- Replace non-existent GPT-5 models with real ones (o1, o3-mini)
- Add accessibility: aria-labels, focus-visible rings, inline errors
- Add more AWS regions (Ohio, London, Paris, Mumbai, Seoul, São Paulo)
- Fix setTimeout cleanup with useRef on component unmount
- Fix TypeScript type consistency in getSelectedAIConfig fallback
* chore: remove unused code
- Remove unused setAccessCodeRequired state in chat-panel.tsx
- Remove unused getSelectedModel export in model-config.ts
* fix: UI/UX improvements for model configuration dialog
- Add gradient header styling with icon badge
- Change Configuration section icon from Key to Settings2
- Add duplicate model detection with warning banner and inline removal
- Filter out already-added models from suggestions dropdown
- Add type-to-confirm for deleting providers with 3+ models
- Enhance delete confirmation dialog with warning icon
- Improve model selector discoverability (show model name + chevron)
- Add truncation for long model names with title tooltip
- Remove AI provider settings from Settings dialog (now in Model Config)
- Extract ValidationButton into reusable component
* fix: prevent duplicate model IDs within same provider
- Block adding model if ID already exists in provider
- Block editing model ID to match existing model in provider
* fix: improve duplicate model ID notifications
- Add toast notification when trying to add duplicate model
- Allow free typing when editing model ID, validate on blur
- Show warning toast instead of blocking input
* fix: improve duplicate model validation UX in config dialog
- Add inline error display for duplicate model IDs
- Show red border on input when error exists
- Validate on blur with shake animation for edit errors
- Prevent saving empty model names
- Clear errors when user starts typing
- Simplify error styling (small red text, no heavy chips)
2025-12-22 22:36:36 +09:00
|
|
|
const [showModelConfigDialog, setShowModelConfigDialog] = useState(false)
|
|
|
|
|
|
|
|
|
|
// Model configuration hook
|
|
|
|
|
const modelConfig = useModelConfig()
|
2025-12-06 12:46:40 +09:00
|
|
|
const [input, setInput] = useState("")
|
2025-12-08 14:26:01 +09:00
|
|
|
const [dailyRequestLimit, setDailyRequestLimit] = useState(0)
|
2025-12-08 18:56:34 +09:00
|
|
|
const [dailyTokenLimit, setDailyTokenLimit] = useState(0)
|
|
|
|
|
const [tpmLimit, setTpmLimit] = useState(0)
|
2025-12-12 06:38:18 +05:30
|
|
|
const [showNewChatDialog, setShowNewChatDialog] = useState(false)
|
2025-12-14 19:38:40 +09:00
|
|
|
const [minimalStyle, setMinimalStyle] = useState(false)
|
2025-08-31 12:54:14 +09:00
|
|
|
|
2025-12-18 20:14:10 +08:00
|
|
|
// Restore input from sessionStorage on mount (when ChatPanel remounts due to key change)
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const savedInput = sessionStorage.getItem(SESSION_STORAGE_INPUT_KEY)
|
|
|
|
|
if (savedInput) {
|
|
|
|
|
setInput(savedInput)
|
|
|
|
|
}
|
|
|
|
|
}, [])
|
|
|
|
|
|
2025-12-08 14:26:01 +09:00
|
|
|
// Check config on mount
|
2025-12-05 21:09:34 +08:00
|
|
|
useEffect(() => {
|
2025-12-22 19:58:55 +05:30
|
|
|
fetch(getApiEndpoint("/api/config"))
|
2025-12-05 21:09:34 +08:00
|
|
|
.then((res) => res.json())
|
2025-12-08 14:26:01 +09:00
|
|
|
.then((data) => {
|
|
|
|
|
setDailyRequestLimit(data.dailyRequestLimit || 0)
|
2025-12-08 18:56:34 +09:00
|
|
|
setDailyTokenLimit(data.dailyTokenLimit || 0)
|
|
|
|
|
setTpmLimit(data.tpmLimit || 0)
|
2025-12-08 14:26:01 +09:00
|
|
|
})
|
feat: multi-provider model configuration with UI/UX improvements (#355)
* feat: add multi-provider model configuration
- Add model config dialog for managing multiple AI providers
- Support for OpenAI, Anthropic, Google, Azure, Bedrock, OpenRouter, DeepSeek, SiliconFlow, Ollama, and AI Gateway
- Add model selector dropdown in chat panel header
- Add API key validation endpoint
- Add custom model ID input with keyboard navigation
- Fix hover highlight in Command component
- Add suggested models for each provider including latest Claude 4.5 series
- Store configuration locally in browser
* feat: improve model config UI and move selector to chat input
- Move model selector from header to chat input (left of send button)
- Add per-model validation status (queued, running, valid, invalid)
- Filter model selector to only show verified models
- Add editable model IDs in config dialog
- Add custom model input field alongside suggested models dropdown
- Fix hover states on provider buttons and select triggers
- Update OpenAI suggested models with GPT-5 series
- Add alert-dialog component for delete confirmation
* refactor: revert shadcn component changes, apply hover fix at usage site
* feat: add AWS credentials support for Bedrock provider
- Add AWS Access Key ID, Secret Access Key, Region fields for Bedrock
- Show different credential fields based on provider type
- Update validation API to handle Bedrock with AWS credentials
- Add region selector with common AWS regions
* fix: reset Test button after validation completes
* fix: reset validation button to Test after success
* fix: complete bedrock support and UI/UX improvements
- Add bedrock to ALLOWED_CLIENT_PROVIDERS for client credentials
- Pass AWS credentials through full chain (headers → API → provider)
- Replace non-existent GPT-5 models with real ones (o1, o3-mini)
- Add accessibility: aria-labels, focus-visible rings, inline errors
- Add more AWS regions (Ohio, London, Paris, Mumbai, Seoul, São Paulo)
- Fix setTimeout cleanup with useRef on component unmount
- Fix TypeScript type consistency in getSelectedAIConfig fallback
* chore: remove unused code
- Remove unused setAccessCodeRequired state in chat-panel.tsx
- Remove unused getSelectedModel export in model-config.ts
* fix: UI/UX improvements for model configuration dialog
- Add gradient header styling with icon badge
- Change Configuration section icon from Key to Settings2
- Add duplicate model detection with warning banner and inline removal
- Filter out already-added models from suggestions dropdown
- Add type-to-confirm for deleting providers with 3+ models
- Enhance delete confirmation dialog with warning icon
- Improve model selector discoverability (show model name + chevron)
- Add truncation for long model names with title tooltip
- Remove AI provider settings from Settings dialog (now in Model Config)
- Extract ValidationButton into reusable component
* fix: prevent duplicate model IDs within same provider
- Block adding model if ID already exists in provider
- Block editing model ID to match existing model in provider
* fix: improve duplicate model ID notifications
- Add toast notification when trying to add duplicate model
- Allow free typing when editing model ID, validate on blur
- Show warning toast instead of blocking input
* fix: improve duplicate model validation UX in config dialog
- Add inline error display for duplicate model IDs
- Show red border on input when error exists
- Validate on blur with shake animation for edit errors
- Prevent saving empty model names
- Clear errors when user starts typing
- Simplify error styling (small red text, no heavy chips)
2025-12-22 22:36:36 +09:00
|
|
|
.catch(() => {})
|
2025-12-06 12:46:40 +09:00
|
|
|
}, [])
|
2025-12-05 21:09:34 +08:00
|
|
|
|
2025-12-11 14:28:02 +09:00
|
|
|
// Quota management using extracted hook
|
|
|
|
|
const quotaManager = useQuotaManager({
|
|
|
|
|
dailyRequestLimit,
|
|
|
|
|
dailyTokenLimit,
|
|
|
|
|
tpmLimit,
|
|
|
|
|
})
|
2025-12-08 18:56:34 +09:00
|
|
|
|
2025-12-07 01:39:09 +09:00
|
|
|
// Generate a unique session ID for Langfuse tracing (restore from localStorage if available)
|
|
|
|
|
const [sessionId, setSessionId] = useState(() => {
|
|
|
|
|
if (typeof window !== "undefined") {
|
|
|
|
|
const saved = localStorage.getItem(STORAGE_SESSION_ID_KEY)
|
|
|
|
|
if (saved) return saved
|
|
|
|
|
}
|
|
|
|
|
return `session-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
|
|
|
|
|
})
|
2025-12-05 21:15:02 +09:00
|
|
|
|
2025-12-04 22:56:59 +09:00
|
|
|
// Store XML snapshots for each user message (keyed by message index)
|
2025-12-06 12:46:40 +09:00
|
|
|
const xmlSnapshotsRef = useRef<Map<number, string>>(new Map())
|
2025-12-04 22:56:59 +09:00
|
|
|
|
2025-12-07 01:39:09 +09:00
|
|
|
// Flag to track if we've restored from localStorage
|
|
|
|
|
const hasRestoredRef = useRef(false)
|
|
|
|
|
|
2025-12-05 00:47:27 +09:00
|
|
|
// Ref to track latest chartXML for use in callbacks (avoids stale closure)
|
2025-12-06 12:46:40 +09:00
|
|
|
const chartXMLRef = useRef(chartXML)
|
2025-12-05 00:47:27 +09:00
|
|
|
useEffect(() => {
|
2025-12-06 12:46:40 +09:00
|
|
|
chartXMLRef.current = chartXML
|
|
|
|
|
}, [chartXML])
|
2025-12-05 00:47:27 +09:00
|
|
|
|
2025-12-07 00:40:11 +09:00
|
|
|
// Ref to hold stop function for use in onToolCall (avoids stale closure)
|
|
|
|
|
const stopRef = useRef<(() => void) | null>(null)
|
|
|
|
|
|
2025-12-11 17:56:40 +09:00
|
|
|
// Ref to track consecutive auto-retry count (reset on user action)
|
|
|
|
|
const autoRetryCountRef = useRef(0)
|
2025-12-23 14:17:06 +09:00
|
|
|
// Ref to track continuation retry count (for truncation handling)
|
|
|
|
|
const continuationRetryCountRef = useRef(0)
|
2025-12-11 17:56:40 +09:00
|
|
|
|
2025-12-14 12:34:34 +09:00
|
|
|
// Ref to accumulate partial XML when output is truncated due to maxOutputTokens
|
|
|
|
|
// When partialXmlRef.current.length > 0, we're in continuation mode
|
|
|
|
|
const partialXmlRef = useRef<string>("")
|
|
|
|
|
|
2025-12-11 17:18:48 +05:30
|
|
|
// Persist processed tool call IDs so collapsing the chat doesn't replay old tool outputs
|
|
|
|
|
const processedToolCallsRef = useRef<Set<string>>(new Set())
|
|
|
|
|
|
2025-12-15 21:28:31 +09:00
|
|
|
// Store original XML for edit_diagram streaming - shared between streaming preview and tool handler
|
|
|
|
|
// Key: toolCallId, Value: original XML before any operations applied
|
|
|
|
|
const editDiagramOriginalXmlRef = useRef<Map<string, string>>(new Map())
|
|
|
|
|
|
2025-12-14 21:23:14 +09:00
|
|
|
// Debounce timeout for localStorage writes (prevents blocking during streaming)
|
|
|
|
|
const localStorageDebounceRef = useRef<ReturnType<
|
|
|
|
|
typeof setTimeout
|
|
|
|
|
> | null>(null)
|
|
|
|
|
const LOCAL_STORAGE_DEBOUNCE_MS = 1000 // Save at most once per second
|
|
|
|
|
|
2025-12-07 00:40:11 +09:00
|
|
|
const {
|
|
|
|
|
messages,
|
|
|
|
|
sendMessage,
|
|
|
|
|
addToolOutput,
|
|
|
|
|
stop,
|
|
|
|
|
status,
|
|
|
|
|
error,
|
|
|
|
|
setMessages,
|
|
|
|
|
} = useChat({
|
|
|
|
|
transport: new DefaultChatTransport({
|
2025-12-22 19:58:55 +05:30
|
|
|
api: getApiEndpoint("/api/chat"),
|
2025-12-07 00:40:11 +09:00
|
|
|
}),
|
|
|
|
|
async onToolCall({ toolCall }) {
|
2025-12-11 09:47:30 +09:00
|
|
|
if (DEBUG) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[onToolCall] Tool: ${toolCall.toolName}, CallId: ${toolCall.toolCallId}`,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-07 00:40:11 +09:00
|
|
|
if (toolCall.toolName === "display_diagram") {
|
|
|
|
|
const { xml } = toolCall.input as { xml: string }
|
2025-12-14 12:34:34 +09:00
|
|
|
|
2025-12-14 19:38:40 +09:00
|
|
|
// DEBUG: Log raw input to diagnose false truncation detection
|
|
|
|
|
console.log(
|
|
|
|
|
"[display_diagram] XML ending (last 100 chars):",
|
|
|
|
|
xml.slice(-100),
|
|
|
|
|
)
|
|
|
|
|
console.log("[display_diagram] XML length:", xml.length)
|
|
|
|
|
|
2025-12-14 14:04:44 +09:00
|
|
|
// Check if XML is truncated (incomplete mxCell indicates truncated output)
|
|
|
|
|
const isTruncated = !isMxCellXmlComplete(xml)
|
2025-12-14 19:38:40 +09:00
|
|
|
console.log("[display_diagram] isTruncated:", isTruncated)
|
2025-12-14 12:34:34 +09:00
|
|
|
|
|
|
|
|
if (isTruncated) {
|
|
|
|
|
// Store the partial XML for continuation via append_diagram
|
|
|
|
|
partialXmlRef.current = xml
|
|
|
|
|
|
|
|
|
|
// Tell LLM to use append_diagram to continue
|
|
|
|
|
const partialEnding = partialXmlRef.current.slice(-500)
|
|
|
|
|
addToolOutput({
|
|
|
|
|
tool: "display_diagram",
|
|
|
|
|
toolCallId: toolCall.toolCallId,
|
|
|
|
|
state: "output-error",
|
|
|
|
|
errorText: `Output was truncated due to length limits. Use the append_diagram tool to continue.
|
|
|
|
|
|
|
|
|
|
Your output ended with:
|
|
|
|
|
\`\`\`
|
|
|
|
|
${partialEnding}
|
|
|
|
|
\`\`\`
|
|
|
|
|
|
|
|
|
|
NEXT STEP: Call append_diagram with the continuation XML.
|
2025-12-14 14:04:44 +09:00
|
|
|
- Do NOT include wrapper tags or root cells (id="0", id="1")
|
2025-12-14 12:34:34 +09:00
|
|
|
- Start from EXACTLY where you stopped
|
2025-12-14 14:04:44 +09:00
|
|
|
- Complete all remaining mxCell elements`,
|
2025-12-14 12:34:34 +09:00
|
|
|
})
|
|
|
|
|
return
|
2025-12-11 09:47:30 +09:00
|
|
|
}
|
2025-12-07 00:40:11 +09:00
|
|
|
|
2025-12-14 12:34:34 +09:00
|
|
|
// Complete XML received - use it directly
|
|
|
|
|
// (continuation is now handled via append_diagram tool)
|
|
|
|
|
const finalXml = xml
|
|
|
|
|
partialXmlRef.current = "" // Reset any partial from previous truncation
|
|
|
|
|
|
2025-12-09 15:53:59 +09:00
|
|
|
// Wrap raw XML with full mxfile structure for draw.io
|
2025-12-14 12:34:34 +09:00
|
|
|
const fullXml = wrapWithMxFile(finalXml)
|
2025-12-09 15:53:59 +09:00
|
|
|
|
2025-12-07 14:38:15 +09:00
|
|
|
// loadDiagram validates and returns error if invalid
|
2025-12-09 15:53:59 +09:00
|
|
|
const validationError = onDisplayChart(fullXml)
|
2025-12-07 00:40:11 +09:00
|
|
|
|
|
|
|
|
if (validationError) {
|
|
|
|
|
console.warn(
|
|
|
|
|
"[display_diagram] Validation error:",
|
|
|
|
|
validationError,
|
|
|
|
|
)
|
|
|
|
|
// Return error to model - sendAutomaticallyWhen will trigger retry
|
2025-12-11 09:47:30 +09:00
|
|
|
if (DEBUG) {
|
|
|
|
|
console.log(
|
|
|
|
|
"[display_diagram] Adding tool output with state: output-error",
|
|
|
|
|
)
|
|
|
|
|
}
|
2025-12-07 00:40:11 +09:00
|
|
|
addToolOutput({
|
|
|
|
|
tool: "display_diagram",
|
|
|
|
|
toolCallId: toolCall.toolCallId,
|
|
|
|
|
state: "output-error",
|
2025-12-11 14:28:02 +09:00
|
|
|
errorText: `${validationError}
|
|
|
|
|
|
|
|
|
|
Please fix the XML issues and call display_diagram again with corrected XML.
|
|
|
|
|
|
|
|
|
|
Your failed XML:
|
|
|
|
|
\`\`\`xml
|
2025-12-14 12:34:34 +09:00
|
|
|
${finalXml}
|
2025-12-11 14:28:02 +09:00
|
|
|
\`\`\``,
|
2025-12-07 00:40:11 +09:00
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
// Success - diagram will be rendered by chat-message-display
|
2025-12-11 09:47:30 +09:00
|
|
|
if (DEBUG) {
|
|
|
|
|
console.log(
|
|
|
|
|
"[display_diagram] Success! Adding tool output with state: output-available",
|
|
|
|
|
)
|
|
|
|
|
}
|
2025-12-07 00:40:11 +09:00
|
|
|
addToolOutput({
|
|
|
|
|
tool: "display_diagram",
|
|
|
|
|
toolCallId: toolCall.toolCallId,
|
|
|
|
|
output: "Successfully displayed the diagram.",
|
|
|
|
|
})
|
2025-12-11 09:47:30 +09:00
|
|
|
if (DEBUG) {
|
|
|
|
|
console.log(
|
|
|
|
|
"[display_diagram] Tool output added. Diagram should be visible now.",
|
|
|
|
|
)
|
|
|
|
|
}
|
2025-12-07 00:40:11 +09:00
|
|
|
}
|
|
|
|
|
} else if (toolCall.toolName === "edit_diagram") {
|
2025-12-15 14:22:56 +09:00
|
|
|
const { operations } = toolCall.input as {
|
|
|
|
|
operations: Array<{
|
|
|
|
|
type: "update" | "add" | "delete"
|
|
|
|
|
cell_id: string
|
|
|
|
|
new_xml?: string
|
|
|
|
|
}>
|
2025-12-07 00:40:11 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let currentXml = ""
|
|
|
|
|
try {
|
2025-12-15 21:28:31 +09:00
|
|
|
// Use the original XML captured during streaming (shared with chat-message-display)
|
|
|
|
|
// This ensures we apply operations to the same base XML that streaming used
|
|
|
|
|
const originalXml = editDiagramOriginalXmlRef.current.get(
|
|
|
|
|
toolCall.toolCallId,
|
|
|
|
|
)
|
|
|
|
|
if (originalXml) {
|
|
|
|
|
currentXml = originalXml
|
2025-12-05 22:42:39 +09:00
|
|
|
} else {
|
2025-12-15 21:28:31 +09:00
|
|
|
// Fallback: use chartXML from ref if streaming didn't capture original
|
|
|
|
|
const cachedXML = chartXMLRef.current
|
|
|
|
|
if (cachedXML) {
|
|
|
|
|
currentXml = cachedXML
|
|
|
|
|
} else {
|
|
|
|
|
// Last resort: export from iframe
|
|
|
|
|
currentXml = await onFetchChart(false)
|
|
|
|
|
}
|
2025-12-06 12:46:40 +09:00
|
|
|
}
|
2025-12-05 22:42:39 +09:00
|
|
|
|
2025-12-15 14:22:56 +09:00
|
|
|
const { applyDiagramOperations } = await import(
|
|
|
|
|
"@/lib/utils"
|
|
|
|
|
)
|
|
|
|
|
const { result: editedXml, errors } =
|
|
|
|
|
applyDiagramOperations(currentXml, operations)
|
|
|
|
|
|
|
|
|
|
// Check for operation errors
|
|
|
|
|
if (errors.length > 0) {
|
|
|
|
|
const errorMessages = errors
|
|
|
|
|
.map(
|
|
|
|
|
(e) =>
|
|
|
|
|
`- ${e.type} on cell_id="${e.cellId}": ${e.message}`,
|
|
|
|
|
)
|
|
|
|
|
.join("\n")
|
|
|
|
|
|
|
|
|
|
addToolOutput({
|
|
|
|
|
tool: "edit_diagram",
|
|
|
|
|
toolCallId: toolCall.toolCallId,
|
|
|
|
|
state: "output-error",
|
|
|
|
|
errorText: `Some operations failed:\n${errorMessages}
|
|
|
|
|
|
|
|
|
|
Current diagram XML:
|
|
|
|
|
\`\`\`xml
|
|
|
|
|
${currentXml}
|
|
|
|
|
\`\`\`
|
|
|
|
|
|
|
|
|
|
Please check the cell IDs and retry.`,
|
|
|
|
|
})
|
2025-12-15 21:28:31 +09:00
|
|
|
// Clean up the shared original XML ref
|
|
|
|
|
editDiagramOriginalXmlRef.current.delete(
|
|
|
|
|
toolCall.toolCallId,
|
|
|
|
|
)
|
2025-12-15 14:22:56 +09:00
|
|
|
return
|
|
|
|
|
}
|
2025-12-07 00:40:11 +09:00
|
|
|
|
2025-12-07 14:38:15 +09:00
|
|
|
// loadDiagram validates and returns error if invalid
|
|
|
|
|
const validationError = onDisplayChart(editedXml)
|
|
|
|
|
if (validationError) {
|
|
|
|
|
console.warn(
|
|
|
|
|
"[edit_diagram] Validation error:",
|
|
|
|
|
validationError,
|
|
|
|
|
)
|
|
|
|
|
addToolOutput({
|
|
|
|
|
tool: "edit_diagram",
|
|
|
|
|
toolCallId: toolCall.toolCallId,
|
|
|
|
|
state: "output-error",
|
|
|
|
|
errorText: `Edit produced invalid XML: ${validationError}
|
|
|
|
|
|
|
|
|
|
Current diagram XML:
|
|
|
|
|
\`\`\`xml
|
|
|
|
|
${currentXml}
|
|
|
|
|
\`\`\`
|
|
|
|
|
|
2025-12-15 14:22:56 +09:00
|
|
|
Please fix the operations to avoid structural issues.`,
|
2025-12-07 14:38:15 +09:00
|
|
|
})
|
2025-12-15 21:28:31 +09:00
|
|
|
// Clean up the shared original XML ref
|
|
|
|
|
editDiagramOriginalXmlRef.current.delete(
|
|
|
|
|
toolCall.toolCallId,
|
|
|
|
|
)
|
2025-12-07 14:38:15 +09:00
|
|
|
return
|
|
|
|
|
}
|
2025-12-08 06:14:10 +05:30
|
|
|
onExport()
|
2025-12-07 00:40:11 +09:00
|
|
|
addToolOutput({
|
|
|
|
|
tool: "edit_diagram",
|
|
|
|
|
toolCallId: toolCall.toolCallId,
|
2025-12-15 14:22:56 +09:00
|
|
|
output: `Successfully applied ${operations.length} operation(s) to the diagram.`,
|
2025-12-07 00:40:11 +09:00
|
|
|
})
|
2025-12-15 21:28:31 +09:00
|
|
|
// Clean up the shared original XML ref
|
|
|
|
|
editDiagramOriginalXmlRef.current.delete(
|
|
|
|
|
toolCall.toolCallId,
|
|
|
|
|
)
|
2025-12-07 00:40:11 +09:00
|
|
|
} catch (error) {
|
|
|
|
|
console.error("[edit_diagram] Failed:", error)
|
|
|
|
|
|
|
|
|
|
const errorMessage =
|
|
|
|
|
error instanceof Error ? error.message : String(error)
|
|
|
|
|
|
|
|
|
|
addToolOutput({
|
|
|
|
|
tool: "edit_diagram",
|
|
|
|
|
toolCallId: toolCall.toolCallId,
|
|
|
|
|
state: "output-error",
|
|
|
|
|
errorText: `Edit failed: ${errorMessage}
|
2025-11-13 22:27:11 +09:00
|
|
|
|
|
|
|
|
Current diagram XML:
|
|
|
|
|
\`\`\`xml
|
2025-12-05 00:43:21 +09:00
|
|
|
${currentXml || "No XML available"}
|
2025-11-13 22:27:11 +09:00
|
|
|
\`\`\`
|
|
|
|
|
|
2025-12-15 14:22:56 +09:00
|
|
|
Please check cell IDs and retry, or use display_diagram to regenerate.`,
|
2025-12-07 00:40:11 +09:00
|
|
|
})
|
2025-12-15 21:28:31 +09:00
|
|
|
// Clean up the shared original XML ref even on error
|
|
|
|
|
editDiagramOriginalXmlRef.current.delete(
|
|
|
|
|
toolCall.toolCallId,
|
|
|
|
|
)
|
2025-12-05 22:42:39 +09:00
|
|
|
}
|
2025-12-14 12:34:34 +09:00
|
|
|
} else if (toolCall.toolName === "append_diagram") {
|
|
|
|
|
const { xml } = toolCall.input as { xml: string }
|
|
|
|
|
|
|
|
|
|
// Detect if LLM incorrectly started fresh instead of continuing
|
2025-12-14 14:04:44 +09:00
|
|
|
// LLM should only output bare mxCells now, so wrapper tags indicate error
|
|
|
|
|
const trimmed = xml.trim()
|
2025-12-14 12:34:34 +09:00
|
|
|
const isFreshStart =
|
2025-12-14 14:04:44 +09:00
|
|
|
trimmed.startsWith("<mxGraphModel") ||
|
|
|
|
|
trimmed.startsWith("<root") ||
|
|
|
|
|
trimmed.startsWith("<mxfile") ||
|
|
|
|
|
trimmed.startsWith('<mxCell id="0"') ||
|
|
|
|
|
trimmed.startsWith('<mxCell id="1"')
|
2025-12-14 12:34:34 +09:00
|
|
|
|
|
|
|
|
if (isFreshStart) {
|
|
|
|
|
addToolOutput({
|
|
|
|
|
tool: "append_diagram",
|
|
|
|
|
toolCallId: toolCall.toolCallId,
|
|
|
|
|
state: "output-error",
|
2025-12-14 14:04:44 +09:00
|
|
|
errorText: `ERROR: You started fresh with wrapper tags. Do NOT include wrapper tags or root cells (id="0", id="1").
|
2025-12-14 12:34:34 +09:00
|
|
|
|
|
|
|
|
Continue from EXACTLY where the partial ended:
|
|
|
|
|
\`\`\`
|
|
|
|
|
${partialXmlRef.current.slice(-500)}
|
|
|
|
|
\`\`\`
|
|
|
|
|
|
|
|
|
|
Start your continuation with the NEXT character after where it stopped.`,
|
|
|
|
|
})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Append to accumulated XML
|
|
|
|
|
partialXmlRef.current += xml
|
|
|
|
|
|
2025-12-14 14:04:44 +09:00
|
|
|
// Check if XML is now complete (last mxCell is complete)
|
|
|
|
|
const isComplete = isMxCellXmlComplete(partialXmlRef.current)
|
2025-12-14 12:34:34 +09:00
|
|
|
|
|
|
|
|
if (isComplete) {
|
|
|
|
|
// Wrap and display the complete diagram
|
|
|
|
|
const finalXml = partialXmlRef.current
|
|
|
|
|
partialXmlRef.current = "" // Reset
|
|
|
|
|
|
|
|
|
|
const fullXml = wrapWithMxFile(finalXml)
|
|
|
|
|
const validationError = onDisplayChart(fullXml)
|
|
|
|
|
|
|
|
|
|
if (validationError) {
|
|
|
|
|
addToolOutput({
|
|
|
|
|
tool: "append_diagram",
|
|
|
|
|
toolCallId: toolCall.toolCallId,
|
|
|
|
|
state: "output-error",
|
|
|
|
|
errorText: `Validation error after assembly: ${validationError}
|
|
|
|
|
|
|
|
|
|
Assembled XML:
|
|
|
|
|
\`\`\`xml
|
|
|
|
|
${finalXml.substring(0, 2000)}...
|
|
|
|
|
\`\`\`
|
|
|
|
|
|
|
|
|
|
Please use display_diagram with corrected XML.`,
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
addToolOutput({
|
|
|
|
|
tool: "append_diagram",
|
|
|
|
|
toolCallId: toolCall.toolCallId,
|
|
|
|
|
output: "Diagram assembly complete and displayed successfully.",
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Still incomplete - signal to continue
|
|
|
|
|
addToolOutput({
|
|
|
|
|
tool: "append_diagram",
|
|
|
|
|
toolCallId: toolCall.toolCallId,
|
|
|
|
|
state: "output-error",
|
2025-12-14 14:04:44 +09:00
|
|
|
errorText: `XML still incomplete (mxCell not closed). Call append_diagram again to continue.
|
2025-12-14 12:34:34 +09:00
|
|
|
|
|
|
|
|
Current ending:
|
|
|
|
|
\`\`\`
|
|
|
|
|
${partialXmlRef.current.slice(-500)}
|
|
|
|
|
\`\`\`
|
|
|
|
|
|
|
|
|
|
Continue from EXACTLY where you stopped.`,
|
|
|
|
|
})
|
|
|
|
|
}
|
2025-12-07 00:40:11 +09:00
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
onError: (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)
|
2025-12-15 21:28:31 +09:00
|
|
|
// 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,
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
})
|
2025-12-07 00:40:11 +09:00
|
|
|
}
|
|
|
|
|
|
2025-12-08 19:52:18 +08:00
|
|
|
// 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."
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-14 12:34:34 +09:00
|
|
|
// 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."
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-09 16:00:19 +09:00
|
|
|
// Translate image not supported error
|
|
|
|
|
if (friendlyMessage.includes("image content block")) {
|
|
|
|
|
friendlyMessage = "This model doesn't support image input."
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-07 00:40:11 +09:00
|
|
|
// Add system message for error so it can be cleared
|
|
|
|
|
setMessages((currentMessages) => {
|
|
|
|
|
const errorMessage = {
|
|
|
|
|
id: `error-${Date.now()}`,
|
|
|
|
|
role: "system" as const,
|
2025-12-08 19:52:18 +08:00
|
|
|
content: friendlyMessage,
|
|
|
|
|
parts: [{ type: "text" as const, text: friendlyMessage }],
|
2025-08-31 12:54:14 +09:00
|
|
|
}
|
2025-12-07 00:40:11 +09:00
|
|
|
return [...currentMessages, errorMessage]
|
|
|
|
|
})
|
2025-12-05 21:09:34 +08:00
|
|
|
|
2025-12-07 00:40:11 +09:00
|
|
|
if (error.message.includes("Invalid or missing access code")) {
|
feat: multi-provider model configuration with UI/UX improvements (#355)
* feat: add multi-provider model configuration
- Add model config dialog for managing multiple AI providers
- Support for OpenAI, Anthropic, Google, Azure, Bedrock, OpenRouter, DeepSeek, SiliconFlow, Ollama, and AI Gateway
- Add model selector dropdown in chat panel header
- Add API key validation endpoint
- Add custom model ID input with keyboard navigation
- Fix hover highlight in Command component
- Add suggested models for each provider including latest Claude 4.5 series
- Store configuration locally in browser
* feat: improve model config UI and move selector to chat input
- Move model selector from header to chat input (left of send button)
- Add per-model validation status (queued, running, valid, invalid)
- Filter model selector to only show verified models
- Add editable model IDs in config dialog
- Add custom model input field alongside suggested models dropdown
- Fix hover states on provider buttons and select triggers
- Update OpenAI suggested models with GPT-5 series
- Add alert-dialog component for delete confirmation
* refactor: revert shadcn component changes, apply hover fix at usage site
* feat: add AWS credentials support for Bedrock provider
- Add AWS Access Key ID, Secret Access Key, Region fields for Bedrock
- Show different credential fields based on provider type
- Update validation API to handle Bedrock with AWS credentials
- Add region selector with common AWS regions
* fix: reset Test button after validation completes
* fix: reset validation button to Test after success
* fix: complete bedrock support and UI/UX improvements
- Add bedrock to ALLOWED_CLIENT_PROVIDERS for client credentials
- Pass AWS credentials through full chain (headers → API → provider)
- Replace non-existent GPT-5 models with real ones (o1, o3-mini)
- Add accessibility: aria-labels, focus-visible rings, inline errors
- Add more AWS regions (Ohio, London, Paris, Mumbai, Seoul, São Paulo)
- Fix setTimeout cleanup with useRef on component unmount
- Fix TypeScript type consistency in getSelectedAIConfig fallback
* chore: remove unused code
- Remove unused setAccessCodeRequired state in chat-panel.tsx
- Remove unused getSelectedModel export in model-config.ts
* fix: UI/UX improvements for model configuration dialog
- Add gradient header styling with icon badge
- Change Configuration section icon from Key to Settings2
- Add duplicate model detection with warning banner and inline removal
- Filter out already-added models from suggestions dropdown
- Add type-to-confirm for deleting providers with 3+ models
- Enhance delete confirmation dialog with warning icon
- Improve model selector discoverability (show model name + chevron)
- Add truncation for long model names with title tooltip
- Remove AI provider settings from Settings dialog (now in Model Config)
- Extract ValidationButton into reusable component
* fix: prevent duplicate model IDs within same provider
- Block adding model if ID already exists in provider
- Block editing model ID to match existing model in provider
* fix: improve duplicate model ID notifications
- Add toast notification when trying to add duplicate model
- Allow free typing when editing model ID, validate on blur
- Show warning toast instead of blocking input
* fix: improve duplicate model validation UX in config dialog
- Add inline error display for duplicate model IDs
- Show red border on input when error exists
- Validate on blur with shake animation for edit errors
- Prevent saving empty model names
- Clear errors when user starts typing
- Simplify error styling (small red text, no heavy chips)
2025-12-22 22:36:36 +09:00
|
|
|
// Show settings dialog to help user fix it
|
2025-12-07 00:40:11 +09:00
|
|
|
setShowSettingsDialog(true)
|
|
|
|
|
}
|
|
|
|
|
},
|
2025-12-08 18:56:34 +09:00
|
|
|
onFinish: ({ message }) => {
|
|
|
|
|
// Track actual token usage from server metadata
|
|
|
|
|
const metadata = message?.metadata as
|
|
|
|
|
| Record<string, unknown>
|
|
|
|
|
| undefined
|
2025-12-14 12:34:34 +09:00
|
|
|
|
2025-12-14 19:38:40 +09:00
|
|
|
// DEBUG: Log finish reason to diagnose truncation
|
|
|
|
|
console.log("[onFinish] finishReason:", metadata?.finishReason)
|
|
|
|
|
console.log("[onFinish] metadata:", metadata)
|
|
|
|
|
|
2025-12-08 18:56:34 +09:00
|
|
|
if (metadata) {
|
|
|
|
|
// Use Number.isFinite to guard against NaN (typeof NaN === 'number' is true)
|
|
|
|
|
const inputTokens = Number.isFinite(metadata.inputTokens)
|
|
|
|
|
? (metadata.inputTokens as number)
|
|
|
|
|
: 0
|
|
|
|
|
const outputTokens = Number.isFinite(metadata.outputTokens)
|
|
|
|
|
? (metadata.outputTokens as number)
|
|
|
|
|
: 0
|
|
|
|
|
const actualTokens = inputTokens + outputTokens
|
|
|
|
|
if (actualTokens > 0) {
|
2025-12-11 14:28:02 +09:00
|
|
|
quotaManager.incrementTokenCount(actualTokens)
|
|
|
|
|
quotaManager.incrementTPMCount(actualTokens)
|
2025-12-08 18:56:34 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
2025-12-11 17:56:40 +09:00
|
|
|
sendAutomaticallyWhen: ({ messages }) => {
|
2025-12-14 12:34:34 +09:00
|
|
|
const isInContinuationMode = partialXmlRef.current.length > 0
|
|
|
|
|
|
2025-12-11 17:56:40 +09:00
|
|
|
const shouldRetry = hasToolErrors(
|
|
|
|
|
messages as unknown as ChatMessage[],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if (!shouldRetry) {
|
2025-12-14 12:34:34 +09:00
|
|
|
// No error, reset retry count and clear state
|
2025-12-11 17:56:40 +09:00
|
|
|
autoRetryCountRef.current = 0
|
2025-12-23 14:17:06 +09:00
|
|
|
continuationRetryCountRef.current = 0
|
2025-12-14 12:34:34 +09:00
|
|
|
partialXmlRef.current = ""
|
2025-12-11 17:56:40 +09:00
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-23 14:17:06 +09:00
|
|
|
// Continuation mode: limited retries for truncation handling
|
2025-12-14 12:34:34 +09:00
|
|
|
if (isInContinuationMode) {
|
2025-12-23 14:17:06 +09:00
|
|
|
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++
|
2025-12-14 12:34:34 +09:00
|
|
|
} 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.`,
|
2025-12-11 17:56:40 +09:00
|
|
|
)
|
2025-12-14 12:34:34 +09:00
|
|
|
autoRetryCountRef.current = 0
|
|
|
|
|
partialXmlRef.current = ""
|
|
|
|
|
return false
|
2025-12-11 17:56:40 +09:00
|
|
|
}
|
2025-12-14 12:34:34 +09:00
|
|
|
// Increment retry count for actual errors
|
|
|
|
|
autoRetryCountRef.current++
|
2025-12-11 17:56:40 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check quota limits before auto-retry
|
|
|
|
|
const tokenLimitCheck = quotaManager.checkTokenLimit()
|
|
|
|
|
if (!tokenLimitCheck.allowed) {
|
|
|
|
|
quotaManager.showTokenLimitToast(tokenLimitCheck.used)
|
|
|
|
|
autoRetryCountRef.current = 0
|
2025-12-23 14:17:06 +09:00
|
|
|
continuationRetryCountRef.current = 0
|
2025-12-14 12:34:34 +09:00
|
|
|
partialXmlRef.current = ""
|
2025-12-11 17:56:40 +09:00
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const tpmCheck = quotaManager.checkTPMLimit()
|
|
|
|
|
if (!tpmCheck.allowed) {
|
|
|
|
|
quotaManager.showTPMLimitToast()
|
|
|
|
|
autoRetryCountRef.current = 0
|
2025-12-23 14:17:06 +09:00
|
|
|
continuationRetryCountRef.current = 0
|
2025-12-14 12:34:34 +09:00
|
|
|
partialXmlRef.current = ""
|
2025-12-11 17:56:40 +09:00
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
},
|
2025-12-07 00:40:11 +09:00
|
|
|
})
|
2025-12-05 22:42:39 +09:00
|
|
|
|
2025-12-07 00:40:11 +09:00
|
|
|
// Update stopRef so onToolCall can access it
|
|
|
|
|
stopRef.current = stop
|
2025-12-03 21:49:34 +09:00
|
|
|
|
2025-12-07 01:39:09 +09:00
|
|
|
// Ref to track latest messages for unload persistence
|
|
|
|
|
const messagesRef = useRef(messages)
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
messagesRef.current = messages
|
|
|
|
|
}, [messages])
|
|
|
|
|
|
2025-12-06 12:46:40 +09:00
|
|
|
const messagesEndRef = useRef<HTMLDivElement>(null)
|
2025-12-03 21:49:34 +09:00
|
|
|
|
2025-12-07 01:39:09 +09:00
|
|
|
// Restore messages and XML snapshots from localStorage on mount
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (hasRestoredRef.current) return
|
|
|
|
|
hasRestoredRef.current = true
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Restore messages
|
|
|
|
|
const savedMessages = localStorage.getItem(STORAGE_MESSAGES_KEY)
|
|
|
|
|
if (savedMessages) {
|
|
|
|
|
const parsed = JSON.parse(savedMessages)
|
|
|
|
|
if (Array.isArray(parsed) && parsed.length > 0) {
|
2025-12-14 21:49:08 +09:00
|
|
|
setMessages(parsed)
|
2025-12-07 01:39:09 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Restore XML snapshots
|
|
|
|
|
const savedSnapshots = localStorage.getItem(
|
|
|
|
|
STORAGE_XML_SNAPSHOTS_KEY,
|
|
|
|
|
)
|
|
|
|
|
if (savedSnapshots) {
|
|
|
|
|
const parsed = JSON.parse(savedSnapshots)
|
|
|
|
|
xmlSnapshotsRef.current = new Map(parsed)
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Failed to restore from localStorage:", error)
|
2025-12-14 20:01:24 +09:00
|
|
|
// On complete failure, clear storage to allow recovery
|
|
|
|
|
localStorage.removeItem(STORAGE_MESSAGES_KEY)
|
|
|
|
|
localStorage.removeItem(STORAGE_XML_SNAPSHOTS_KEY)
|
|
|
|
|
toast.error("Session data was corrupted. Starting fresh.")
|
2025-12-07 01:39:09 +09:00
|
|
|
}
|
|
|
|
|
}, [setMessages])
|
|
|
|
|
|
2025-12-14 21:23:14 +09:00
|
|
|
// Save messages to localStorage whenever they change (debounced to prevent blocking during streaming)
|
2025-12-07 01:39:09 +09:00
|
|
|
useEffect(() => {
|
|
|
|
|
if (!hasRestoredRef.current) return
|
2025-12-14 21:23:14 +09:00
|
|
|
|
|
|
|
|
// Clear any pending save
|
|
|
|
|
if (localStorageDebounceRef.current) {
|
|
|
|
|
clearTimeout(localStorageDebounceRef.current)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Debounce: save after 1 second of no changes
|
|
|
|
|
localStorageDebounceRef.current = setTimeout(() => {
|
|
|
|
|
try {
|
|
|
|
|
localStorage.setItem(
|
|
|
|
|
STORAGE_MESSAGES_KEY,
|
|
|
|
|
JSON.stringify(messages),
|
|
|
|
|
)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Failed to save messages to localStorage:", error)
|
|
|
|
|
}
|
|
|
|
|
}, LOCAL_STORAGE_DEBOUNCE_MS)
|
|
|
|
|
|
|
|
|
|
// Cleanup on unmount
|
|
|
|
|
return () => {
|
|
|
|
|
if (localStorageDebounceRef.current) {
|
|
|
|
|
clearTimeout(localStorageDebounceRef.current)
|
|
|
|
|
}
|
2025-12-07 01:39:09 +09:00
|
|
|
}
|
|
|
|
|
}, [messages])
|
|
|
|
|
|
|
|
|
|
// Save XML snapshots to localStorage whenever they change
|
|
|
|
|
const saveXmlSnapshots = useCallback(() => {
|
|
|
|
|
try {
|
|
|
|
|
const snapshotsArray = Array.from(xmlSnapshotsRef.current.entries())
|
|
|
|
|
localStorage.setItem(
|
|
|
|
|
STORAGE_XML_SNAPSHOTS_KEY,
|
|
|
|
|
JSON.stringify(snapshotsArray),
|
|
|
|
|
)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error(
|
|
|
|
|
"Failed to save XML snapshots to localStorage:",
|
|
|
|
|
error,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
// Save session ID to localStorage
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
localStorage.setItem(STORAGE_SESSION_ID_KEY, sessionId)
|
|
|
|
|
}, [sessionId])
|
|
|
|
|
|
2025-03-19 07:20:22 +00:00
|
|
|
useEffect(() => {
|
|
|
|
|
if (messagesEndRef.current) {
|
2025-12-06 12:46:40 +09:00
|
|
|
messagesEndRef.current.scrollIntoView({ behavior: "smooth" })
|
2025-03-19 07:20:22 +00:00
|
|
|
}
|
2025-12-06 12:46:40 +09:00
|
|
|
}, [messages])
|
2025-03-19 07:20:22 +00:00
|
|
|
|
2025-12-07 01:39:09 +09:00
|
|
|
// Save state right before page unload (refresh/close)
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const handleBeforeUnload = () => {
|
|
|
|
|
try {
|
|
|
|
|
localStorage.setItem(
|
|
|
|
|
STORAGE_MESSAGES_KEY,
|
|
|
|
|
JSON.stringify(messagesRef.current),
|
|
|
|
|
)
|
|
|
|
|
localStorage.setItem(
|
|
|
|
|
STORAGE_XML_SNAPSHOTS_KEY,
|
|
|
|
|
JSON.stringify(
|
|
|
|
|
Array.from(xmlSnapshotsRef.current.entries()),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
const xml = chartXMLRef.current
|
|
|
|
|
if (xml && xml.length > 300) {
|
|
|
|
|
localStorage.setItem(STORAGE_DIAGRAM_XML_KEY, xml)
|
|
|
|
|
}
|
|
|
|
|
localStorage.setItem(STORAGE_SESSION_ID_KEY, sessionId)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Failed to persist state before unload:", error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
window.addEventListener("beforeunload", handleBeforeUnload)
|
|
|
|
|
return () =>
|
|
|
|
|
window.removeEventListener("beforeunload", handleBeforeUnload)
|
|
|
|
|
}, [sessionId])
|
|
|
|
|
|
2025-03-22 16:03:03 +00:00
|
|
|
const onFormSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
2025-12-06 12:46:40 +09:00
|
|
|
e.preventDefault()
|
|
|
|
|
const isProcessing = status === "streaming" || status === "submitted"
|
2025-11-15 14:29:18 +09:00
|
|
|
if (input.trim() && !isProcessing) {
|
2025-12-07 19:36:09 +09:00
|
|
|
// Check if input matches a cached example (only when no messages yet)
|
|
|
|
|
if (messages.length === 0) {
|
|
|
|
|
const cached = findCachedResponse(
|
|
|
|
|
input.trim(),
|
|
|
|
|
files.length > 0,
|
|
|
|
|
)
|
|
|
|
|
if (cached) {
|
|
|
|
|
// Add user message and fake assistant response to messages
|
|
|
|
|
// The chat-message-display useEffect will handle displaying the diagram
|
|
|
|
|
const toolCallId = `cached-${Date.now()}`
|
2025-12-10 21:32:35 +09:00
|
|
|
|
|
|
|
|
// Build user message text including any file content
|
2025-12-11 14:28:02 +09:00
|
|
|
const userText = await processFilesAndAppendContent(
|
|
|
|
|
input,
|
|
|
|
|
files,
|
|
|
|
|
pdfData,
|
|
|
|
|
)
|
2025-12-10 21:32:35 +09:00
|
|
|
|
2025-12-07 19:36:09 +09:00
|
|
|
setMessages([
|
|
|
|
|
{
|
|
|
|
|
id: `user-${Date.now()}`,
|
|
|
|
|
role: "user" as const,
|
2025-12-10 21:32:35 +09:00
|
|
|
parts: [{ type: "text" as const, text: userText }],
|
2025-12-07 19:36:09 +09:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: `assistant-${Date.now()}`,
|
|
|
|
|
role: "assistant" as const,
|
|
|
|
|
parts: [
|
|
|
|
|
{
|
|
|
|
|
type: "tool-display_diagram" as const,
|
|
|
|
|
toolCallId,
|
|
|
|
|
state: "output-available" as const,
|
|
|
|
|
input: { xml: cached.xml },
|
|
|
|
|
output: "Successfully displayed the diagram.",
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
] as any)
|
|
|
|
|
setInput("")
|
2025-12-18 20:14:10 +08:00
|
|
|
sessionStorage.removeItem(SESSION_STORAGE_INPUT_KEY)
|
2025-12-07 19:36:09 +09:00
|
|
|
setFiles([])
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-22 16:03:03 +00:00
|
|
|
try {
|
2025-12-06 12:46:40 +09:00
|
|
|
let chartXml = await onFetchChart()
|
|
|
|
|
chartXml = formatXML(chartXml)
|
2025-08-31 12:54:14 +09:00
|
|
|
|
2025-12-05 00:54:35 +09:00
|
|
|
// Update ref directly to avoid race condition with React's async state update
|
|
|
|
|
// This ensures edit_diagram has the correct XML before AI responds
|
2025-12-06 12:46:40 +09:00
|
|
|
chartXMLRef.current = chartXml
|
2025-12-05 00:54:35 +09:00
|
|
|
|
2025-12-10 21:32:35 +09:00
|
|
|
// Build user text by concatenating input with pre-extracted text
|
|
|
|
|
// (Backend only reads first text part, so we must combine them)
|
|
|
|
|
const parts: any[] = []
|
2025-12-11 14:28:02 +09:00
|
|
|
const userText = await processFilesAndAppendContent(
|
|
|
|
|
input,
|
|
|
|
|
files,
|
|
|
|
|
pdfData,
|
|
|
|
|
parts,
|
|
|
|
|
)
|
2025-08-31 12:54:14 +09:00
|
|
|
|
2025-12-10 21:32:35 +09:00
|
|
|
// Add the combined text as the first part
|
|
|
|
|
parts.unshift({ type: "text", text: userText })
|
|
|
|
|
|
2025-12-10 18:04:37 +09:00
|
|
|
// Get previous XML from the last snapshot (before this message)
|
|
|
|
|
const snapshotKeys = Array.from(
|
|
|
|
|
xmlSnapshotsRef.current.keys(),
|
|
|
|
|
).sort((a, b) => b - a)
|
|
|
|
|
const previousXml =
|
|
|
|
|
snapshotKeys.length > 0
|
|
|
|
|
? xmlSnapshotsRef.current.get(snapshotKeys[0]) || ""
|
|
|
|
|
: ""
|
|
|
|
|
|
2025-12-04 22:56:59 +09:00
|
|
|
// Save XML snapshot for this message (will be at index = current messages.length)
|
2025-12-06 12:46:40 +09:00
|
|
|
const messageIndex = messages.length
|
|
|
|
|
xmlSnapshotsRef.current.set(messageIndex, chartXml)
|
2025-12-07 01:39:09 +09:00
|
|
|
saveXmlSnapshots()
|
2025-12-04 22:56:59 +09:00
|
|
|
|
2025-12-11 14:28:02 +09:00
|
|
|
// Check all quota limits
|
|
|
|
|
if (!checkAllQuotaLimits()) return
|
2025-12-08 18:56:34 +09:00
|
|
|
|
2025-12-11 14:28:02 +09:00
|
|
|
sendChatMessage(parts, chartXml, previousXml, sessionId)
|
2025-08-31 12:54:14 +09:00
|
|
|
|
2025-12-08 18:56:34 +09:00
|
|
|
// Token count is tracked in onFinish with actual server usage
|
2025-12-06 12:46:40 +09:00
|
|
|
setInput("")
|
2025-12-18 20:14:10 +08:00
|
|
|
sessionStorage.removeItem(SESSION_STORAGE_INPUT_KEY)
|
2025-12-06 12:46:40 +09:00
|
|
|
setFiles([])
|
2025-03-22 16:03:03 +00:00
|
|
|
} catch (error) {
|
2025-12-06 12:46:40 +09:00
|
|
|
console.error("Error fetching chart data:", error)
|
2025-03-22 16:03:03 +00:00
|
|
|
}
|
2025-03-19 07:20:22 +00:00
|
|
|
}
|
2025-12-06 12:46:40 +09:00
|
|
|
}
|
2025-03-19 07:20:22 +00:00
|
|
|
|
2025-12-12 06:38:18 +05:30
|
|
|
const handleNewChat = useCallback(() => {
|
|
|
|
|
setMessages([])
|
|
|
|
|
clearDiagram()
|
|
|
|
|
handleFileChange([]) // Use handleFileChange to also clear pdfData
|
|
|
|
|
const newSessionId = `session-${Date.now()}-${Math.random()
|
|
|
|
|
.toString(36)
|
|
|
|
|
.slice(2, 9)}`
|
|
|
|
|
setSessionId(newSessionId)
|
|
|
|
|
xmlSnapshotsRef.current.clear()
|
|
|
|
|
// Clear localStorage with error handling
|
|
|
|
|
try {
|
|
|
|
|
localStorage.removeItem(STORAGE_MESSAGES_KEY)
|
|
|
|
|
localStorage.removeItem(STORAGE_XML_SNAPSHOTS_KEY)
|
|
|
|
|
localStorage.removeItem(STORAGE_DIAGRAM_XML_KEY)
|
|
|
|
|
localStorage.setItem(STORAGE_SESSION_ID_KEY, newSessionId)
|
2025-12-18 20:14:10 +08:00
|
|
|
sessionStorage.removeItem(SESSION_STORAGE_INPUT_KEY)
|
2025-12-12 06:38:18 +05:30
|
|
|
toast.success("Started a fresh chat")
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Failed to clear localStorage:", error)
|
|
|
|
|
toast.warning(
|
|
|
|
|
"Chat cleared but browser storage could not be updated",
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setShowNewChatDialog(false)
|
|
|
|
|
}, [clearDiagram, handleFileChange, setMessages, setSessionId])
|
|
|
|
|
|
2025-08-31 12:54:14 +09:00
|
|
|
const handleInputChange = (
|
2025-12-06 12:46:40 +09:00
|
|
|
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
2025-08-31 12:54:14 +09:00
|
|
|
) => {
|
2025-12-18 20:14:10 +08:00
|
|
|
saveInputToSessionStorage(e.target.value)
|
2025-12-06 12:46:40 +09:00
|
|
|
setInput(e.target.value)
|
|
|
|
|
}
|
2025-08-31 12:54:14 +09:00
|
|
|
|
2025-12-18 20:14:10 +08:00
|
|
|
const saveInputToSessionStorage = (input: string) => {
|
|
|
|
|
sessionStorage.setItem(SESSION_STORAGE_INPUT_KEY, input)
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-11 14:28:02 +09:00
|
|
|
// Helper functions for message actions (regenerate/edit)
|
|
|
|
|
// Extract previous XML snapshot before a given message index
|
|
|
|
|
const getPreviousXml = (beforeIndex: number): string => {
|
|
|
|
|
const snapshotKeys = Array.from(xmlSnapshotsRef.current.keys())
|
|
|
|
|
.filter((k) => k < beforeIndex)
|
|
|
|
|
.sort((a, b) => b - a)
|
|
|
|
|
return snapshotKeys.length > 0
|
|
|
|
|
? xmlSnapshotsRef.current.get(snapshotKeys[0]) || ""
|
|
|
|
|
: ""
|
|
|
|
|
}
|
2025-12-10 21:32:35 +09:00
|
|
|
|
2025-12-11 14:28:02 +09:00
|
|
|
// Restore diagram from snapshot and update ref
|
|
|
|
|
const restoreDiagramFromSnapshot = (savedXml: string) => {
|
|
|
|
|
onDisplayChart(savedXml, true) // Skip validation for trusted snapshots
|
|
|
|
|
chartXMLRef.current = savedXml
|
|
|
|
|
}
|
2025-12-10 21:32:35 +09:00
|
|
|
|
2025-12-11 14:28:02 +09:00
|
|
|
// Clean up snapshots after a given message index
|
|
|
|
|
const cleanupSnapshotsAfter = (messageIndex: number) => {
|
|
|
|
|
for (const key of xmlSnapshotsRef.current.keys()) {
|
|
|
|
|
if (key > messageIndex) {
|
|
|
|
|
xmlSnapshotsRef.current.delete(key)
|
2025-12-10 21:32:35 +09:00
|
|
|
}
|
|
|
|
|
}
|
2025-12-11 14:28:02 +09:00
|
|
|
saveXmlSnapshots()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check all quota limits (daily requests, tokens, TPM)
|
|
|
|
|
const checkAllQuotaLimits = (): boolean => {
|
|
|
|
|
const limitCheck = quotaManager.checkDailyLimit()
|
|
|
|
|
if (!limitCheck.allowed) {
|
|
|
|
|
quotaManager.showQuotaLimitToast()
|
|
|
|
|
return false
|
|
|
|
|
}
|
2025-12-10 21:32:35 +09:00
|
|
|
|
2025-12-11 14:28:02 +09:00
|
|
|
const tokenLimitCheck = quotaManager.checkTokenLimit()
|
|
|
|
|
if (!tokenLimitCheck.allowed) {
|
|
|
|
|
quotaManager.showTokenLimitToast(tokenLimitCheck.used)
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const tpmCheck = quotaManager.checkTPMLimit()
|
|
|
|
|
if (!tpmCheck.allowed) {
|
|
|
|
|
quotaManager.showTPMLimitToast()
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Send chat message with headers and increment quota
|
|
|
|
|
const sendChatMessage = (
|
|
|
|
|
parts: any,
|
|
|
|
|
xml: string,
|
|
|
|
|
previousXml: string,
|
|
|
|
|
sessionId: string,
|
|
|
|
|
) => {
|
2025-12-14 12:34:34 +09:00
|
|
|
// Reset all retry/continuation state on user-initiated message
|
2025-12-11 17:56:40 +09:00
|
|
|
autoRetryCountRef.current = 0
|
2025-12-23 14:17:06 +09:00
|
|
|
continuationRetryCountRef.current = 0
|
2025-12-14 12:34:34 +09:00
|
|
|
partialXmlRef.current = ""
|
2025-12-11 17:56:40 +09:00
|
|
|
|
feat: multi-provider model configuration with UI/UX improvements (#355)
* feat: add multi-provider model configuration
- Add model config dialog for managing multiple AI providers
- Support for OpenAI, Anthropic, Google, Azure, Bedrock, OpenRouter, DeepSeek, SiliconFlow, Ollama, and AI Gateway
- Add model selector dropdown in chat panel header
- Add API key validation endpoint
- Add custom model ID input with keyboard navigation
- Fix hover highlight in Command component
- Add suggested models for each provider including latest Claude 4.5 series
- Store configuration locally in browser
* feat: improve model config UI and move selector to chat input
- Move model selector from header to chat input (left of send button)
- Add per-model validation status (queued, running, valid, invalid)
- Filter model selector to only show verified models
- Add editable model IDs in config dialog
- Add custom model input field alongside suggested models dropdown
- Fix hover states on provider buttons and select triggers
- Update OpenAI suggested models with GPT-5 series
- Add alert-dialog component for delete confirmation
* refactor: revert shadcn component changes, apply hover fix at usage site
* feat: add AWS credentials support for Bedrock provider
- Add AWS Access Key ID, Secret Access Key, Region fields for Bedrock
- Show different credential fields based on provider type
- Update validation API to handle Bedrock with AWS credentials
- Add region selector with common AWS regions
* fix: reset Test button after validation completes
* fix: reset validation button to Test after success
* fix: complete bedrock support and UI/UX improvements
- Add bedrock to ALLOWED_CLIENT_PROVIDERS for client credentials
- Pass AWS credentials through full chain (headers → API → provider)
- Replace non-existent GPT-5 models with real ones (o1, o3-mini)
- Add accessibility: aria-labels, focus-visible rings, inline errors
- Add more AWS regions (Ohio, London, Paris, Mumbai, Seoul, São Paulo)
- Fix setTimeout cleanup with useRef on component unmount
- Fix TypeScript type consistency in getSelectedAIConfig fallback
* chore: remove unused code
- Remove unused setAccessCodeRequired state in chat-panel.tsx
- Remove unused getSelectedModel export in model-config.ts
* fix: UI/UX improvements for model configuration dialog
- Add gradient header styling with icon badge
- Change Configuration section icon from Key to Settings2
- Add duplicate model detection with warning banner and inline removal
- Filter out already-added models from suggestions dropdown
- Add type-to-confirm for deleting providers with 3+ models
- Enhance delete confirmation dialog with warning icon
- Improve model selector discoverability (show model name + chevron)
- Add truncation for long model names with title tooltip
- Remove AI provider settings from Settings dialog (now in Model Config)
- Extract ValidationButton into reusable component
* fix: prevent duplicate model IDs within same provider
- Block adding model if ID already exists in provider
- Block editing model ID to match existing model in provider
* fix: improve duplicate model ID notifications
- Add toast notification when trying to add duplicate model
- Allow free typing when editing model ID, validate on blur
- Show warning toast instead of blocking input
* fix: improve duplicate model validation UX in config dialog
- Add inline error display for duplicate model IDs
- Show red border on input when error exists
- Validate on blur with shake animation for edit errors
- Prevent saving empty model names
- Clear errors when user starts typing
- Simplify error styling (small red text, no heavy chips)
2025-12-22 22:36:36 +09:00
|
|
|
const config = getSelectedAIConfig()
|
2025-12-11 14:28:02 +09:00
|
|
|
|
|
|
|
|
sendMessage(
|
|
|
|
|
{ parts },
|
|
|
|
|
{
|
|
|
|
|
body: { xml, previousXml, sessionId },
|
|
|
|
|
headers: {
|
|
|
|
|
"x-access-code": config.accessCode,
|
|
|
|
|
...(config.aiProvider && {
|
|
|
|
|
"x-ai-provider": config.aiProvider,
|
2025-12-12 08:33:07 +08:00
|
|
|
...(config.aiBaseUrl && {
|
|
|
|
|
"x-ai-base-url": config.aiBaseUrl,
|
|
|
|
|
}),
|
|
|
|
|
...(config.aiApiKey && {
|
|
|
|
|
"x-ai-api-key": config.aiApiKey,
|
|
|
|
|
}),
|
|
|
|
|
...(config.aiModel && { "x-ai-model": config.aiModel }),
|
feat: multi-provider model configuration with UI/UX improvements (#355)
* feat: add multi-provider model configuration
- Add model config dialog for managing multiple AI providers
- Support for OpenAI, Anthropic, Google, Azure, Bedrock, OpenRouter, DeepSeek, SiliconFlow, Ollama, and AI Gateway
- Add model selector dropdown in chat panel header
- Add API key validation endpoint
- Add custom model ID input with keyboard navigation
- Fix hover highlight in Command component
- Add suggested models for each provider including latest Claude 4.5 series
- Store configuration locally in browser
* feat: improve model config UI and move selector to chat input
- Move model selector from header to chat input (left of send button)
- Add per-model validation status (queued, running, valid, invalid)
- Filter model selector to only show verified models
- Add editable model IDs in config dialog
- Add custom model input field alongside suggested models dropdown
- Fix hover states on provider buttons and select triggers
- Update OpenAI suggested models with GPT-5 series
- Add alert-dialog component for delete confirmation
* refactor: revert shadcn component changes, apply hover fix at usage site
* feat: add AWS credentials support for Bedrock provider
- Add AWS Access Key ID, Secret Access Key, Region fields for Bedrock
- Show different credential fields based on provider type
- Update validation API to handle Bedrock with AWS credentials
- Add region selector with common AWS regions
* fix: reset Test button after validation completes
* fix: reset validation button to Test after success
* fix: complete bedrock support and UI/UX improvements
- Add bedrock to ALLOWED_CLIENT_PROVIDERS for client credentials
- Pass AWS credentials through full chain (headers → API → provider)
- Replace non-existent GPT-5 models with real ones (o1, o3-mini)
- Add accessibility: aria-labels, focus-visible rings, inline errors
- Add more AWS regions (Ohio, London, Paris, Mumbai, Seoul, São Paulo)
- Fix setTimeout cleanup with useRef on component unmount
- Fix TypeScript type consistency in getSelectedAIConfig fallback
* chore: remove unused code
- Remove unused setAccessCodeRequired state in chat-panel.tsx
- Remove unused getSelectedModel export in model-config.ts
* fix: UI/UX improvements for model configuration dialog
- Add gradient header styling with icon badge
- Change Configuration section icon from Key to Settings2
- Add duplicate model detection with warning banner and inline removal
- Filter out already-added models from suggestions dropdown
- Add type-to-confirm for deleting providers with 3+ models
- Enhance delete confirmation dialog with warning icon
- Improve model selector discoverability (show model name + chevron)
- Add truncation for long model names with title tooltip
- Remove AI provider settings from Settings dialog (now in Model Config)
- Extract ValidationButton into reusable component
* fix: prevent duplicate model IDs within same provider
- Block adding model if ID already exists in provider
- Block editing model ID to match existing model in provider
* fix: improve duplicate model ID notifications
- Add toast notification when trying to add duplicate model
- Allow free typing when editing model ID, validate on blur
- Show warning toast instead of blocking input
* fix: improve duplicate model validation UX in config dialog
- Add inline error display for duplicate model IDs
- Show red border on input when error exists
- Validate on blur with shake animation for edit errors
- Prevent saving empty model names
- Clear errors when user starts typing
- Simplify error styling (small red text, no heavy chips)
2025-12-22 22:36:36 +09:00
|
|
|
// AWS Bedrock credentials
|
|
|
|
|
...(config.awsAccessKeyId && {
|
|
|
|
|
"x-aws-access-key-id": config.awsAccessKeyId,
|
|
|
|
|
}),
|
|
|
|
|
...(config.awsSecretAccessKey && {
|
|
|
|
|
"x-aws-secret-access-key":
|
|
|
|
|
config.awsSecretAccessKey,
|
|
|
|
|
}),
|
|
|
|
|
...(config.awsRegion && {
|
|
|
|
|
"x-aws-region": config.awsRegion,
|
|
|
|
|
}),
|
|
|
|
|
...(config.awsSessionToken && {
|
|
|
|
|
"x-aws-session-token": config.awsSessionToken,
|
|
|
|
|
}),
|
2025-12-11 14:28:02 +09:00
|
|
|
}),
|
2025-12-14 19:38:40 +09:00
|
|
|
...(minimalStyle && {
|
|
|
|
|
"x-minimal-style": "true",
|
|
|
|
|
}),
|
2025-12-11 14:28:02 +09:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
quotaManager.incrementRequestCount()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Process files and append content to user text (handles PDF, text, and optionally images)
|
|
|
|
|
const processFilesAndAppendContent = async (
|
|
|
|
|
baseText: string,
|
|
|
|
|
files: File[],
|
|
|
|
|
pdfData: Map<File, FileData>,
|
|
|
|
|
imageParts?: any[],
|
|
|
|
|
): Promise<string> => {
|
|
|
|
|
let userText = baseText
|
|
|
|
|
|
|
|
|
|
for (const file of files) {
|
|
|
|
|
if (isPdfFile(file)) {
|
|
|
|
|
const extracted = pdfData.get(file)
|
|
|
|
|
if (extracted?.text) {
|
|
|
|
|
userText += `\n\n[PDF: ${file.name}]\n${extracted.text}`
|
2025-12-10 21:32:35 +09:00
|
|
|
}
|
2025-12-11 14:28:02 +09:00
|
|
|
} else if (isTextFile(file)) {
|
|
|
|
|
const extracted = pdfData.get(file)
|
|
|
|
|
if (extracted?.text) {
|
|
|
|
|
userText += `\n\n[File: ${file.name}]\n${extracted.text}`
|
|
|
|
|
}
|
|
|
|
|
} else if (imageParts) {
|
|
|
|
|
// Handle as image (only if imageParts array provided)
|
|
|
|
|
const reader = new FileReader()
|
|
|
|
|
const dataUrl = await new Promise<string>((resolve) => {
|
|
|
|
|
reader.onload = () => resolve(reader.result as string)
|
|
|
|
|
reader.readAsDataURL(file)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
imageParts.push({
|
|
|
|
|
type: "file",
|
|
|
|
|
url: dataUrl,
|
|
|
|
|
mediaType: file.type,
|
|
|
|
|
})
|
2025-12-10 21:32:35 +09:00
|
|
|
}
|
2025-12-11 14:28:02 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return userText
|
2025-12-06 12:46:40 +09:00
|
|
|
}
|
2025-03-23 13:54:21 +00:00
|
|
|
|
2025-12-04 22:56:59 +09:00
|
|
|
const handleRegenerate = async (messageIndex: number) => {
|
2025-12-06 12:46:40 +09:00
|
|
|
const isProcessing = status === "streaming" || status === "submitted"
|
|
|
|
|
if (isProcessing) return
|
2025-12-04 22:56:59 +09:00
|
|
|
|
|
|
|
|
// Find the user message before this assistant message
|
2025-12-06 12:46:40 +09:00
|
|
|
let userMessageIndex = messageIndex - 1
|
2025-12-05 16:46:17 +09:00
|
|
|
while (
|
|
|
|
|
userMessageIndex >= 0 &&
|
|
|
|
|
messages[userMessageIndex].role !== "user"
|
|
|
|
|
) {
|
2025-12-06 12:46:40 +09:00
|
|
|
userMessageIndex--
|
2025-12-04 22:56:59 +09:00
|
|
|
}
|
|
|
|
|
|
2025-12-06 12:46:40 +09:00
|
|
|
if (userMessageIndex < 0) return
|
2025-12-04 22:56:59 +09:00
|
|
|
|
2025-12-06 12:46:40 +09:00
|
|
|
const userMessage = messages[userMessageIndex]
|
|
|
|
|
const userParts = userMessage.parts
|
2025-12-04 22:56:59 +09:00
|
|
|
|
|
|
|
|
// Get the text from the user message
|
2025-12-06 12:46:40 +09:00
|
|
|
const textPart = userParts?.find((p: any) => p.type === "text")
|
|
|
|
|
if (!textPart) return
|
2025-12-04 22:56:59 +09:00
|
|
|
|
|
|
|
|
// Get the saved XML snapshot for this user message
|
2025-12-06 12:46:40 +09:00
|
|
|
const savedXml = xmlSnapshotsRef.current.get(userMessageIndex)
|
2025-12-04 22:56:59 +09:00
|
|
|
if (!savedXml) {
|
2025-12-05 16:46:17 +09:00
|
|
|
console.error(
|
|
|
|
|
"No saved XML snapshot for message index:",
|
2025-12-06 12:46:40 +09:00
|
|
|
userMessageIndex,
|
|
|
|
|
)
|
|
|
|
|
return
|
2025-12-04 22:56:59 +09:00
|
|
|
}
|
|
|
|
|
|
2025-12-11 14:28:02 +09:00
|
|
|
// Get previous XML and restore diagram state
|
|
|
|
|
const previousXml = getPreviousXml(userMessageIndex)
|
|
|
|
|
restoreDiagramFromSnapshot(savedXml)
|
2025-12-05 00:54:35 +09:00
|
|
|
|
2025-12-04 22:56:59 +09:00
|
|
|
// Clean up snapshots for messages after the user message (they will be removed)
|
2025-12-11 14:28:02 +09:00
|
|
|
cleanupSnapshotsAfter(userMessageIndex)
|
2025-12-04 22:56:59 +09:00
|
|
|
|
|
|
|
|
// Remove the user message AND assistant message onwards (sendMessage will re-add the user message)
|
|
|
|
|
// Use flushSync to ensure state update is processed synchronously before sending
|
2025-12-06 12:46:40 +09:00
|
|
|
const newMessages = messages.slice(0, userMessageIndex)
|
2025-12-04 22:56:59 +09:00
|
|
|
flushSync(() => {
|
2025-12-06 12:46:40 +09:00
|
|
|
setMessages(newMessages)
|
|
|
|
|
})
|
2025-12-04 22:56:59 +09:00
|
|
|
|
2025-12-11 14:28:02 +09:00
|
|
|
// Check all quota limits
|
|
|
|
|
if (!checkAllQuotaLimits()) return
|
2025-12-08 18:56:34 +09:00
|
|
|
|
2025-12-04 22:56:59 +09:00
|
|
|
// Now send the message after state is guaranteed to be updated
|
2025-12-11 14:28:02 +09:00
|
|
|
sendChatMessage(userParts, savedXml, previousXml, sessionId)
|
feat: add bring-your-own-API-key support (#186)
- Add AI provider settings to config panel (provider, model, API key, base URL)
- Support 7 providers: OpenAI, Anthropic, Google, Azure, OpenRouter, DeepSeek, SiliconFlow
- Client API keys stored in localStorage, never stored on server
- Client settings override server env vars when provided
- Skip server credential validation when client provides API key
- Bypass usage limits (request/token/TPM) when using own API key
- Add /api/config endpoint for fetching usage limits
- Add privacy notices to settings dialog, about pages, and quota toast
- Add clear settings button to reset saved API keys
- Update README files (EN/CN/JA) with BYOK documentation
Co-authored-by: dayuan.jiang <jiangdy@amazon.co.jp>
2025-12-09 17:50:07 +09:00
|
|
|
|
2025-12-08 18:56:34 +09:00
|
|
|
// Token count is tracked in onFinish with actual server usage
|
2025-12-06 12:46:40 +09:00
|
|
|
}
|
2025-12-04 22:56:59 +09:00
|
|
|
|
|
|
|
|
const handleEditMessage = async (messageIndex: number, newText: string) => {
|
2025-12-06 12:46:40 +09:00
|
|
|
const isProcessing = status === "streaming" || status === "submitted"
|
|
|
|
|
if (isProcessing) return
|
2025-12-04 22:56:59 +09:00
|
|
|
|
2025-12-06 12:46:40 +09:00
|
|
|
const message = messages[messageIndex]
|
|
|
|
|
if (!message || message.role !== "user") return
|
2025-12-04 22:56:59 +09:00
|
|
|
|
|
|
|
|
// Get the saved XML snapshot for this user message
|
2025-12-06 12:46:40 +09:00
|
|
|
const savedXml = xmlSnapshotsRef.current.get(messageIndex)
|
2025-12-04 22:56:59 +09:00
|
|
|
if (!savedXml) {
|
2025-12-05 16:46:17 +09:00
|
|
|
console.error(
|
|
|
|
|
"No saved XML snapshot for message index:",
|
2025-12-06 12:46:40 +09:00
|
|
|
messageIndex,
|
|
|
|
|
)
|
|
|
|
|
return
|
2025-12-04 22:56:59 +09:00
|
|
|
}
|
|
|
|
|
|
2025-12-11 14:28:02 +09:00
|
|
|
// Get previous XML and restore diagram state
|
|
|
|
|
const previousXml = getPreviousXml(messageIndex)
|
|
|
|
|
restoreDiagramFromSnapshot(savedXml)
|
2025-12-05 00:54:35 +09:00
|
|
|
|
2025-12-04 22:56:59 +09:00
|
|
|
// Clean up snapshots for messages after the user message (they will be removed)
|
2025-12-11 14:28:02 +09:00
|
|
|
cleanupSnapshotsAfter(messageIndex)
|
2025-12-04 22:56:59 +09:00
|
|
|
|
|
|
|
|
// Create new parts with updated text
|
|
|
|
|
const newParts = message.parts?.map((part: any) => {
|
|
|
|
|
if (part.type === "text") {
|
2025-12-06 12:46:40 +09:00
|
|
|
return { ...part, text: newText }
|
2025-12-04 22:56:59 +09:00
|
|
|
}
|
2025-12-06 12:46:40 +09:00
|
|
|
return part
|
|
|
|
|
}) || [{ type: "text", text: newText }]
|
2025-12-04 22:56:59 +09:00
|
|
|
|
|
|
|
|
// Remove the user message AND assistant message onwards (sendMessage will re-add the user message)
|
|
|
|
|
// Use flushSync to ensure state update is processed synchronously before sending
|
2025-12-06 12:46:40 +09:00
|
|
|
const newMessages = messages.slice(0, messageIndex)
|
2025-12-04 22:56:59 +09:00
|
|
|
flushSync(() => {
|
2025-12-06 12:46:40 +09:00
|
|
|
setMessages(newMessages)
|
|
|
|
|
})
|
2025-12-04 22:56:59 +09:00
|
|
|
|
2025-12-11 14:28:02 +09:00
|
|
|
// Check all quota limits
|
|
|
|
|
if (!checkAllQuotaLimits()) return
|
2025-12-08 18:56:34 +09:00
|
|
|
|
2025-12-04 22:56:59 +09:00
|
|
|
// Now send the edited message after state is guaranteed to be updated
|
2025-12-11 14:28:02 +09:00
|
|
|
sendChatMessage(newParts, savedXml, previousXml, sessionId)
|
2025-12-08 18:56:34 +09:00
|
|
|
// Token count is tracked in onFinish with actual server usage
|
2025-12-06 12:46:40 +09:00
|
|
|
}
|
2025-12-04 22:56:59 +09:00
|
|
|
|
2025-12-05 23:25:59 +09:00
|
|
|
// Collapsed view (desktop only)
|
|
|
|
|
if (!isVisible && !isMobile) {
|
2025-11-15 12:09:32 +09:00
|
|
|
return (
|
2025-12-03 21:49:34 +09:00
|
|
|
<div className="h-full flex flex-col items-center pt-4 bg-card border border-border/30 rounded-xl">
|
2025-11-15 12:09:32 +09:00
|
|
|
<ButtonWithTooltip
|
|
|
|
|
tooltipContent="Show chat panel (Ctrl+B)"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon"
|
|
|
|
|
onClick={onToggleVisibility}
|
2025-12-03 21:49:34 +09:00
|
|
|
className="hover:bg-accent transition-colors"
|
2025-11-15 12:09:32 +09:00
|
|
|
>
|
2025-12-03 21:49:34 +09:00
|
|
|
<PanelRightOpen className="h-5 w-5 text-muted-foreground" />
|
2025-11-15 12:09:32 +09:00
|
|
|
</ButtonWithTooltip>
|
|
|
|
|
<div
|
2025-12-03 21:49:34 +09:00
|
|
|
className="text-sm font-medium text-muted-foreground mt-8 tracking-wide"
|
2025-12-03 16:47:45 +09:00
|
|
|
style={{
|
|
|
|
|
writingMode: "vertical-rl",
|
|
|
|
|
transform: "rotate(180deg)",
|
|
|
|
|
}}
|
2025-11-15 12:09:32 +09:00
|
|
|
>
|
2025-12-03 21:49:34 +09:00
|
|
|
AI Chat
|
2025-11-15 12:09:32 +09:00
|
|
|
</div>
|
2025-12-03 21:49:34 +09:00
|
|
|
</div>
|
2025-12-06 12:46:40 +09:00
|
|
|
)
|
2025-11-15 12:09:32 +09:00
|
|
|
}
|
|
|
|
|
|
2025-12-03 21:49:34 +09:00
|
|
|
// Full view
|
2025-03-19 07:20:22 +00:00
|
|
|
return (
|
2025-12-05 19:30:50 +09:00
|
|
|
<div className="h-full flex flex-col bg-card shadow-soft animate-slide-in-right rounded-xl border border-border/30 relative">
|
2025-12-05 22:42:39 +09:00
|
|
|
<Toaster
|
|
|
|
|
position="bottom-center"
|
|
|
|
|
richColors
|
2025-12-08 14:26:01 +09:00
|
|
|
expand
|
2025-12-05 22:42:39 +09:00
|
|
|
style={{ position: "absolute" }}
|
2025-12-08 14:26:01 +09:00
|
|
|
toastOptions={{
|
|
|
|
|
style: {
|
|
|
|
|
maxWidth: "480px",
|
|
|
|
|
},
|
2025-12-14 13:04:18 +09:00
|
|
|
duration: 2000,
|
2025-12-08 14:26:01 +09:00
|
|
|
}}
|
2025-12-05 22:42:39 +09:00
|
|
|
/>
|
2025-12-03 21:49:34 +09:00
|
|
|
{/* Header */}
|
2025-12-06 12:46:40 +09:00
|
|
|
<header
|
|
|
|
|
className={`${isMobile ? "px-3 py-2" : "px-5 py-4"} border-b border-border/50`}
|
|
|
|
|
>
|
2025-12-03 21:49:34 +09:00
|
|
|
<div className="flex items-center justify-between">
|
2025-12-18 20:16:32 +08:00
|
|
|
<div className="flex items-center gap-2 overflow-x-hidden">
|
2025-12-03 21:49:34 +09:00
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<Image
|
|
|
|
|
src="/favicon.ico"
|
|
|
|
|
alt="Next AI Drawio"
|
2025-12-05 23:25:59 +09:00
|
|
|
width={isMobile ? 24 : 28}
|
|
|
|
|
height={isMobile ? 24 : 28}
|
2025-12-18 20:16:32 +08:00
|
|
|
className="rounded flex-shrink-0"
|
2025-12-03 21:49:34 +09:00
|
|
|
/>
|
2025-12-06 12:46:40 +09:00
|
|
|
<h1
|
|
|
|
|
className={`${isMobile ? "text-sm" : "text-base"} font-semibold tracking-tight whitespace-nowrap`}
|
|
|
|
|
>
|
2025-12-03 21:49:34 +09:00
|
|
|
Next AI Drawio
|
|
|
|
|
</h1>
|
|
|
|
|
</div>
|
2025-12-23 00:32:22 +09:00
|
|
|
{!isMobile &&
|
|
|
|
|
process.env.NEXT_PUBLIC_SHOW_ABOUT_AND_NOTICE ===
|
|
|
|
|
"true" && (
|
|
|
|
|
<Link
|
|
|
|
|
href="/about"
|
|
|
|
|
target="_blank"
|
|
|
|
|
rel="noopener noreferrer"
|
|
|
|
|
className="text-sm text-muted-foreground hover:text-foreground transition-colors ml-2"
|
|
|
|
|
>
|
|
|
|
|
About
|
|
|
|
|
</Link>
|
|
|
|
|
)}
|
2025-12-03 21:49:34 +09:00
|
|
|
</div>
|
2025-12-20 20:18:54 +05:30
|
|
|
<div className="flex items-center gap-1 justify-end overflow-visible">
|
2025-12-12 06:38:18 +05:30
|
|
|
<ButtonWithTooltip
|
2025-12-20 20:18:54 +05:30
|
|
|
tooltipContent={dict.nav.newChat}
|
2025-12-12 06:38:18 +05:30
|
|
|
variant="ghost"
|
|
|
|
|
size="icon"
|
|
|
|
|
onClick={() => setShowNewChatDialog(true)}
|
|
|
|
|
className="hover:bg-accent"
|
|
|
|
|
>
|
|
|
|
|
<MessageSquarePlus
|
|
|
|
|
className={`${isMobile ? "h-4 w-4" : "h-5 w-5"} text-muted-foreground`}
|
|
|
|
|
/>
|
|
|
|
|
</ButtonWithTooltip>
|
|
|
|
|
<div className="w-px h-5 bg-border mx-1" />
|
2025-12-22 21:54:25 +08:00
|
|
|
|
|
|
|
|
<Tooltip>
|
|
|
|
|
<TooltipTrigger asChild>
|
|
|
|
|
<a
|
|
|
|
|
href="https://github.com/DayuanJiang/next-ai-draw-io"
|
|
|
|
|
target="_blank"
|
|
|
|
|
rel="noopener noreferrer"
|
|
|
|
|
className="inline-flex items-center justify-center h-9 w-9 rounded-md text-muted-foreground hover:text-foreground hover:bg-accent transition-colors"
|
|
|
|
|
>
|
|
|
|
|
<FaGithub
|
|
|
|
|
className={`${isMobile ? "w-4 h-4" : "w-5 h-5"}`}
|
|
|
|
|
/>
|
|
|
|
|
</a>
|
|
|
|
|
</TooltipTrigger>
|
|
|
|
|
<TooltipContent>{dict.nav.github}</TooltipContent>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
2025-12-06 21:42:28 +09:00
|
|
|
<ButtonWithTooltip
|
2025-12-20 20:18:54 +05:30
|
|
|
tooltipContent={dict.nav.settings}
|
2025-12-06 21:42:28 +09:00
|
|
|
variant="ghost"
|
|
|
|
|
size="icon"
|
|
|
|
|
onClick={() => setShowSettingsDialog(true)}
|
|
|
|
|
className="hover:bg-accent"
|
|
|
|
|
>
|
|
|
|
|
<Settings
|
|
|
|
|
className={`${isMobile ? "h-4 w-4" : "h-5 w-5"} text-muted-foreground`}
|
|
|
|
|
/>
|
|
|
|
|
</ButtonWithTooltip>
|
2025-12-20 20:18:54 +05:30
|
|
|
<div className="hidden sm:flex items-center gap-2">
|
|
|
|
|
{!isMobile && (
|
|
|
|
|
<ButtonWithTooltip
|
|
|
|
|
tooltipContent={dict.nav.hidePanel}
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon"
|
|
|
|
|
className="hover:bg-accent"
|
|
|
|
|
onClick={onToggleVisibility}
|
|
|
|
|
>
|
|
|
|
|
<PanelRightClose className="h-5 w-5 text-muted-foreground" />
|
|
|
|
|
</ButtonWithTooltip>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2025-12-03 21:49:34 +09:00
|
|
|
</div>
|
feat: Separate SEO content to /about page (best practice)
- Remove header from main page for clean editor-only interface
- Create /app/about/page.tsx with comprehensive SEO content (1000+ words)
- Add About link next to 'Next-AI-Drawio' title in chat panel
- Add GitHub icon link to /about page navigation
- Update sitemap.ts to include /about page (priority: 0.8)
SEO improvements following industry best practices:
- Separate marketing content from app interface (Figma/Canva/Miro approach)
- Server-rendered /about page for optimal crawlability
- Clean URL structure for better internal linking
- Multiple indexable pages for broader keyword coverage
- Proper semantic HTML: H1, H2, H3, article, section tags
- 1000+ words of keyword-rich content
/about page includes:
- AI diagram generator overview with value proposition
- 6 detailed feature sections (AI creation, AWS diagrams, image replication, etc.)
- 3 popular use cases (AWS architecture, flowcharts, system design)
- Step-by-step usage guide (4 steps)
- Benefits section (save time, precision, free, privacy)
- Clear call-to-action with link back to editor
- GitHub link in navigation for social proof
This follows Google-approved architecture and avoids hidden content penalties.
2025-11-16 09:04:01 +09:00
|
|
|
</div>
|
2025-12-03 21:49:34 +09:00
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
{/* Messages */}
|
2025-12-05 22:42:39 +09:00
|
|
|
<main className="flex-1 w-full overflow-hidden">
|
2025-03-25 02:58:11 +00:00
|
|
|
<ChatMessageDisplay
|
|
|
|
|
messages={messages}
|
|
|
|
|
setInput={setInput}
|
|
|
|
|
setFiles={handleFileChange}
|
2025-12-11 17:18:48 +05:30
|
|
|
processedToolCallsRef={processedToolCallsRef}
|
2025-12-15 21:28:31 +09:00
|
|
|
editDiagramOriginalXmlRef={editDiagramOriginalXmlRef}
|
2025-12-05 21:15:02 +09:00
|
|
|
sessionId={sessionId}
|
2025-12-04 22:56:59 +09:00
|
|
|
onRegenerate={handleRegenerate}
|
2025-12-10 20:54:43 +05:30
|
|
|
status={status}
|
2025-12-04 22:56:59 +09:00
|
|
|
onEditMessage={handleEditMessage}
|
2025-03-25 02:58:11 +00:00
|
|
|
/>
|
2025-12-03 21:49:34 +09:00
|
|
|
</main>
|
2025-03-23 13:15:28 +00:00
|
|
|
|
2025-12-03 21:49:34 +09:00
|
|
|
{/* Input */}
|
2025-12-06 12:46:40 +09:00
|
|
|
<footer
|
|
|
|
|
className={`${isMobile ? "p-2" : "p-4"} border-t border-border/50 bg-card/50`}
|
|
|
|
|
>
|
2025-03-22 13:15:51 +00:00
|
|
|
<ChatInput
|
|
|
|
|
input={input}
|
2025-03-22 13:26:14 +00:00
|
|
|
status={status}
|
2025-03-22 13:15:51 +00:00
|
|
|
onSubmit={onFormSubmit}
|
|
|
|
|
onChange={handleInputChange}
|
2025-12-12 06:38:18 +05:30
|
|
|
onClearChat={handleNewChat}
|
2025-03-23 11:03:25 +00:00
|
|
|
files={files}
|
|
|
|
|
onFileChange={handleFileChange}
|
2025-12-10 21:32:35 +09:00
|
|
|
pdfData={pdfData}
|
2025-03-23 13:54:21 +00:00
|
|
|
showHistory={showHistory}
|
2025-03-27 07:48:19 +00:00
|
|
|
onToggleHistory={setShowHistory}
|
2025-12-05 21:15:02 +09:00
|
|
|
sessionId={sessionId}
|
2025-12-05 20:18:19 +09:00
|
|
|
error={error}
|
2025-12-14 19:38:40 +09:00
|
|
|
minimalStyle={minimalStyle}
|
|
|
|
|
onMinimalStyleChange={setMinimalStyle}
|
feat: multi-provider model configuration with UI/UX improvements (#355)
* feat: add multi-provider model configuration
- Add model config dialog for managing multiple AI providers
- Support for OpenAI, Anthropic, Google, Azure, Bedrock, OpenRouter, DeepSeek, SiliconFlow, Ollama, and AI Gateway
- Add model selector dropdown in chat panel header
- Add API key validation endpoint
- Add custom model ID input with keyboard navigation
- Fix hover highlight in Command component
- Add suggested models for each provider including latest Claude 4.5 series
- Store configuration locally in browser
* feat: improve model config UI and move selector to chat input
- Move model selector from header to chat input (left of send button)
- Add per-model validation status (queued, running, valid, invalid)
- Filter model selector to only show verified models
- Add editable model IDs in config dialog
- Add custom model input field alongside suggested models dropdown
- Fix hover states on provider buttons and select triggers
- Update OpenAI suggested models with GPT-5 series
- Add alert-dialog component for delete confirmation
* refactor: revert shadcn component changes, apply hover fix at usage site
* feat: add AWS credentials support for Bedrock provider
- Add AWS Access Key ID, Secret Access Key, Region fields for Bedrock
- Show different credential fields based on provider type
- Update validation API to handle Bedrock with AWS credentials
- Add region selector with common AWS regions
* fix: reset Test button after validation completes
* fix: reset validation button to Test after success
* fix: complete bedrock support and UI/UX improvements
- Add bedrock to ALLOWED_CLIENT_PROVIDERS for client credentials
- Pass AWS credentials through full chain (headers → API → provider)
- Replace non-existent GPT-5 models with real ones (o1, o3-mini)
- Add accessibility: aria-labels, focus-visible rings, inline errors
- Add more AWS regions (Ohio, London, Paris, Mumbai, Seoul, São Paulo)
- Fix setTimeout cleanup with useRef on component unmount
- Fix TypeScript type consistency in getSelectedAIConfig fallback
* chore: remove unused code
- Remove unused setAccessCodeRequired state in chat-panel.tsx
- Remove unused getSelectedModel export in model-config.ts
* fix: UI/UX improvements for model configuration dialog
- Add gradient header styling with icon badge
- Change Configuration section icon from Key to Settings2
- Add duplicate model detection with warning banner and inline removal
- Filter out already-added models from suggestions dropdown
- Add type-to-confirm for deleting providers with 3+ models
- Enhance delete confirmation dialog with warning icon
- Improve model selector discoverability (show model name + chevron)
- Add truncation for long model names with title tooltip
- Remove AI provider settings from Settings dialog (now in Model Config)
- Extract ValidationButton into reusable component
* fix: prevent duplicate model IDs within same provider
- Block adding model if ID already exists in provider
- Block editing model ID to match existing model in provider
* fix: improve duplicate model ID notifications
- Add toast notification when trying to add duplicate model
- Allow free typing when editing model ID, validate on blur
- Show warning toast instead of blocking input
* fix: improve duplicate model validation UX in config dialog
- Add inline error display for duplicate model IDs
- Show red border on input when error exists
- Validate on blur with shake animation for edit errors
- Prevent saving empty model names
- Clear errors when user starts typing
- Simplify error styling (small red text, no heavy chips)
2025-12-22 22:36:36 +09:00
|
|
|
models={modelConfig.models}
|
|
|
|
|
selectedModelId={modelConfig.selectedModelId}
|
|
|
|
|
onModelSelect={modelConfig.setSelectedModelId}
|
|
|
|
|
onConfigureModels={() => setShowModelConfigDialog(true)}
|
2025-03-22 13:15:51 +00:00
|
|
|
/>
|
2025-12-03 21:49:34 +09:00
|
|
|
</footer>
|
2025-12-05 21:09:34 +08:00
|
|
|
|
|
|
|
|
<SettingsDialog
|
|
|
|
|
open={showSettingsDialog}
|
|
|
|
|
onOpenChange={setShowSettingsDialog}
|
2025-12-06 21:42:28 +09:00
|
|
|
onCloseProtectionChange={onCloseProtectionChange}
|
2025-12-10 08:21:15 +08:00
|
|
|
drawioUi={drawioUi}
|
|
|
|
|
onToggleDrawioUi={onToggleDrawioUi}
|
|
|
|
|
darkMode={darkMode}
|
|
|
|
|
onToggleDarkMode={onToggleDarkMode}
|
2025-12-05 21:09:34 +08:00
|
|
|
/>
|
2025-12-12 06:38:18 +05:30
|
|
|
|
feat: multi-provider model configuration with UI/UX improvements (#355)
* feat: add multi-provider model configuration
- Add model config dialog for managing multiple AI providers
- Support for OpenAI, Anthropic, Google, Azure, Bedrock, OpenRouter, DeepSeek, SiliconFlow, Ollama, and AI Gateway
- Add model selector dropdown in chat panel header
- Add API key validation endpoint
- Add custom model ID input with keyboard navigation
- Fix hover highlight in Command component
- Add suggested models for each provider including latest Claude 4.5 series
- Store configuration locally in browser
* feat: improve model config UI and move selector to chat input
- Move model selector from header to chat input (left of send button)
- Add per-model validation status (queued, running, valid, invalid)
- Filter model selector to only show verified models
- Add editable model IDs in config dialog
- Add custom model input field alongside suggested models dropdown
- Fix hover states on provider buttons and select triggers
- Update OpenAI suggested models with GPT-5 series
- Add alert-dialog component for delete confirmation
* refactor: revert shadcn component changes, apply hover fix at usage site
* feat: add AWS credentials support for Bedrock provider
- Add AWS Access Key ID, Secret Access Key, Region fields for Bedrock
- Show different credential fields based on provider type
- Update validation API to handle Bedrock with AWS credentials
- Add region selector with common AWS regions
* fix: reset Test button after validation completes
* fix: reset validation button to Test after success
* fix: complete bedrock support and UI/UX improvements
- Add bedrock to ALLOWED_CLIENT_PROVIDERS for client credentials
- Pass AWS credentials through full chain (headers → API → provider)
- Replace non-existent GPT-5 models with real ones (o1, o3-mini)
- Add accessibility: aria-labels, focus-visible rings, inline errors
- Add more AWS regions (Ohio, London, Paris, Mumbai, Seoul, São Paulo)
- Fix setTimeout cleanup with useRef on component unmount
- Fix TypeScript type consistency in getSelectedAIConfig fallback
* chore: remove unused code
- Remove unused setAccessCodeRequired state in chat-panel.tsx
- Remove unused getSelectedModel export in model-config.ts
* fix: UI/UX improvements for model configuration dialog
- Add gradient header styling with icon badge
- Change Configuration section icon from Key to Settings2
- Add duplicate model detection with warning banner and inline removal
- Filter out already-added models from suggestions dropdown
- Add type-to-confirm for deleting providers with 3+ models
- Enhance delete confirmation dialog with warning icon
- Improve model selector discoverability (show model name + chevron)
- Add truncation for long model names with title tooltip
- Remove AI provider settings from Settings dialog (now in Model Config)
- Extract ValidationButton into reusable component
* fix: prevent duplicate model IDs within same provider
- Block adding model if ID already exists in provider
- Block editing model ID to match existing model in provider
* fix: improve duplicate model ID notifications
- Add toast notification when trying to add duplicate model
- Allow free typing when editing model ID, validate on blur
- Show warning toast instead of blocking input
* fix: improve duplicate model validation UX in config dialog
- Add inline error display for duplicate model IDs
- Show red border on input when error exists
- Validate on blur with shake animation for edit errors
- Prevent saving empty model names
- Clear errors when user starts typing
- Simplify error styling (small red text, no heavy chips)
2025-12-22 22:36:36 +09:00
|
|
|
<ModelConfigDialog
|
|
|
|
|
open={showModelConfigDialog}
|
|
|
|
|
onOpenChange={setShowModelConfigDialog}
|
|
|
|
|
modelConfig={modelConfig}
|
|
|
|
|
/>
|
|
|
|
|
|
2025-12-12 06:38:18 +05:30
|
|
|
<ResetWarningModal
|
|
|
|
|
open={showNewChatDialog}
|
|
|
|
|
onOpenChange={setShowNewChatDialog}
|
|
|
|
|
onClear={handleNewChat}
|
|
|
|
|
/>
|
2025-12-03 21:49:34 +09:00
|
|
|
</div>
|
2025-12-06 12:46:40 +09:00
|
|
|
)
|
2025-03-19 07:20:22 +00:00
|
|
|
}
|