Compare commits

...

1 Commits

Author SHA1 Message Date
Biki Kalita
c9d9dd257c fix: move History and Download buttons to Settings dialog for cleaner chat interface 2025-12-27 20:27:48 +05:30
6 changed files with 259 additions and 237 deletions

View File

@@ -1,22 +1,15 @@
"use client" "use client"
import { import { Image as ImageIcon, Loader2, Send, Trash2 } from "lucide-react"
Download,
History,
Image as ImageIcon,
Loader2,
Send,
Trash2,
} from "lucide-react"
import type React from "react" import type React from "react"
import { useCallback, useEffect, useRef, useState } from "react" import { useCallback, useEffect, useRef, useState } from "react"
import { toast } from "sonner" import { toast } from "sonner"
import { ButtonWithTooltip } from "@/components/button-with-tooltip" import { ButtonWithTooltip } from "@/components/button-with-tooltip"
import { ErrorToast } from "@/components/error-toast" import { ErrorToast } from "@/components/error-toast"
import { HistoryDialog } from "@/components/history-dialog"
import { ModelSelector } from "@/components/model-selector" import { ModelSelector } from "@/components/model-selector"
import { ResetWarningModal } from "@/components/reset-warning-modal" import { ResetWarningModal } from "@/components/reset-warning-modal"
import { SaveDialog } from "@/components/save-dialog"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Switch } from "@/components/ui/switch" import { Switch } from "@/components/ui/switch"
import { Textarea } from "@/components/ui/textarea" import { Textarea } from "@/components/ui/textarea"
@@ -25,7 +18,7 @@ import {
TooltipContent, TooltipContent,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip" } from "@/components/ui/tooltip"
import { useDiagram } from "@/contexts/diagram-context"
import { useDictionary } from "@/hooks/use-dictionary" import { useDictionary } from "@/hooks/use-dictionary"
import { formatMessage } from "@/lib/i18n/utils" import { formatMessage } from "@/lib/i18n/utils"
import { isPdfFile, isTextFile } from "@/lib/pdf-utils" import { isPdfFile, isTextFile } from "@/lib/pdf-utils"
@@ -152,8 +145,7 @@ interface ChatInputProps {
File, File,
{ text: string; charCount: number; isExtracting: boolean } { text: string; charCount: number; isExtracting: boolean }
> >
showHistory?: boolean
onToggleHistory?: (show: boolean) => void
sessionId?: string sessionId?: string
error?: Error | null error?: Error | null
minimalStyle?: boolean minimalStyle?: boolean
@@ -175,9 +167,7 @@ export function ChatInput({
files = [], files = [],
onFileChange = () => {}, onFileChange = () => {},
pdfData = new Map(), pdfData = new Map(),
showHistory = false,
onToggleHistory = () => {},
sessionId,
error = null, error = null,
minimalStyle = false, minimalStyle = false,
onMinimalStyleChange = () => {}, onMinimalStyleChange = () => {},
@@ -188,12 +178,7 @@ export function ChatInput({
onConfigureModels = () => {}, onConfigureModels = () => {},
}: ChatInputProps) { }: ChatInputProps) {
const dict = useDictionary() const dict = useDictionary()
const {
diagramHistory,
saveDiagramToFile,
showSaveDialog,
setShowSaveDialog,
} = useDiagram()
const textareaRef = useRef<HTMLTextAreaElement>(null) const textareaRef = useRef<HTMLTextAreaElement>(null)
const fileInputRef = useRef<HTMLInputElement>(null) const fileInputRef = useRef<HTMLInputElement>(null)
const [isDragging, setIsDragging] = useState(false) const [isDragging, setIsDragging] = useState(false)
@@ -386,11 +371,6 @@ export function ChatInput({
onClear={handleClear} onClear={handleClear}
/> />
<HistoryDialog
showHistory={showHistory}
onToggleHistory={onToggleHistory}
/>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
@@ -421,63 +401,29 @@ export function ChatInput({
</div> </div>
<div className="flex items-center gap-1 overflow-hidden justify-end"> <div className="flex items-center gap-1 overflow-hidden justify-end">
<ButtonWithTooltip <div className="flex items-center gap-1 overflow-x-hidden">
type="button" <ButtonWithTooltip
variant="ghost" type="button"
size="sm" variant="ghost"
onClick={() => onToggleHistory(true)} size="sm"
disabled={isDisabled || diagramHistory.length === 0} onClick={triggerFileInput}
tooltipContent={dict.chat.diagramHistory} disabled={isDisabled}
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground" tooltipContent={dict.chat.uploadFile}
> className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
<History className="h-4 w-4" /> >
</ButtonWithTooltip> <ImageIcon className="h-4 w-4" />
</ButtonWithTooltip>
<ButtonWithTooltip
type="button"
variant="ghost"
size="sm"
onClick={() => setShowSaveDialog(true)}
disabled={isDisabled}
tooltipContent={dict.chat.saveDiagram}
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
>
<Download className="h-4 w-4" />
</ButtonWithTooltip>
<SaveDialog
open={showSaveDialog}
onOpenChange={setShowSaveDialog}
onSave={(filename, format) =>
saveDiagramToFile(filename, format, sessionId)
}
defaultFilename={`diagram-${new Date()
.toISOString()
.slice(0, 10)}`}
/>
<ButtonWithTooltip
type="button"
variant="ghost"
size="sm"
onClick={triggerFileInput}
disabled={isDisabled}
tooltipContent={dict.chat.uploadFile}
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
>
<ImageIcon className="h-4 w-4" />
</ButtonWithTooltip>
<input
type="file"
ref={fileInputRef}
className="hidden"
onChange={handleFileChange}
accept="image/*,.pdf,application/pdf,text/*,.md,.markdown,.json,.csv,.xml,.yaml,.yml,.toml"
multiple
disabled={isDisabled}
/>
<input
type="file"
ref={fileInputRef}
className="hidden"
onChange={handleFileChange}
accept="image/*,.pdf,application/pdf,text/*,.md,.markdown,.json,.csv,.xml,.yaml,.yml,.toml"
multiple
disabled={isDisabled}
/>
</div>
<ModelSelector <ModelSelector
models={models} models={models}
selectedModelId={selectedModelId} selectedModelId={selectedModelId}
@@ -486,9 +432,7 @@ export function ChatInput({
disabled={isDisabled} disabled={isDisabled}
showUnvalidatedModels={showUnvalidatedModels} showUnvalidatedModels={showUnvalidatedModels}
/> />
<div className="w-px h-5 bg-border mx-1" /> <div className="w-px h-5 bg-border mx-1" />
<Button <Button
type="submit" type="submit"
disabled={isDisabled || !input.trim()} disabled={isDisabled || !input.trim()}

View File

@@ -154,7 +154,6 @@ export default function ChatPanel({
// File processing using extracted hook // File processing using extracted hook
const { files, pdfData, handleFileChange, setFiles } = useFileProcessor() const { files, pdfData, handleFileChange, setFiles } = useFileProcessor()
const [showHistory, setShowHistory] = useState(false)
const [showSettingsDialog, setShowSettingsDialog] = useState(false) const [showSettingsDialog, setShowSettingsDialog] = useState(false)
const [showModelConfigDialog, setShowModelConfigDialog] = useState(false) const [showModelConfigDialog, setShowModelConfigDialog] = useState(false)
@@ -1066,8 +1065,6 @@ export default function ChatPanel({
files={files} files={files}
onFileChange={handleFileChange} onFileChange={handleFileChange}
pdfData={pdfData} pdfData={pdfData}
showHistory={showHistory}
onToggleHistory={setShowHistory}
sessionId={sessionId} sessionId={sessionId}
error={error} error={error}
minimalStyle={minimalStyle} minimalStyle={minimalStyle}
@@ -1088,6 +1085,7 @@ export default function ChatPanel({
onToggleDrawioUi={onToggleDrawioUi} onToggleDrawioUi={onToggleDrawioUi}
darkMode={darkMode} darkMode={darkMode}
onToggleDarkMode={onToggleDarkMode} onToggleDarkMode={onToggleDarkMode}
sessionId={sessionId}
/> />
<ModelConfigDialog <ModelConfigDialog

View File

@@ -1,8 +1,10 @@
"use client" "use client"
import { Moon, Sun } from "lucide-react" import { Download, History, Moon, Sun } from "lucide-react"
import { usePathname, useRouter, useSearchParams } from "next/navigation" import { usePathname, useRouter, useSearchParams } from "next/navigation"
import { Suspense, useEffect, useState } from "react" import { Suspense, useEffect, useState } from "react"
import { HistoryDialog } from "@/components/history-dialog"
import { type ExportFormat, SaveDialog } from "@/components/save-dialog"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { import {
Dialog, Dialog,
@@ -21,6 +23,7 @@ import {
SelectValue, SelectValue,
} from "@/components/ui/select" } from "@/components/ui/select"
import { Switch } from "@/components/ui/switch" import { Switch } from "@/components/ui/switch"
import { useDiagram } from "@/contexts/diagram-context"
import { useDictionary } from "@/hooks/use-dictionary" import { useDictionary } from "@/hooks/use-dictionary"
import { getApiEndpoint } from "@/lib/base-path" import { getApiEndpoint } from "@/lib/base-path"
import { i18n, type Locale } from "@/lib/i18n/config" import { i18n, type Locale } from "@/lib/i18n/config"
@@ -65,6 +68,7 @@ interface SettingsDialogProps {
onToggleDrawioUi: () => void onToggleDrawioUi: () => void
darkMode: boolean darkMode: boolean
onToggleDarkMode: () => void onToggleDarkMode: () => void
sessionId?: string
} }
export const STORAGE_ACCESS_CODE_KEY = "next-ai-draw-io-access-code" export const STORAGE_ACCESS_CODE_KEY = "next-ai-draw-io-access-code"
@@ -86,6 +90,7 @@ function SettingsContent({
onToggleDrawioUi, onToggleDrawioUi,
darkMode, darkMode,
onToggleDarkMode, onToggleDarkMode,
sessionId,
}: SettingsDialogProps) { }: SettingsDialogProps) {
const dict = useDictionary() const dict = useDictionary()
const router = useRouter() const router = useRouter()
@@ -95,11 +100,21 @@ function SettingsContent({
const [closeProtection, setCloseProtection] = useState(true) const [closeProtection, setCloseProtection] = useState(true)
const [isVerifying, setIsVerifying] = useState(false) const [isVerifying, setIsVerifying] = useState(false)
const [error, setError] = useState("") const [error, setError] = useState("")
const [showHistory, setShowHistory] = useState(false)
const [showSaveDialog, setShowSaveDialog] = useState(false)
const [accessCodeRequired, setAccessCodeRequired] = useState( const [accessCodeRequired, setAccessCodeRequired] = useState(
() => getStoredAccessCodeRequired() ?? false, () => getStoredAccessCodeRequired() ?? false,
) )
const [currentLang, setCurrentLang] = useState("en") const [currentLang, setCurrentLang] = useState("en")
// Get diagram context
const { diagramHistory, saveDiagramToFile } = useDiagram()
// Handler for saving diagram (RENAMED to avoid conflict)
const handleDiagramSave = (filename: string, format: ExportFormat) => {
saveDiagramToFile(filename, format, sessionId)
}
useEffect(() => { useEffect(() => {
// Only fetch if not cached in localStorage // Only fetch if not cached in localStorage
if (getStoredAccessCodeRequired() !== null) return if (getStoredAccessCodeRequired() !== null) return
@@ -206,158 +221,211 @@ function SettingsContent({
} }
return ( return (
<DialogContent className="sm:max-w-lg p-0 gap-0"> <>
{/* Header */} <DialogContent className="sm:max-w-lg p-0 gap-0">
<DialogHeader className="px-6 pt-6 pb-4"> {/* Header */}
<DialogTitle>{dict.settings.title}</DialogTitle> <DialogHeader className="px-6 pt-6 pb-4">
<DialogDescription className="mt-1"> <DialogTitle>{dict.settings.title}</DialogTitle>
{dict.settings.description} <DialogDescription className="mt-1">
</DialogDescription> {dict.settings.description}
</DialogHeader> </DialogDescription>
</DialogHeader>
{/* Content */} {/* Content */}
<div className="px-6 pb-6"> <div className="px-6 pb-6">
<div className="divide-y divide-border-subtle"> <div className="divide-y divide-border-subtle">
{/* Access Code (conditional) */} {/* Access Code (conditional) */}
{accessCodeRequired && ( {accessCodeRequired && (
<div className="py-4 first:pt-0 space-y-3"> <div className="py-4 first:pt-0 space-y-3">
<div className="space-y-0.5"> <div className="space-y-0.5">
<Label <Label
htmlFor="access-code" htmlFor="access-code"
className="text-sm font-medium" className="text-sm font-medium"
> >
{dict.settings.accessCode} {dict.settings.accessCode}
</Label> </Label>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{dict.settings.accessCodeDescription} {dict.settings.accessCodeDescription}
</p> </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>
)}
</div> </div>
<div className="flex gap-2"> )}
<Input
id="access-code" {/* Language */}
type="password" <SettingItem
value={accessCode} label={dict.settings.language}
onChange={(e) => description={dict.settings.languageDescription}
setAccessCode(e.target.value) >
} <Select
onKeyDown={handleKeyDown} value={currentLang}
placeholder={ onValueChange={changeLanguage}
dict.settings.accessCodePlaceholder >
} <SelectTrigger
autoComplete="off" id="language-select"
className="h-9" className="w-[120px] h-9 rounded-xl"
/>
<Button
onClick={handleSave}
disabled={isVerifying || !accessCode.trim()}
className="h-9 px-4 rounded-xl"
> >
{isVerifying ? "..." : dict.common.save} <SelectValue />
</SelectTrigger>
<SelectContent>
{i18n.locales.map((locale) => (
<SelectItem key={locale} value={locale}>
{LANGUAGE_LABELS[locale]}
</SelectItem>
))}
</SelectContent>
</Select>
</SettingItem>
{/* Theme */}
<SettingItem
label={dict.settings.theme}
description={dict.settings.themeDescription}
>
<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>
{/* Draw.io Style */}
<SettingItem
label={dict.settings.drawioStyle}
description={`${dict.settings.drawioStyleDescription} ${
drawioUi === "min"
? dict.settings.minimal
: dict.settings.sketch
}`}
>
<Button
id="drawio-ui"
variant="outline"
onClick={onToggleDrawioUi}
className="h-9 w-[120px] rounded-xl border-border-subtle hover:bg-interactive-hover font-normal"
>
{dict.settings.switchTo}{" "}
{drawioUi === "min"
? dict.settings.sketch
: dict.settings.minimal}
</Button>
</SettingItem>
{/* Close Protection */}
<SettingItem
label={dict.settings.closeProtection}
description={
dict.settings.closeProtectionDescription
}
>
<Switch
id="close-protection"
checked={closeProtection}
onCheckedChange={(checked) => {
setCloseProtection(checked)
localStorage.setItem(
STORAGE_CLOSE_PROTECTION_KEY,
checked.toString(),
)
onCloseProtectionChange?.(checked)
}}
/>
</SettingItem>
{/* Diagram Actions */}
<SettingItem
label={dict.settings.diagramActions}
description={
dict.settings.diagramActionsDescription
}
>
<div className="flex gap-2">
<Button
id="history-button"
variant="outline"
size="sm"
onClick={() => setShowHistory(true)}
disabled={diagramHistory.length === 0}
className="h-9 rounded-xl border-border-subtle hover:bg-interactive-hover"
>
<History className="h-4 w-4 mr-1.5" />
{dict.settings.history}
</Button>
<Button
id="download-button"
variant="outline"
size="sm"
onClick={() => setShowSaveDialog(true)}
className="h-9 rounded-xl border-border-subtle hover:bg-interactive-hover"
>
<Download className="h-4 w-4 mr-1.5" />
{dict.settings.download}
</Button> </Button>
</div> </div>
{error && ( </SettingItem>
<p className="text-xs text-destructive"> </div>
{error}
</p>
)}
</div>
)}
{/* 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>
{/* Theme */}
<SettingItem
label={dict.settings.theme}
description={dict.settings.themeDescription}
>
<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>
{/* Draw.io Style */}
<SettingItem
label={dict.settings.drawioStyle}
description={`${dict.settings.drawioStyleDescription} ${
drawioUi === "min"
? dict.settings.minimal
: dict.settings.sketch
}`}
>
<Button
id="drawio-ui"
variant="outline"
onClick={onToggleDrawioUi}
className="h-9 w-[120px] rounded-xl border-border-subtle hover:bg-interactive-hover font-normal"
>
{dict.settings.switchTo}{" "}
{drawioUi === "min"
? dict.settings.sketch
: dict.settings.minimal}
</Button>
</SettingItem>
{/* Close Protection */}
<SettingItem
label={dict.settings.closeProtection}
description={dict.settings.closeProtectionDescription}
>
<Switch
id="close-protection"
checked={closeProtection}
onCheckedChange={(checked) => {
setCloseProtection(checked)
localStorage.setItem(
STORAGE_CLOSE_PROTECTION_KEY,
checked.toString(),
)
onCloseProtectionChange?.(checked)
}}
/>
</SettingItem>
</div> </div>
</div>
{/* Footer */} {/* Footer */}
<div className="px-6 py-4 border-t border-border-subtle bg-surface-1/50 rounded-b-2xl"> <div className="px-6 py-4 border-t border-border-subtle bg-surface-1/50 rounded-b-2xl">
<p className="text-xs text-muted-foreground text-center"> <p className="text-xs text-muted-foreground text-center">
Version {process.env.APP_VERSION} Version {process.env.APP_VERSION}
</p> </p>
</div> </div>
</DialogContent> </DialogContent>
;(
<HistoryDialog
showHistory={showHistory}
onToggleHistory={setShowHistory}
/>
)
<SaveDialog
open={showSaveDialog}
onOpenChange={setShowSaveDialog}
onSave={handleDiagramSave}
defaultFilename={`diagram-${new Date()
.toISOString()
.slice(0, 10)}
`}
/>
</>
) )
} }

View File

@@ -98,7 +98,11 @@
"minimal": "Minimal", "minimal": "Minimal",
"sketch": "Sketch", "sketch": "Sketch",
"closeProtection": "Close Protection", "closeProtection": "Close Protection",
"closeProtectionDescription": "Show confirmation when leaving the page." "closeProtectionDescription": "Show confirmation when leaving the page.",
"diagramActions": "Diagram Actions",
"diagramActionsDescription": "Manage diagram history and exports",
"history": "History",
"download": "Download"
}, },
"save": { "save": {
"title": "Save Diagram", "title": "Save Diagram",

View File

@@ -98,7 +98,11 @@
"minimal": "ミニマル", "minimal": "ミニマル",
"sketch": "スケッチ", "sketch": "スケッチ",
"closeProtection": "ページ離脱確認", "closeProtection": "ページ離脱確認",
"closeProtectionDescription": "ページを離れる際に確認を表示します。" "closeProtectionDescription": "ページを離れる際に確認を表示します。",
"diagramActions": "ダイアグラム操作",
"diagramActionsDescription": "ダイアグラムの履歴とエクスポートを管理",
"history": "履歴",
"download": "ダウンロード"
}, },
"save": { "save": {
"title": "ダイアグラムを保存", "title": "ダイアグラムを保存",

View File

@@ -98,7 +98,11 @@
"minimal": "简约", "minimal": "简约",
"sketch": "草图", "sketch": "草图",
"closeProtection": "关闭确认", "closeProtection": "关闭确认",
"closeProtectionDescription": "离开页面时显示确认。" "closeProtectionDescription": "离开页面时显示确认。",
"diagramActions": "图表操作",
"diagramActionsDescription": "管理图表历史记录和导出",
"history": "历史记录",
"download": "下载"
}, },
"save": { "save": {
"title": "保存图表", "title": "保存图表",