2025-12-06 12:46:40 +09:00
|
|
|
"use client"
|
2025-12-05 21:09:34 +08:00
|
|
|
|
2026-01-28 14:36:54 +01:00
|
|
|
import { ChevronRight, Github, Info, Moon, Sun, Tag } from "lucide-react"
|
2025-12-22 21:54:25 +08:00
|
|
|
import { usePathname, useRouter, useSearchParams } from "next/navigation"
|
feat: add personal My Templates library alongside Quick Examples (#773)
* 增加了ralph自动化编程梳理
* feat: US-001 - 为模板库建立独立的 IndexedDB 存储层
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: US-002 - 在空聊天状态用我的模板库替换官方示例
- 将 ChatLobby 中的 Quick Examples 替换为 TemplatePanel
- 当没有历史会话时,展示完整的模板库面板
- 当有历史会话时,展示可折叠的 "My Templates" 区域
- 使用 TemplatePanel 组件展示用户的个人模板库
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: US-004 - Provide template creation flow
- Create TemplateCreateDialog component with form fields for prompt, title, description, tags, and pinned
- Add i18n translations for template creation UI in en, zh, zh-Hant, ja
- Update TemplatePanel to integrate the create dialog
- Support initialPrompt prop for pre-filling from current input
- Validate required prompt field (empty prompt not allowed)
- Auto-generate default title from first 20 chars of Pin templates appear at top of list
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: US-005 - 为模板卡片提供编辑、删除和复制操作
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: US-006 - Send template directly on click and record usage statistics
- Implement click-to-send template functionality with confirmation dialog
- Add clickCount and runCount increment logic
- Display runCount and lastUsedAt on template card
- Add i18n translations for confirmation dialog (en, zh, zh-Hant, ja)
- Pass onSendTemplate and currentInput props through component hierarchy
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: US-007 - Support template search, pin and default sorting
- Add search bar to TemplatePanel with real-time filtering by title, description, and tags
- Add pin/unpin toggle button on template cards (uses Bookmark icon with fill indicator)
- Search uses existing searchTemplates function from template-storage
- Sort uses existing sortTemplates function (pinned desc, runCount desc, lastUsedAt desc, updatedAt desc)
- Show empty state with Search icon when search returns no results
- List re-sorts immediately after pin/unpin toggle
- Add i18n keys: searchPlaceholder, searchNoResults, pin, unpin for all 4 languages
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: US-008 - Support saving current input as template
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: US-009 - Support saving historical user message as template
- Add "Save as Template" button to user messages
- Pre-fill prompt with original user message text
- Only show on user messages,- Dialog opens TemplateCreateDialog on click
- Template appears in list immediately after saving
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: US-010 - Support template import and export
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* chore: remove local-only dirs from git tracking (.agents, .cursor, scripts, screenshots)
These directories contain local IDE configs, agent scripts, and
dev tooling that should not be part of the upstream repository.
Added them to .gitignore to prevent future accidental commits.
* chore: remove AGENTS.md from git tracking
* fix: add missing i18n keys for template export/import (en/zh/zh-Hant/ja)
* fix: review fixes for my-templates PR
- Fix fragile querySelector("form") with id-based lookup
- Add objectStoreNames.contains guards for IndexedDB upgrades
- Remove duplicate TemplateSchema, import from template-storage
- Revert contributor-specific .gitignore additions
- Fix broken i18n placeholders and missing translations (zh/ja/zh-Hant)
- Remove unused setFiles prop from ChatLobby
- Remove tags feature (unnecessary complexity)
- Improve template card layout: overlay icons on hover, align stats
- Add break-all and overflow-hidden for long prompt text in dialogs
- Move incrementClickCount into sendTemplate for accurate tracking
- Use Intl.RelativeTimeFormat for locale-aware relative time
* feat: restore Quick Examples panel and add lobby panel visibility settings
Bring back the ExamplePanel as a third collapsible section in ChatLobby
alongside Recent Chats and My Templates. Add toggle switches in Settings
to show/hide each lobby panel, persisted via localStorage.
* fix: template send race condition, import defaults, and empty title bug
- Use flushSync instead of setTimeout(0) in handleSendTemplate to
ensure React state is flushed before form submission
- Explicitly validate and default all fields in importTemplates to
prevent undefined counters from malformed import JSON
- Fall back to existing title in edit dialog instead of writing undefined
* fix: address Copilot review comments and remove PRD file
- Fix fallback formatLastUsed returning "Not used yet" for recent usage
- Remove dead mounted flag in TemplatePanel useEffect
- Respect panel visibility settings in no-history lobby state
- Reject empty/whitespace titles in import validation
- Trim title/prompt in importTemplates with default title fallback
- Remove tasks/prd-template-library-replaces-examples.md from repo
* fix: remove double sort, dead code, redundant stats, and break-all CSS
- Remove redundant sortTemplates call in loadTemplates (already sorted by getAllTemplates)
- Remove unused createEmptyTemplateInput and sortTemplates import
- Show "Not used yet" only once for unused templates instead of twice
- Use break-words instead of break-all on prompt textareas
---------
Co-authored-by: 杜雷 <dreamfly@126.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-04-03 11:16:01 +08:00
|
|
|
import { Suspense, useCallback, useEffect, useState } from "react"
|
2026-01-09 09:26:19 +09:00
|
|
|
import { toast } from "sonner"
|
2025-12-06 12:46:40 +09:00
|
|
|
import { Button } from "@/components/ui/button"
|
2025-12-05 21:09:34 +08:00
|
|
|
import {
|
|
|
|
|
Dialog,
|
|
|
|
|
DialogContent,
|
2025-12-06 12:46:40 +09:00
|
|
|
DialogDescription,
|
2025-12-05 21:09:34 +08:00
|
|
|
DialogHeader,
|
|
|
|
|
DialogTitle,
|
2025-12-06 12:46:40 +09:00
|
|
|
} from "@/components/ui/dialog"
|
|
|
|
|
import { Input } from "@/components/ui/input"
|
2025-12-06 21:42:28 +09:00
|
|
|
import { Label } from "@/components/ui/label"
|
2025-12-22 21:54:25 +08:00
|
|
|
import {
|
|
|
|
|
Select,
|
|
|
|
|
SelectContent,
|
|
|
|
|
SelectItem,
|
|
|
|
|
SelectTrigger,
|
|
|
|
|
SelectValue,
|
|
|
|
|
} from "@/components/ui/select"
|
2025-12-06 21:42:28 +09:00
|
|
|
import { Switch } from "@/components/ui/switch"
|
2026-03-07 19:07:54 +09:00
|
|
|
import { Textarea } from "@/components/ui/textarea"
|
2025-12-20 20:18:54 +05:30
|
|
|
import { useDictionary } from "@/hooks/use-dictionary"
|
2025-12-22 19:58:55 +05:30
|
|
|
import { getApiEndpoint } from "@/lib/base-path"
|
2026-05-15 22:14:20 +08:00
|
|
|
import type { DrawioTheme } from "@/lib/drawio-themes"
|
2025-12-22 21:54:25 +08:00
|
|
|
import { i18n, type Locale } from "@/lib/i18n/config"
|
2026-01-11 07:54:32 +06:00
|
|
|
import { STORAGE_KEYS } from "@/lib/storage"
|
2025-12-23 21:52:04 +09:00
|
|
|
|
|
|
|
|
// Reusable setting item component for consistent layout
|
|
|
|
|
function SettingItem({
|
|
|
|
|
label,
|
|
|
|
|
description,
|
|
|
|
|
children,
|
|
|
|
|
}: {
|
|
|
|
|
label: string
|
|
|
|
|
description?: string
|
|
|
|
|
children: React.ReactNode
|
|
|
|
|
}) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex items-center justify-between py-4 first:pt-0 last:pb-0">
|
|
|
|
|
<div className="space-y-0.5 pr-4">
|
|
|
|
|
<Label className="text-sm font-medium">{label}</Label>
|
|
|
|
|
{description && (
|
|
|
|
|
<p className="text-xs text-muted-foreground max-w-[260px]">
|
|
|
|
|
{description}
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="shrink-0">{children}</div>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
2025-12-22 21:54:25 +08:00
|
|
|
|
|
|
|
|
const LANGUAGE_LABELS: Record<Locale, string> = {
|
|
|
|
|
en: "English",
|
|
|
|
|
zh: "中文",
|
|
|
|
|
ja: "日本語",
|
2026-01-27 13:21:20 +09:00
|
|
|
"zh-Hant": "繁體中文",
|
2025-12-22 21:54:25 +08:00
|
|
|
}
|
2025-12-05 21:09:34 +08:00
|
|
|
|
|
|
|
|
interface SettingsDialogProps {
|
2025-12-06 12:46:40 +09:00
|
|
|
open: boolean
|
|
|
|
|
onOpenChange: (open: boolean) => void
|
2026-05-15 22:14:20 +08:00
|
|
|
drawioUi: DrawioTheme
|
|
|
|
|
onDrawioUiChange: (theme: DrawioTheme) => void
|
2025-12-10 08:21:15 +08:00
|
|
|
darkMode: boolean
|
|
|
|
|
onToggleDarkMode: () => void
|
2025-12-28 18:46:10 +05:30
|
|
|
minimalStyle?: boolean
|
|
|
|
|
onMinimalStyleChange?: (value: boolean) => void
|
2026-01-20 20:52:04 +09:00
|
|
|
vlmValidationEnabled?: boolean
|
|
|
|
|
onVlmValidationChange?: (value: boolean) => void
|
2026-01-28 14:36:54 +01:00
|
|
|
onOpenModelConfig?: () => void
|
2026-03-07 19:07:54 +09:00
|
|
|
customSystemMessage?: string
|
|
|
|
|
onCustomSystemMessageChange?: (value: string) => void
|
fix: raise the output budget so reasoning models reach the tool call
A reasoning model spends the output budget in order: thinking first, then prose,
then the tool call. With 16000 the thinking alone can consume all of it, so the
turn ends with finishReason "length" before display_diagram is ever called. The
canvas stays empty and nothing surfaces in the UI, because no tool call means no
tool error, and the client never reads finishReason.
Measured on openrouter deepseek/deepseek-v4-flash, the model from the report:
- max_tokens=800 with reasoning on returns reasoning_tokens=800, empty content,
finish_reason length. So reasoning is billed against this budget, not exempt.
- refining an existing diagram (19k chars of XML in the input) produced 49142
chars of reasoning, zero tool calls, finishReason "length" at 16000
- the same request at 40000 finished and called edit_diagram with 12 operations
64000 cannot just be sent to every model: bedrock claude-3-haiku caps at 4096,
nova-lite at 10000, and the openrouter deepseek-r1 endpoint counts input and
output against one 64000 ceiling. All three name the real limit in the 400, so
parse it and retry once. Verified: nova-lite logs "64000 rejected, retrying with
10000" and then completes its tool call.
Also expose the budget in Settings. It is sent as a header rather than read from
env only, so desktop users can raise it themselves without an env file.
vercel.json goes back to the 300s it had before #238 traded it for $2-4/month.
That is now Vercel's own default, and billing pauses while the function waits on
the model, so the saving that motivated 120s no longer applies. edgeone.json is
left alone: its 120 may be that platform's actual ceiling.
2026-08-22 11:45:09 +09:00
|
|
|
maxOutputTokens?: string
|
|
|
|
|
onMaxOutputTokensChange?: (value: string) => void
|
2025-12-05 21:09:34 +08:00
|
|
|
}
|
|
|
|
|
|
2025-12-06 12:46:40 +09:00
|
|
|
export const STORAGE_ACCESS_CODE_KEY = "next-ai-draw-io-access-code"
|
2025-12-08 11:00:14 +09:00
|
|
|
const STORAGE_ACCESS_CODE_REQUIRED_KEY = "next-ai-draw-io-access-code-required"
|
|
|
|
|
|
|
|
|
|
function getStoredAccessCodeRequired(): boolean | null {
|
|
|
|
|
if (typeof window === "undefined") return null
|
|
|
|
|
const stored = localStorage.getItem(STORAGE_ACCESS_CODE_REQUIRED_KEY)
|
|
|
|
|
if (stored === null) return null
|
|
|
|
|
return stored === "true"
|
|
|
|
|
}
|
2025-12-05 21:09:34 +08:00
|
|
|
|
2025-12-22 21:54:25 +08:00
|
|
|
function SettingsContent({
|
2025-12-06 21:42:28 +09:00
|
|
|
open,
|
|
|
|
|
onOpenChange,
|
2025-12-10 08:21:15 +08:00
|
|
|
drawioUi,
|
2026-05-15 22:14:20 +08:00
|
|
|
onDrawioUiChange,
|
2025-12-10 08:21:15 +08:00
|
|
|
darkMode,
|
|
|
|
|
onToggleDarkMode,
|
2025-12-28 18:46:10 +05:30
|
|
|
minimalStyle = false,
|
|
|
|
|
onMinimalStyleChange = () => {},
|
2026-01-20 20:52:04 +09:00
|
|
|
vlmValidationEnabled = false,
|
|
|
|
|
onVlmValidationChange = () => {},
|
2026-01-28 14:36:54 +01:00
|
|
|
onOpenModelConfig,
|
2026-03-07 19:07:54 +09:00
|
|
|
customSystemMessage = "",
|
|
|
|
|
onCustomSystemMessageChange = () => {},
|
fix: raise the output budget so reasoning models reach the tool call
A reasoning model spends the output budget in order: thinking first, then prose,
then the tool call. With 16000 the thinking alone can consume all of it, so the
turn ends with finishReason "length" before display_diagram is ever called. The
canvas stays empty and nothing surfaces in the UI, because no tool call means no
tool error, and the client never reads finishReason.
Measured on openrouter deepseek/deepseek-v4-flash, the model from the report:
- max_tokens=800 with reasoning on returns reasoning_tokens=800, empty content,
finish_reason length. So reasoning is billed against this budget, not exempt.
- refining an existing diagram (19k chars of XML in the input) produced 49142
chars of reasoning, zero tool calls, finishReason "length" at 16000
- the same request at 40000 finished and called edit_diagram with 12 operations
64000 cannot just be sent to every model: bedrock claude-3-haiku caps at 4096,
nova-lite at 10000, and the openrouter deepseek-r1 endpoint counts input and
output against one 64000 ceiling. All three name the real limit in the 400, so
parse it and retry once. Verified: nova-lite logs "64000 rejected, retrying with
10000" and then completes its tool call.
Also expose the budget in Settings. It is sent as a header rather than read from
env only, so desktop users can raise it themselves without an env file.
vercel.json goes back to the 300s it had before #238 traded it for $2-4/month.
That is now Vercel's own default, and billing pauses while the function waits on
the model, so the saving that motivated 120s no longer applies. edgeone.json is
left alone: its 120 may be that platform's actual ceiling.
2026-08-22 11:45:09 +09:00
|
|
|
maxOutputTokens = "",
|
|
|
|
|
onMaxOutputTokensChange = () => {},
|
2025-12-06 21:42:28 +09:00
|
|
|
}: SettingsDialogProps) {
|
2025-12-20 20:18:54 +05:30
|
|
|
const dict = useDictionary()
|
2025-12-22 21:54:25 +08:00
|
|
|
const router = useRouter()
|
|
|
|
|
const pathname = usePathname() || "/"
|
|
|
|
|
const search = useSearchParams()
|
2025-12-06 12:46:40 +09:00
|
|
|
const [accessCode, setAccessCode] = useState("")
|
2025-12-07 00:21:59 +09:00
|
|
|
const [isVerifying, setIsVerifying] = useState(false)
|
|
|
|
|
const [error, setError] = useState("")
|
2025-12-08 11:00:14 +09:00
|
|
|
const [accessCodeRequired, setAccessCodeRequired] = useState(
|
|
|
|
|
() => getStoredAccessCodeRequired() ?? false,
|
|
|
|
|
)
|
2025-12-22 21:54:25 +08:00
|
|
|
const [currentLang, setCurrentLang] = useState("en")
|
2026-01-11 07:54:32 +06:00
|
|
|
const [sendShortcut, setSendShortcut] = useState("ctrl-enter")
|
2025-12-08 11:00:14 +09:00
|
|
|
|
feat: add personal My Templates library alongside Quick Examples (#773)
* 增加了ralph自动化编程梳理
* feat: US-001 - 为模板库建立独立的 IndexedDB 存储层
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: US-002 - 在空聊天状态用我的模板库替换官方示例
- 将 ChatLobby 中的 Quick Examples 替换为 TemplatePanel
- 当没有历史会话时,展示完整的模板库面板
- 当有历史会话时,展示可折叠的 "My Templates" 区域
- 使用 TemplatePanel 组件展示用户的个人模板库
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: US-004 - Provide template creation flow
- Create TemplateCreateDialog component with form fields for prompt, title, description, tags, and pinned
- Add i18n translations for template creation UI in en, zh, zh-Hant, ja
- Update TemplatePanel to integrate the create dialog
- Support initialPrompt prop for pre-filling from current input
- Validate required prompt field (empty prompt not allowed)
- Auto-generate default title from first 20 chars of Pin templates appear at top of list
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: US-005 - 为模板卡片提供编辑、删除和复制操作
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: US-006 - Send template directly on click and record usage statistics
- Implement click-to-send template functionality with confirmation dialog
- Add clickCount and runCount increment logic
- Display runCount and lastUsedAt on template card
- Add i18n translations for confirmation dialog (en, zh, zh-Hant, ja)
- Pass onSendTemplate and currentInput props through component hierarchy
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: US-007 - Support template search, pin and default sorting
- Add search bar to TemplatePanel with real-time filtering by title, description, and tags
- Add pin/unpin toggle button on template cards (uses Bookmark icon with fill indicator)
- Search uses existing searchTemplates function from template-storage
- Sort uses existing sortTemplates function (pinned desc, runCount desc, lastUsedAt desc, updatedAt desc)
- Show empty state with Search icon when search returns no results
- List re-sorts immediately after pin/unpin toggle
- Add i18n keys: searchPlaceholder, searchNoResults, pin, unpin for all 4 languages
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: US-008 - Support saving current input as template
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: US-009 - Support saving historical user message as template
- Add "Save as Template" button to user messages
- Pre-fill prompt with original user message text
- Only show on user messages,- Dialog opens TemplateCreateDialog on click
- Template appears in list immediately after saving
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: US-010 - Support template import and export
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* chore: remove local-only dirs from git tracking (.agents, .cursor, scripts, screenshots)
These directories contain local IDE configs, agent scripts, and
dev tooling that should not be part of the upstream repository.
Added them to .gitignore to prevent future accidental commits.
* chore: remove AGENTS.md from git tracking
* fix: add missing i18n keys for template export/import (en/zh/zh-Hant/ja)
* fix: review fixes for my-templates PR
- Fix fragile querySelector("form") with id-based lookup
- Add objectStoreNames.contains guards for IndexedDB upgrades
- Remove duplicate TemplateSchema, import from template-storage
- Revert contributor-specific .gitignore additions
- Fix broken i18n placeholders and missing translations (zh/ja/zh-Hant)
- Remove unused setFiles prop from ChatLobby
- Remove tags feature (unnecessary complexity)
- Improve template card layout: overlay icons on hover, align stats
- Add break-all and overflow-hidden for long prompt text in dialogs
- Move incrementClickCount into sendTemplate for accurate tracking
- Use Intl.RelativeTimeFormat for locale-aware relative time
* feat: restore Quick Examples panel and add lobby panel visibility settings
Bring back the ExamplePanel as a third collapsible section in ChatLobby
alongside Recent Chats and My Templates. Add toggle switches in Settings
to show/hide each lobby panel, persisted via localStorage.
* fix: template send race condition, import defaults, and empty title bug
- Use flushSync instead of setTimeout(0) in handleSendTemplate to
ensure React state is flushed before form submission
- Explicitly validate and default all fields in importTemplates to
prevent undefined counters from malformed import JSON
- Fall back to existing title in edit dialog instead of writing undefined
* fix: address Copilot review comments and remove PRD file
- Fix fallback formatLastUsed returning "Not used yet" for recent usage
- Remove dead mounted flag in TemplatePanel useEffect
- Respect panel visibility settings in no-history lobby state
- Reject empty/whitespace titles in import validation
- Trim title/prompt in importTemplates with default title fallback
- Remove tasks/prd-template-library-replaces-examples.md from repo
* fix: remove double sort, dead code, redundant stats, and break-all CSS
- Remove redundant sortTemplates call in loadTemplates (already sorted by getAllTemplates)
- Remove unused createEmptyTemplateInput and sortTemplates import
- Show "Not used yet" only once for unused templates instead of twice
- Use break-words instead of break-all on prompt textareas
---------
Co-authored-by: 杜雷 <dreamfly@126.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-04-03 11:16:01 +08:00
|
|
|
// Panel visibility state
|
|
|
|
|
const [showRecentChats, setShowRecentChats] = useState(true)
|
|
|
|
|
const [showMyTemplates, setShowMyTemplates] = useState(true)
|
|
|
|
|
const [showQuickExamples, setShowQuickExamples] = useState(true)
|
|
|
|
|
|
|
|
|
|
const handlePanelToggle = useCallback(
|
|
|
|
|
(key: string, value: boolean, setter: (v: boolean) => void) => {
|
|
|
|
|
setter(value)
|
|
|
|
|
localStorage.setItem(key, String(value))
|
|
|
|
|
window.dispatchEvent(new CustomEvent("panelVisibilityChange"))
|
|
|
|
|
},
|
|
|
|
|
[],
|
|
|
|
|
)
|
|
|
|
|
|
2026-01-09 09:26:19 +09:00
|
|
|
// Proxy settings state (Electron only)
|
|
|
|
|
const [httpProxy, setHttpProxy] = useState("")
|
|
|
|
|
const [httpsProxy, setHttpsProxy] = useState("")
|
|
|
|
|
const [isApplyingProxy, setIsApplyingProxy] = useState(false)
|
|
|
|
|
|
2025-12-08 11:00:14 +09:00
|
|
|
useEffect(() => {
|
2026-05-15 09:52:51 +08:00
|
|
|
// Re-fetch config whenever the dialog opens to ensure we always show
|
|
|
|
|
// the access code input if the server requires it. This fixes the case
|
|
|
|
|
// where a stale localStorage cache (from before ACCESS_CODE_LIST was
|
|
|
|
|
// configured) would hide the access code input.
|
|
|
|
|
if (!open) return
|
2025-12-08 11:00:14 +09:00
|
|
|
|
2025-12-22 19:58:55 +05:30
|
|
|
fetch(getApiEndpoint("/api/config"))
|
2025-12-08 11:00:14 +09:00
|
|
|
.then((res) => {
|
|
|
|
|
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
|
|
|
return res.json()
|
|
|
|
|
})
|
|
|
|
|
.then((data) => {
|
|
|
|
|
const required = data?.accessCodeRequired === true
|
|
|
|
|
localStorage.setItem(
|
|
|
|
|
STORAGE_ACCESS_CODE_REQUIRED_KEY,
|
|
|
|
|
String(required),
|
|
|
|
|
)
|
|
|
|
|
setAccessCodeRequired(required)
|
|
|
|
|
})
|
|
|
|
|
.catch(() => {
|
2026-05-15 09:52:51 +08:00
|
|
|
// Keep existing cached value on error
|
2025-12-08 11:00:14 +09:00
|
|
|
})
|
2026-05-15 09:52:51 +08:00
|
|
|
}, [open])
|
2025-12-05 21:09:34 +08:00
|
|
|
|
2025-12-22 21:54:25 +08:00
|
|
|
// Detect current language from pathname
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const seg = pathname.split("/").filter(Boolean)
|
|
|
|
|
const first = seg[0]
|
|
|
|
|
if (first && i18n.locales.includes(first as Locale)) {
|
|
|
|
|
setCurrentLang(first)
|
|
|
|
|
} else {
|
|
|
|
|
setCurrentLang(i18n.defaultLocale)
|
|
|
|
|
}
|
|
|
|
|
}, [pathname])
|
|
|
|
|
|
2025-12-05 21:09:34 +08:00
|
|
|
useEffect(() => {
|
|
|
|
|
if (open) {
|
2025-12-06 12:46:40 +09:00
|
|
|
const storedCode =
|
|
|
|
|
localStorage.getItem(STORAGE_ACCESS_CODE_KEY) || ""
|
|
|
|
|
setAccessCode(storedCode)
|
2025-12-06 21:42:28 +09:00
|
|
|
|
2026-01-11 07:54:32 +06:00
|
|
|
const storedSendShortcut = localStorage.getItem(
|
|
|
|
|
STORAGE_KEYS.sendShortcut,
|
|
|
|
|
)
|
|
|
|
|
setSendShortcut(storedSendShortcut || "ctrl-enter")
|
|
|
|
|
|
feat: add personal My Templates library alongside Quick Examples (#773)
* 增加了ralph自动化编程梳理
* feat: US-001 - 为模板库建立独立的 IndexedDB 存储层
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: US-002 - 在空聊天状态用我的模板库替换官方示例
- 将 ChatLobby 中的 Quick Examples 替换为 TemplatePanel
- 当没有历史会话时,展示完整的模板库面板
- 当有历史会话时,展示可折叠的 "My Templates" 区域
- 使用 TemplatePanel 组件展示用户的个人模板库
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: US-004 - Provide template creation flow
- Create TemplateCreateDialog component with form fields for prompt, title, description, tags, and pinned
- Add i18n translations for template creation UI in en, zh, zh-Hant, ja
- Update TemplatePanel to integrate the create dialog
- Support initialPrompt prop for pre-filling from current input
- Validate required prompt field (empty prompt not allowed)
- Auto-generate default title from first 20 chars of Pin templates appear at top of list
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: US-005 - 为模板卡片提供编辑、删除和复制操作
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: US-006 - Send template directly on click and record usage statistics
- Implement click-to-send template functionality with confirmation dialog
- Add clickCount and runCount increment logic
- Display runCount and lastUsedAt on template card
- Add i18n translations for confirmation dialog (en, zh, zh-Hant, ja)
- Pass onSendTemplate and currentInput props through component hierarchy
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: US-007 - Support template search, pin and default sorting
- Add search bar to TemplatePanel with real-time filtering by title, description, and tags
- Add pin/unpin toggle button on template cards (uses Bookmark icon with fill indicator)
- Search uses existing searchTemplates function from template-storage
- Sort uses existing sortTemplates function (pinned desc, runCount desc, lastUsedAt desc, updatedAt desc)
- Show empty state with Search icon when search returns no results
- List re-sorts immediately after pin/unpin toggle
- Add i18n keys: searchPlaceholder, searchNoResults, pin, unpin for all 4 languages
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: US-008 - Support saving current input as template
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: US-009 - Support saving historical user message as template
- Add "Save as Template" button to user messages
- Pre-fill prompt with original user message text
- Only show on user messages,- Dialog opens TemplateCreateDialog on click
- Template appears in list immediately after saving
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: US-010 - Support template import and export
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* chore: remove local-only dirs from git tracking (.agents, .cursor, scripts, screenshots)
These directories contain local IDE configs, agent scripts, and
dev tooling that should not be part of the upstream repository.
Added them to .gitignore to prevent future accidental commits.
* chore: remove AGENTS.md from git tracking
* fix: add missing i18n keys for template export/import (en/zh/zh-Hant/ja)
* fix: review fixes for my-templates PR
- Fix fragile querySelector("form") with id-based lookup
- Add objectStoreNames.contains guards for IndexedDB upgrades
- Remove duplicate TemplateSchema, import from template-storage
- Revert contributor-specific .gitignore additions
- Fix broken i18n placeholders and missing translations (zh/ja/zh-Hant)
- Remove unused setFiles prop from ChatLobby
- Remove tags feature (unnecessary complexity)
- Improve template card layout: overlay icons on hover, align stats
- Add break-all and overflow-hidden for long prompt text in dialogs
- Move incrementClickCount into sendTemplate for accurate tracking
- Use Intl.RelativeTimeFormat for locale-aware relative time
* feat: restore Quick Examples panel and add lobby panel visibility settings
Bring back the ExamplePanel as a third collapsible section in ChatLobby
alongside Recent Chats and My Templates. Add toggle switches in Settings
to show/hide each lobby panel, persisted via localStorage.
* fix: template send race condition, import defaults, and empty title bug
- Use flushSync instead of setTimeout(0) in handleSendTemplate to
ensure React state is flushed before form submission
- Explicitly validate and default all fields in importTemplates to
prevent undefined counters from malformed import JSON
- Fall back to existing title in edit dialog instead of writing undefined
* fix: address Copilot review comments and remove PRD file
- Fix fallback formatLastUsed returning "Not used yet" for recent usage
- Remove dead mounted flag in TemplatePanel useEffect
- Respect panel visibility settings in no-history lobby state
- Reject empty/whitespace titles in import validation
- Trim title/prompt in importTemplates with default title fallback
- Remove tasks/prd-template-library-replaces-examples.md from repo
* fix: remove double sort, dead code, redundant stats, and break-all CSS
- Remove redundant sortTemplates call in loadTemplates (already sorted by getAllTemplates)
- Remove unused createEmptyTemplateInput and sortTemplates import
- Show "Not used yet" only once for unused templates instead of twice
- Use break-words instead of break-all on prompt textareas
---------
Co-authored-by: 杜雷 <dreamfly@126.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-04-03 11:16:01 +08:00
|
|
|
setShowRecentChats(
|
|
|
|
|
localStorage.getItem(STORAGE_KEYS.showRecentChats) !== "false",
|
|
|
|
|
)
|
|
|
|
|
setShowMyTemplates(
|
|
|
|
|
localStorage.getItem(STORAGE_KEYS.showMyTemplates) !== "false",
|
|
|
|
|
)
|
|
|
|
|
setShowQuickExamples(
|
|
|
|
|
localStorage.getItem(STORAGE_KEYS.showQuickExamples) !==
|
|
|
|
|
"false",
|
|
|
|
|
)
|
|
|
|
|
|
2025-12-07 00:21:59 +09:00
|
|
|
setError("")
|
2026-01-09 09:26:19 +09:00
|
|
|
|
|
|
|
|
// Load proxy settings (Electron only)
|
|
|
|
|
if (window.electronAPI?.getProxy) {
|
|
|
|
|
window.electronAPI.getProxy().then((config) => {
|
|
|
|
|
setHttpProxy(config.httpProxy || "")
|
|
|
|
|
setHttpsProxy(config.httpsProxy || "")
|
|
|
|
|
})
|
|
|
|
|
}
|
2025-12-05 21:09:34 +08:00
|
|
|
}
|
2025-12-06 12:46:40 +09:00
|
|
|
}, [open])
|
2025-12-05 21:09:34 +08:00
|
|
|
|
2025-12-22 21:54:25 +08:00
|
|
|
const changeLanguage = (lang: string) => {
|
2025-12-25 22:20:59 +09:00
|
|
|
// Save locale to localStorage for persistence across restarts
|
|
|
|
|
localStorage.setItem("next-ai-draw-io-locale", lang)
|
|
|
|
|
|
2026-01-17 23:10:57 +05:30
|
|
|
// Notify Electron main process to update its menu language
|
|
|
|
|
if (window.electronAPI?.setUserLocale) {
|
|
|
|
|
window.electronAPI.setUserLocale(lang).catch((error) => {
|
|
|
|
|
console.error("Failed to sync locale with Electron:", error)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-22 21:54:25 +08:00
|
|
|
const parts = pathname.split("/")
|
|
|
|
|
if (parts.length > 1 && i18n.locales.includes(parts[1] as Locale)) {
|
|
|
|
|
parts[1] = lang
|
|
|
|
|
} else {
|
|
|
|
|
parts.splice(1, 0, lang)
|
|
|
|
|
}
|
|
|
|
|
const newPath = parts.join("/") || "/"
|
|
|
|
|
const searchStr = search?.toString() ? `?${search.toString()}` : ""
|
|
|
|
|
router.push(newPath + searchStr)
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-07 00:21:59 +09:00
|
|
|
const handleSave = async () => {
|
2025-12-08 11:00:14 +09:00
|
|
|
if (!accessCodeRequired) return
|
|
|
|
|
|
2025-12-07 00:21:59 +09:00
|
|
|
setError("")
|
|
|
|
|
setIsVerifying(true)
|
|
|
|
|
|
|
|
|
|
try {
|
2025-12-22 19:58:55 +05:30
|
|
|
const response = await fetch(
|
|
|
|
|
getApiEndpoint("/api/verify-access-code"),
|
|
|
|
|
{
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: {
|
|
|
|
|
"x-access-code": accessCode.trim(),
|
|
|
|
|
},
|
2025-12-07 00:21:59 +09:00
|
|
|
},
|
2025-12-22 19:58:55 +05:30
|
|
|
)
|
2025-12-07 00:21:59 +09:00
|
|
|
|
|
|
|
|
const data = await response.json()
|
|
|
|
|
|
|
|
|
|
if (!data.valid) {
|
2025-12-20 20:18:54 +05:30
|
|
|
setError(data.message || dict.errors.invalidAccessCode)
|
2025-12-07 00:21:59 +09:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
localStorage.setItem(STORAGE_ACCESS_CODE_KEY, accessCode.trim())
|
|
|
|
|
onOpenChange(false)
|
|
|
|
|
} catch {
|
2025-12-20 20:18:54 +05:30
|
|
|
setError(dict.errors.networkError)
|
2025-12-07 00:21:59 +09:00
|
|
|
} finally {
|
|
|
|
|
setIsVerifying(false)
|
|
|
|
|
}
|
2025-12-06 12:46:40 +09:00
|
|
|
}
|
2025-12-05 21:09:34 +08:00
|
|
|
|
|
|
|
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
|
|
|
if (e.key === "Enter") {
|
2025-12-06 12:46:40 +09:00
|
|
|
e.preventDefault()
|
|
|
|
|
handleSave()
|
2025-12-05 21:09:34 +08:00
|
|
|
}
|
2025-12-06 12:46:40 +09:00
|
|
|
}
|
2025-12-05 21:09:34 +08:00
|
|
|
|
2026-01-09 09:26:19 +09:00
|
|
|
const handleApplyProxy = async () => {
|
|
|
|
|
if (!window.electronAPI?.setProxy) return
|
|
|
|
|
|
|
|
|
|
// Validate proxy URLs (must start with http:// or https://)
|
|
|
|
|
const validateProxyUrl = (url: string): boolean => {
|
|
|
|
|
if (!url) return true // Empty is OK
|
|
|
|
|
return url.startsWith("http://") || url.startsWith("https://")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const trimmedHttp = httpProxy.trim()
|
|
|
|
|
const trimmedHttps = httpsProxy.trim()
|
|
|
|
|
|
|
|
|
|
if (trimmedHttp && !validateProxyUrl(trimmedHttp)) {
|
|
|
|
|
toast.error("HTTP Proxy must start with http:// or https://")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if (trimmedHttps && !validateProxyUrl(trimmedHttps)) {
|
|
|
|
|
toast.error("HTTPS Proxy must start with http:// or https://")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setIsApplyingProxy(true)
|
|
|
|
|
try {
|
|
|
|
|
const result = await window.electronAPI.setProxy({
|
|
|
|
|
httpProxy: trimmedHttp || undefined,
|
|
|
|
|
httpsProxy: trimmedHttps || undefined,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (result.success) {
|
|
|
|
|
toast.success(dict.settings.proxyApplied)
|
|
|
|
|
} else {
|
|
|
|
|
toast.error(result.error || "Failed to apply proxy settings")
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
toast.error("Failed to apply proxy settings")
|
|
|
|
|
} finally {
|
|
|
|
|
setIsApplyingProxy(false)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-05 21:09:34 +08:00
|
|
|
return (
|
2026-03-24 17:43:53 +05:30
|
|
|
<DialogContent className="sm:max-w-lg p-0 gap-0 max-h-[90vh] flex flex-col overflow-hidden">
|
2025-12-23 21:52:04 +09:00
|
|
|
{/* Header */}
|
|
|
|
|
<DialogHeader className="px-6 pt-6 pb-4">
|
2025-12-22 21:54:25 +08:00
|
|
|
<DialogTitle>{dict.settings.title}</DialogTitle>
|
2025-12-23 21:52:04 +09:00
|
|
|
<DialogDescription className="mt-1">
|
2025-12-22 21:54:25 +08:00
|
|
|
{dict.settings.description}
|
|
|
|
|
</DialogDescription>
|
|
|
|
|
</DialogHeader>
|
2025-12-23 21:52:04 +09:00
|
|
|
|
|
|
|
|
{/* Content */}
|
2026-03-20 12:31:16 +00:00
|
|
|
<div className="px-6 pb-6 overflow-y-auto flex-1 scrollbar-thin">
|
2025-12-23 21:52:04 +09:00
|
|
|
<div className="divide-y divide-border-subtle">
|
2026-01-28 14:36:54 +01:00
|
|
|
{/* API Keys & Models */}
|
|
|
|
|
{onOpenModelConfig && (
|
|
|
|
|
<SettingItem
|
|
|
|
|
label={dict.settings.apiKeysModels}
|
|
|
|
|
description={dict.settings.apiKeysModelsDescription}
|
|
|
|
|
>
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="sm"
|
|
|
|
|
className="h-9 w-9 p-0"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
onOpenChange(false)
|
|
|
|
|
onOpenModelConfig()
|
|
|
|
|
}}
|
|
|
|
|
aria-label={dict.settings.apiKeysModels}
|
|
|
|
|
>
|
|
|
|
|
<ChevronRight className="h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
</SettingItem>
|
|
|
|
|
)}
|
|
|
|
|
|
2025-12-23 21:52:04 +09:00
|
|
|
{/* Access Code (conditional) */}
|
|
|
|
|
{accessCodeRequired && (
|
|
|
|
|
<div className="py-4 first:pt-0 space-y-3">
|
|
|
|
|
<div className="space-y-0.5">
|
|
|
|
|
<Label
|
|
|
|
|
htmlFor="access-code"
|
|
|
|
|
className="text-sm font-medium"
|
|
|
|
|
>
|
|
|
|
|
{dict.settings.accessCode}
|
|
|
|
|
</Label>
|
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
{dict.settings.accessCodeDescription}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
<Input
|
|
|
|
|
id="access-code"
|
|
|
|
|
type="password"
|
|
|
|
|
value={accessCode}
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
setAccessCode(e.target.value)
|
|
|
|
|
}
|
|
|
|
|
onKeyDown={handleKeyDown}
|
|
|
|
|
placeholder={
|
|
|
|
|
dict.settings.accessCodePlaceholder
|
|
|
|
|
}
|
|
|
|
|
autoComplete="off"
|
|
|
|
|
className="h-9"
|
|
|
|
|
/>
|
|
|
|
|
<Button
|
|
|
|
|
onClick={handleSave}
|
|
|
|
|
disabled={isVerifying || !accessCode.trim()}
|
|
|
|
|
className="h-9 px-4 rounded-xl"
|
|
|
|
|
>
|
|
|
|
|
{isVerifying ? "..." : dict.common.save}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
{error && (
|
|
|
|
|
<p className="text-xs text-destructive">
|
|
|
|
|
{error}
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
2025-12-08 11:00:14 +09:00
|
|
|
</div>
|
2025-12-23 21:52:04 +09:00
|
|
|
)}
|
2025-12-10 08:21:15 +08:00
|
|
|
|
2025-12-23 21:52:04 +09:00
|
|
|
{/* Language */}
|
|
|
|
|
<SettingItem
|
|
|
|
|
label={dict.settings.language}
|
|
|
|
|
description={dict.settings.languageDescription}
|
|
|
|
|
>
|
|
|
|
|
<Select
|
|
|
|
|
value={currentLang}
|
|
|
|
|
onValueChange={changeLanguage}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger
|
|
|
|
|
id="language-select"
|
|
|
|
|
className="w-[120px] h-9 rounded-xl"
|
|
|
|
|
>
|
|
|
|
|
<SelectValue />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
{i18n.locales.map((locale) => (
|
|
|
|
|
<SelectItem key={locale} value={locale}>
|
|
|
|
|
{LANGUAGE_LABELS[locale]}
|
|
|
|
|
</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
</SettingItem>
|
2025-12-10 08:21:15 +08:00
|
|
|
|
2025-12-23 21:52:04 +09:00
|
|
|
{/* Theme */}
|
|
|
|
|
<SettingItem
|
|
|
|
|
label={dict.settings.theme}
|
|
|
|
|
description={dict.settings.themeDescription}
|
2025-12-22 21:54:25 +08:00
|
|
|
>
|
2025-12-23 21:52:04 +09:00
|
|
|
<Button
|
|
|
|
|
id="theme-toggle"
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="icon"
|
|
|
|
|
onClick={onToggleDarkMode}
|
|
|
|
|
className="h-9 w-9 rounded-xl border-border-subtle hover:bg-interactive-hover"
|
|
|
|
|
>
|
|
|
|
|
{darkMode ? (
|
|
|
|
|
<Sun className="h-4 w-4" />
|
|
|
|
|
) : (
|
|
|
|
|
<Moon className="h-4 w-4" />
|
|
|
|
|
)}
|
|
|
|
|
</Button>
|
|
|
|
|
</SettingItem>
|
2025-12-22 21:54:25 +08:00
|
|
|
|
2025-12-23 21:52:04 +09:00
|
|
|
{/* Draw.io Style */}
|
|
|
|
|
<SettingItem
|
|
|
|
|
label={dict.settings.drawioStyle}
|
2026-05-15 22:14:20 +08:00
|
|
|
description={dict.settings.drawioStyleDescription}
|
2025-12-22 21:54:25 +08:00
|
|
|
>
|
2026-05-15 22:14:20 +08:00
|
|
|
<Select
|
|
|
|
|
value={drawioUi}
|
|
|
|
|
onValueChange={(v) =>
|
|
|
|
|
onDrawioUiChange(v as DrawioTheme)
|
|
|
|
|
}
|
2025-12-23 21:52:04 +09:00
|
|
|
>
|
2026-05-15 22:14:20 +08:00
|
|
|
<SelectTrigger
|
|
|
|
|
id="drawio-ui-select"
|
|
|
|
|
aria-label={dict.settings.drawioStyle}
|
|
|
|
|
className="w-[120px] h-9 rounded-xl"
|
|
|
|
|
>
|
|
|
|
|
<SelectValue />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
<SelectItem value="kennedy">
|
|
|
|
|
{dict.settings.themeDefault}
|
|
|
|
|
</SelectItem>
|
|
|
|
|
<SelectItem value="atlas">Atlas</SelectItem>
|
|
|
|
|
<SelectItem value="dark">
|
|
|
|
|
{dict.settings.themeDark}
|
|
|
|
|
</SelectItem>
|
|
|
|
|
<SelectItem value="min">
|
|
|
|
|
{dict.settings.themeMinimal}
|
|
|
|
|
</SelectItem>
|
|
|
|
|
<SelectItem value="sketch">
|
|
|
|
|
{dict.settings.themeSketch}
|
|
|
|
|
</SelectItem>
|
|
|
|
|
<SelectItem value="simple">
|
|
|
|
|
{dict.settings.themeSimple}
|
|
|
|
|
</SelectItem>
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
2025-12-23 21:52:04 +09:00
|
|
|
</SettingItem>
|
2025-12-22 21:54:25 +08:00
|
|
|
|
2025-12-28 18:46:10 +05:30
|
|
|
{/* Diagram Style */}
|
|
|
|
|
<SettingItem
|
|
|
|
|
label={dict.settings.diagramStyle}
|
|
|
|
|
description={dict.settings.diagramStyleDescription}
|
|
|
|
|
>
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<Switch
|
|
|
|
|
id="minimal-style"
|
|
|
|
|
checked={minimalStyle}
|
|
|
|
|
onCheckedChange={onMinimalStyleChange}
|
|
|
|
|
/>
|
|
|
|
|
<span className="text-sm text-muted-foreground">
|
|
|
|
|
{minimalStyle
|
|
|
|
|
? dict.chat.minimalStyle
|
|
|
|
|
: dict.chat.styledMode}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</SettingItem>
|
2026-01-11 07:54:32 +06:00
|
|
|
|
feat: add personal My Templates library alongside Quick Examples (#773)
* 增加了ralph自动化编程梳理
* feat: US-001 - 为模板库建立独立的 IndexedDB 存储层
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: US-002 - 在空聊天状态用我的模板库替换官方示例
- 将 ChatLobby 中的 Quick Examples 替换为 TemplatePanel
- 当没有历史会话时,展示完整的模板库面板
- 当有历史会话时,展示可折叠的 "My Templates" 区域
- 使用 TemplatePanel 组件展示用户的个人模板库
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: US-004 - Provide template creation flow
- Create TemplateCreateDialog component with form fields for prompt, title, description, tags, and pinned
- Add i18n translations for template creation UI in en, zh, zh-Hant, ja
- Update TemplatePanel to integrate the create dialog
- Support initialPrompt prop for pre-filling from current input
- Validate required prompt field (empty prompt not allowed)
- Auto-generate default title from first 20 chars of Pin templates appear at top of list
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: US-005 - 为模板卡片提供编辑、删除和复制操作
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: US-006 - Send template directly on click and record usage statistics
- Implement click-to-send template functionality with confirmation dialog
- Add clickCount and runCount increment logic
- Display runCount and lastUsedAt on template card
- Add i18n translations for confirmation dialog (en, zh, zh-Hant, ja)
- Pass onSendTemplate and currentInput props through component hierarchy
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: US-007 - Support template search, pin and default sorting
- Add search bar to TemplatePanel with real-time filtering by title, description, and tags
- Add pin/unpin toggle button on template cards (uses Bookmark icon with fill indicator)
- Search uses existing searchTemplates function from template-storage
- Sort uses existing sortTemplates function (pinned desc, runCount desc, lastUsedAt desc, updatedAt desc)
- Show empty state with Search icon when search returns no results
- List re-sorts immediately after pin/unpin toggle
- Add i18n keys: searchPlaceholder, searchNoResults, pin, unpin for all 4 languages
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: US-008 - Support saving current input as template
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: US-009 - Support saving historical user message as template
- Add "Save as Template" button to user messages
- Pre-fill prompt with original user message text
- Only show on user messages,- Dialog opens TemplateCreateDialog on click
- Template appears in list immediately after saving
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: US-010 - Support template import and export
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* chore: remove local-only dirs from git tracking (.agents, .cursor, scripts, screenshots)
These directories contain local IDE configs, agent scripts, and
dev tooling that should not be part of the upstream repository.
Added them to .gitignore to prevent future accidental commits.
* chore: remove AGENTS.md from git tracking
* fix: add missing i18n keys for template export/import (en/zh/zh-Hant/ja)
* fix: review fixes for my-templates PR
- Fix fragile querySelector("form") with id-based lookup
- Add objectStoreNames.contains guards for IndexedDB upgrades
- Remove duplicate TemplateSchema, import from template-storage
- Revert contributor-specific .gitignore additions
- Fix broken i18n placeholders and missing translations (zh/ja/zh-Hant)
- Remove unused setFiles prop from ChatLobby
- Remove tags feature (unnecessary complexity)
- Improve template card layout: overlay icons on hover, align stats
- Add break-all and overflow-hidden for long prompt text in dialogs
- Move incrementClickCount into sendTemplate for accurate tracking
- Use Intl.RelativeTimeFormat for locale-aware relative time
* feat: restore Quick Examples panel and add lobby panel visibility settings
Bring back the ExamplePanel as a third collapsible section in ChatLobby
alongside Recent Chats and My Templates. Add toggle switches in Settings
to show/hide each lobby panel, persisted via localStorage.
* fix: template send race condition, import defaults, and empty title bug
- Use flushSync instead of setTimeout(0) in handleSendTemplate to
ensure React state is flushed before form submission
- Explicitly validate and default all fields in importTemplates to
prevent undefined counters from malformed import JSON
- Fall back to existing title in edit dialog instead of writing undefined
* fix: address Copilot review comments and remove PRD file
- Fix fallback formatLastUsed returning "Not used yet" for recent usage
- Remove dead mounted flag in TemplatePanel useEffect
- Respect panel visibility settings in no-history lobby state
- Reject empty/whitespace titles in import validation
- Trim title/prompt in importTemplates with default title fallback
- Remove tasks/prd-template-library-replaces-examples.md from repo
* fix: remove double sort, dead code, redundant stats, and break-all CSS
- Remove redundant sortTemplates call in loadTemplates (already sorted by getAllTemplates)
- Remove unused createEmptyTemplateInput and sortTemplates import
- Show "Not used yet" only once for unused templates instead of twice
- Use break-words instead of break-all on prompt textareas
---------
Co-authored-by: 杜雷 <dreamfly@126.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-04-03 11:16:01 +08:00
|
|
|
{/* Panel Visibility */}
|
|
|
|
|
<SettingItem
|
|
|
|
|
label={dict.settings.panelVisibility}
|
|
|
|
|
description={dict.settings.panelVisibilityDescription}
|
|
|
|
|
>
|
|
|
|
|
<div className="flex flex-col gap-2">
|
|
|
|
|
<label className="flex items-center gap-2 cursor-pointer">
|
|
|
|
|
<Switch
|
|
|
|
|
id="show-recent-chats"
|
|
|
|
|
checked={showRecentChats}
|
|
|
|
|
onCheckedChange={(v) =>
|
|
|
|
|
handlePanelToggle(
|
|
|
|
|
STORAGE_KEYS.showRecentChats,
|
|
|
|
|
v,
|
|
|
|
|
setShowRecentChats,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
<span className="text-xs text-muted-foreground">
|
|
|
|
|
{dict.settings.showRecentChats}
|
|
|
|
|
</span>
|
|
|
|
|
</label>
|
|
|
|
|
<label className="flex items-center gap-2 cursor-pointer">
|
|
|
|
|
<Switch
|
|
|
|
|
id="show-my-templates"
|
|
|
|
|
checked={showMyTemplates}
|
|
|
|
|
onCheckedChange={(v) =>
|
|
|
|
|
handlePanelToggle(
|
|
|
|
|
STORAGE_KEYS.showMyTemplates,
|
|
|
|
|
v,
|
|
|
|
|
setShowMyTemplates,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
<span className="text-xs text-muted-foreground">
|
|
|
|
|
{dict.settings.showMyTemplates}
|
|
|
|
|
</span>
|
|
|
|
|
</label>
|
|
|
|
|
<label className="flex items-center gap-2 cursor-pointer">
|
|
|
|
|
<Switch
|
|
|
|
|
id="show-quick-examples"
|
|
|
|
|
checked={showQuickExamples}
|
|
|
|
|
onCheckedChange={(v) =>
|
|
|
|
|
handlePanelToggle(
|
|
|
|
|
STORAGE_KEYS.showQuickExamples,
|
|
|
|
|
v,
|
|
|
|
|
setShowQuickExamples,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
<span className="text-xs text-muted-foreground">
|
|
|
|
|
{dict.settings.showQuickExamples}
|
|
|
|
|
</span>
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
</SettingItem>
|
|
|
|
|
|
2026-01-20 20:52:04 +09:00
|
|
|
{/* VLM Diagram Validation */}
|
|
|
|
|
<SettingItem
|
|
|
|
|
label={dict.settings.diagramValidation}
|
|
|
|
|
description={dict.settings.diagramValidationDescription}
|
|
|
|
|
>
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<Switch
|
|
|
|
|
id="vlm-validation"
|
|
|
|
|
checked={vlmValidationEnabled}
|
|
|
|
|
onCheckedChange={onVlmValidationChange}
|
|
|
|
|
/>
|
|
|
|
|
<span className="text-sm text-muted-foreground">
|
|
|
|
|
{vlmValidationEnabled
|
|
|
|
|
? dict.settings.enabled
|
|
|
|
|
: dict.settings.disabled}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</SettingItem>
|
|
|
|
|
|
2026-03-07 19:07:54 +09:00
|
|
|
{/* Custom System Message */}
|
|
|
|
|
<div className="py-4 space-y-3">
|
|
|
|
|
<div className="space-y-0.5">
|
|
|
|
|
<Label
|
|
|
|
|
htmlFor="custom-system-message"
|
|
|
|
|
className="text-sm font-medium"
|
|
|
|
|
>
|
|
|
|
|
{dict.settings.customSystemMessage}
|
|
|
|
|
</Label>
|
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
{dict.settings.customSystemMessageDescription}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<Textarea
|
|
|
|
|
id="custom-system-message"
|
|
|
|
|
value={customSystemMessage}
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
onCustomSystemMessageChange(e.target.value)
|
|
|
|
|
}
|
|
|
|
|
placeholder={
|
|
|
|
|
dict.settings.customSystemMessagePlaceholder
|
|
|
|
|
}
|
|
|
|
|
className="min-h-[80px] max-h-[160px] text-sm"
|
|
|
|
|
maxLength={5000}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
fix: raise the output budget so reasoning models reach the tool call
A reasoning model spends the output budget in order: thinking first, then prose,
then the tool call. With 16000 the thinking alone can consume all of it, so the
turn ends with finishReason "length" before display_diagram is ever called. The
canvas stays empty and nothing surfaces in the UI, because no tool call means no
tool error, and the client never reads finishReason.
Measured on openrouter deepseek/deepseek-v4-flash, the model from the report:
- max_tokens=800 with reasoning on returns reasoning_tokens=800, empty content,
finish_reason length. So reasoning is billed against this budget, not exempt.
- refining an existing diagram (19k chars of XML in the input) produced 49142
chars of reasoning, zero tool calls, finishReason "length" at 16000
- the same request at 40000 finished and called edit_diagram with 12 operations
64000 cannot just be sent to every model: bedrock claude-3-haiku caps at 4096,
nova-lite at 10000, and the openrouter deepseek-r1 endpoint counts input and
output against one 64000 ceiling. All three name the real limit in the 400, so
parse it and retry once. Verified: nova-lite logs "64000 rejected, retrying with
10000" and then completes its tool call.
Also expose the budget in Settings. It is sent as a header rather than read from
env only, so desktop users can raise it themselves without an env file.
vercel.json goes back to the 300s it had before #238 traded it for $2-4/month.
That is now Vercel's own default, and billing pauses while the function waits on
the model, so the saving that motivated 120s no longer applies. edgeone.json is
left alone: its 120 may be that platform's actual ceiling.
2026-08-22 11:45:09 +09:00
|
|
|
{/* Max Output Tokens */}
|
|
|
|
|
<SettingItem
|
|
|
|
|
label={dict.settings.maxOutputTokens}
|
|
|
|
|
description={dict.settings.maxOutputTokensDescription}
|
|
|
|
|
>
|
|
|
|
|
<Input
|
|
|
|
|
id="max-output-tokens"
|
|
|
|
|
type="text"
|
|
|
|
|
inputMode="numeric"
|
|
|
|
|
value={maxOutputTokens}
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
onMaxOutputTokensChange(e.target.value)
|
|
|
|
|
}
|
|
|
|
|
placeholder="64000"
|
|
|
|
|
className="h-9 w-28 text-sm"
|
|
|
|
|
/>
|
|
|
|
|
</SettingItem>
|
|
|
|
|
|
2026-01-11 07:54:32 +06:00
|
|
|
{/* Send Shortcut */}
|
|
|
|
|
<SettingItem
|
|
|
|
|
label={dict.settings.sendShortcut}
|
|
|
|
|
description={dict.settings.sendShortcutDescription}
|
|
|
|
|
>
|
|
|
|
|
<Select
|
|
|
|
|
value={sendShortcut}
|
|
|
|
|
onValueChange={(value) => {
|
|
|
|
|
setSendShortcut(value)
|
|
|
|
|
localStorage.setItem(
|
|
|
|
|
STORAGE_KEYS.sendShortcut,
|
|
|
|
|
value,
|
|
|
|
|
)
|
|
|
|
|
window.dispatchEvent(
|
|
|
|
|
new CustomEvent("sendShortcutChange", {
|
|
|
|
|
detail: value,
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger
|
|
|
|
|
id="send-shortcut-select"
|
2026-01-20 18:14:18 +08:00
|
|
|
className="w-auto h-9 rounded-xl"
|
2026-01-11 07:54:32 +06:00
|
|
|
>
|
|
|
|
|
<SelectValue />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
<SelectItem value="enter">
|
|
|
|
|
{dict.settings.enterToSend}
|
|
|
|
|
</SelectItem>
|
|
|
|
|
<SelectItem value="ctrl-enter">
|
|
|
|
|
{dict.settings.ctrlEnterToSend}
|
|
|
|
|
</SelectItem>
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
</SettingItem>
|
2026-01-09 09:26:19 +09:00
|
|
|
|
|
|
|
|
{/* Proxy Settings - Electron only */}
|
|
|
|
|
{typeof window !== "undefined" &&
|
|
|
|
|
window.electronAPI?.isElectron && (
|
|
|
|
|
<div className="py-4 space-y-3">
|
|
|
|
|
<div className="space-y-0.5">
|
|
|
|
|
<Label className="text-sm font-medium">
|
|
|
|
|
{dict.settings.proxy}
|
|
|
|
|
</Label>
|
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
{dict.settings.proxyDescription}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Input
|
|
|
|
|
id="http-proxy"
|
|
|
|
|
type="text"
|
|
|
|
|
value={httpProxy}
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
setHttpProxy(e.target.value)
|
|
|
|
|
}
|
|
|
|
|
placeholder={`${dict.settings.httpProxy}: http://proxy:8080`}
|
|
|
|
|
className="h-9"
|
|
|
|
|
/>
|
|
|
|
|
<Input
|
|
|
|
|
id="https-proxy"
|
|
|
|
|
type="text"
|
|
|
|
|
value={httpsProxy}
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
setHttpsProxy(e.target.value)
|
|
|
|
|
}
|
|
|
|
|
placeholder={`${dict.settings.httpsProxy}: http://proxy:8080`}
|
|
|
|
|
className="h-9"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<Button
|
|
|
|
|
onClick={handleApplyProxy}
|
|
|
|
|
disabled={isApplyingProxy}
|
|
|
|
|
className="h-9 px-4 rounded-xl w-full"
|
|
|
|
|
>
|
|
|
|
|
{isApplyingProxy
|
|
|
|
|
? "..."
|
|
|
|
|
: dict.settings.applyProxy}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-12-21 00:55:54 +09:00
|
|
|
</div>
|
2025-12-22 21:54:25 +08:00
|
|
|
</div>
|
2025-12-23 21:52:04 +09:00
|
|
|
|
|
|
|
|
{/* Footer */}
|
|
|
|
|
<div className="px-6 py-4 border-t border-border-subtle bg-surface-1/50 rounded-b-2xl">
|
2025-12-28 18:46:10 +05:30
|
|
|
<div className="flex items-center justify-center gap-3">
|
|
|
|
|
<span className="text-xs text-muted-foreground flex items-center gap-1">
|
|
|
|
|
<Tag className="h-3 w-3" />
|
|
|
|
|
{process.env.APP_VERSION}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="text-muted-foreground">·</span>
|
|
|
|
|
<a
|
|
|
|
|
href="https://github.com/DayuanJiang/next-ai-draw-io"
|
|
|
|
|
target="_blank"
|
|
|
|
|
rel="noopener noreferrer"
|
|
|
|
|
className="text-xs text-muted-foreground hover:text-foreground transition-colors flex items-center gap-1"
|
|
|
|
|
>
|
|
|
|
|
<Github className="h-3 w-3" />
|
|
|
|
|
GitHub
|
|
|
|
|
</a>
|
|
|
|
|
{process.env.NEXT_PUBLIC_SHOW_ABOUT_AND_NOTICE ===
|
|
|
|
|
"true" && (
|
|
|
|
|
<>
|
|
|
|
|
<span className="text-muted-foreground">·</span>
|
|
|
|
|
<a
|
2025-12-30 23:45:31 +09:00
|
|
|
href={`/${currentLang}/about${currentLang === "zh" ? "/cn" : currentLang === "ja" ? "/ja" : ""}`}
|
2025-12-28 18:46:10 +05:30
|
|
|
target="_blank"
|
|
|
|
|
rel="noopener noreferrer"
|
|
|
|
|
className="text-xs text-muted-foreground hover:text-foreground transition-colors flex items-center gap-1"
|
|
|
|
|
>
|
|
|
|
|
<Info className="h-3 w-3" />
|
2025-12-30 23:45:31 +09:00
|
|
|
{dict.nav.about}
|
2025-12-28 18:46:10 +05:30
|
|
|
</a>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2025-12-22 21:54:25 +08:00
|
|
|
</div>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function SettingsDialog(props: SettingsDialogProps) {
|
|
|
|
|
return (
|
|
|
|
|
<Dialog open={props.open} onOpenChange={props.onOpenChange}>
|
|
|
|
|
<Suspense
|
|
|
|
|
fallback={
|
2025-12-23 21:52:04 +09:00
|
|
|
<DialogContent className="sm:max-w-lg p-0">
|
|
|
|
|
<div className="h-80 flex items-center justify-center">
|
|
|
|
|
<div className="animate-spin h-6 w-6 border-2 border-primary border-t-transparent rounded-full" />
|
2025-12-22 21:54:25 +08:00
|
|
|
</div>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
<SettingsContent {...props} />
|
|
|
|
|
</Suspense>
|
2025-12-05 21:09:34 +08:00
|
|
|
</Dialog>
|
2025-12-06 12:46:40 +09:00
|
|
|
)
|
2025-12-05 21:09:34 +08:00
|
|
|
}
|