Merge pull request #6381 from menloresearch/enhancement/dialog-modal-responsive
enhancement: responsive dialog modals
This commit is contained in:
commit
cd85ae062e
@ -1,4 +1,4 @@
|
|||||||
import { Link, useNavigate, useRouterState } from '@tanstack/react-router'
|
import { Link, useRouterState } from '@tanstack/react-router'
|
||||||
import { useLeftPanel } from '@/hooks/useLeftPanel'
|
import { useLeftPanel } from '@/hooks/useLeftPanel'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import {
|
import {
|
||||||
@ -6,7 +6,6 @@ import {
|
|||||||
IconDots,
|
IconDots,
|
||||||
IconCirclePlusFilled,
|
IconCirclePlusFilled,
|
||||||
IconSettingsFilled,
|
IconSettingsFilled,
|
||||||
IconTrash,
|
|
||||||
IconStar,
|
IconStar,
|
||||||
IconMessageFilled,
|
IconMessageFilled,
|
||||||
IconAppsFilled,
|
IconAppsFilled,
|
||||||
@ -27,17 +26,6 @@ import { useThreads } from '@/hooks/useThreads'
|
|||||||
|
|
||||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||||
import { useMemo, useState, useEffect, useRef } from 'react'
|
import { useMemo, useState, useEffect, useRef } from 'react'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogClose,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { DownloadManagement } from '@/containers/DownloadManegement'
|
import { DownloadManagement } from '@/containers/DownloadManegement'
|
||||||
import { useSmallScreen } from '@/hooks/useMediaQuery'
|
import { useSmallScreen } from '@/hooks/useMediaQuery'
|
||||||
@ -45,6 +33,7 @@ import { useClickOutside } from '@/hooks/useClickOutside'
|
|||||||
import { useDownloadStore } from '@/hooks/useDownloadStore'
|
import { useDownloadStore } from '@/hooks/useDownloadStore'
|
||||||
import { PlatformFeatures } from '@/lib/platform/const'
|
import { PlatformFeatures } from '@/lib/platform/const'
|
||||||
import { PlatformFeature } from '@/lib/platform/types'
|
import { PlatformFeature } from '@/lib/platform/types'
|
||||||
|
import { DeleteAllThreadsDialog } from '@/containers/dialogs'
|
||||||
|
|
||||||
const mainMenus = [
|
const mainMenus = [
|
||||||
{
|
{
|
||||||
@ -76,7 +65,6 @@ const mainMenus = [
|
|||||||
const LeftPanel = () => {
|
const LeftPanel = () => {
|
||||||
const { open, setLeftPanel } = useLeftPanel()
|
const { open, setLeftPanel } = useLeftPanel()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const navigate = useNavigate()
|
|
||||||
const [searchTerm, setSearchTerm] = useState('')
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
|
|
||||||
const isSmallScreen = useSmallScreen()
|
const isSmallScreen = useSmallScreen()
|
||||||
@ -362,80 +350,25 @@ const LeftPanel = () => {
|
|||||||
{t('common:recents')}
|
{t('common:recents')}
|
||||||
</span>
|
</span>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Dialog>
|
<DropdownMenu>
|
||||||
<DropdownMenu>
|
<DropdownMenuTrigger asChild>
|
||||||
<DropdownMenuTrigger asChild>
|
<button
|
||||||
<button
|
className="size-6 flex cursor-pointer items-center justify-center rounded hover:bg-left-panel-fg/10 transition-all duration-200 ease-in-out data-[state=open]:bg-left-panel-fg/10"
|
||||||
className="size-6 flex cursor-pointer items-center justify-center rounded hover:bg-left-panel-fg/10 transition-all duration-200 ease-in-out data-[state=open]:bg-left-panel-fg/10"
|
onClick={(e) => {
|
||||||
onClick={(e) => {
|
e.preventDefault()
|
||||||
e.preventDefault()
|
e.stopPropagation()
|
||||||
e.stopPropagation()
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<IconDots
|
||||||
<IconDots
|
size={18}
|
||||||
size={18}
|
className="text-left-panel-fg/60"
|
||||||
className="text-left-panel-fg/60"
|
/>
|
||||||
/>
|
</button>
|
||||||
</button>
|
</DropdownMenuTrigger>
|
||||||
</DropdownMenuTrigger>
|
<DropdownMenuContent side="bottom" align="end">
|
||||||
<DropdownMenuContent side="bottom" align="end">
|
<DeleteAllThreadsDialog onDeleteAll={deleteAllThreads} />
|
||||||
<DialogTrigger asChild>
|
</DropdownMenuContent>
|
||||||
<DropdownMenuItem
|
</DropdownMenu>
|
||||||
onSelect={(e) => e.preventDefault()}
|
|
||||||
>
|
|
||||||
<IconTrash size={16} />
|
|
||||||
<span>{t('common:deleteAll')}</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>
|
|
||||||
{t('common:dialogs.deleteAllThreads.title')}
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
{t(
|
|
||||||
'common:dialogs.deleteAllThreads.description'
|
|
||||||
)}
|
|
||||||
</DialogDescription>
|
|
||||||
<DialogFooter className="mt-2">
|
|
||||||
<DialogClose asChild>
|
|
||||||
<Button
|
|
||||||
variant="link"
|
|
||||||
size="sm"
|
|
||||||
className="hover:no-underline"
|
|
||||||
>
|
|
||||||
{t('common:cancel')}
|
|
||||||
</Button>
|
|
||||||
</DialogClose>
|
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => {
|
|
||||||
deleteAllThreads()
|
|
||||||
toast.success(
|
|
||||||
t(
|
|
||||||
'common:toast.deleteAllThreads.title'
|
|
||||||
),
|
|
||||||
{
|
|
||||||
id: 'delete-all-thread',
|
|
||||||
description: t(
|
|
||||||
'common:toast.deleteAllThreads.description'
|
|
||||||
),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
setTimeout(() => {
|
|
||||||
navigate({ to: route.home })
|
|
||||||
}, 0)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('common:deleteAll')}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogHeader>
|
|
||||||
</DialogContent>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -2,14 +2,7 @@
|
|||||||
import { ThreadMessage } from '@janhq/core'
|
import { ThreadMessage } from '@janhq/core'
|
||||||
import { RenderMarkdown } from './RenderMarkdown'
|
import { RenderMarkdown } from './RenderMarkdown'
|
||||||
import React, { Fragment, memo, useCallback, useMemo, useState } from 'react'
|
import React, { Fragment, memo, useCallback, useMemo, useState } from 'react'
|
||||||
import {
|
import { IconCopy, IconCopyCheck, IconRefresh } from '@tabler/icons-react'
|
||||||
IconCopy,
|
|
||||||
IconCopyCheck,
|
|
||||||
IconRefresh,
|
|
||||||
IconTrash,
|
|
||||||
IconPencil,
|
|
||||||
IconInfoCircle,
|
|
||||||
} from '@tabler/icons-react'
|
|
||||||
import { useAppState } from '@/hooks/useAppState'
|
import { useAppState } from '@/hooks/useAppState'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { useMessages } from '@/hooks/useMessages'
|
import { useMessages } from '@/hooks/useMessages'
|
||||||
@ -17,16 +10,10 @@ import ThinkingBlock from '@/containers/ThinkingBlock'
|
|||||||
import ToolCallBlock from '@/containers/ToolCallBlock'
|
import ToolCallBlock from '@/containers/ToolCallBlock'
|
||||||
import { useChat } from '@/hooks/useChat'
|
import { useChat } from '@/hooks/useChat'
|
||||||
import {
|
import {
|
||||||
Dialog,
|
EditMessageDialog,
|
||||||
DialogClose,
|
MessageMetadataDialog,
|
||||||
DialogContent,
|
DeleteMessageDialog,
|
||||||
DialogFooter,
|
} from '@/containers/dialogs'
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
@ -37,8 +24,6 @@ import { AvatarEmoji } from '@/containers/AvatarEmoji'
|
|||||||
|
|
||||||
import TokenSpeedIndicator from '@/containers/TokenSpeedIndicator'
|
import TokenSpeedIndicator from '@/containers/TokenSpeedIndicator'
|
||||||
|
|
||||||
import CodeEditor from '@uiw/react-textarea-code-editor'
|
|
||||||
import '@uiw/react-textarea-code-editor/dist.css'
|
|
||||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||||
import { useModelProvider } from '@/hooks/useModelProvider'
|
import { useModelProvider } from '@/hooks/useModelProvider'
|
||||||
|
|
||||||
@ -76,69 +61,6 @@ const CopyButton = ({ text }: { text: string }) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const EditDialog = ({
|
|
||||||
message,
|
|
||||||
setMessage,
|
|
||||||
}: {
|
|
||||||
message: string
|
|
||||||
setMessage: (message: string) => void
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const [draft, setDraft] = useState(message)
|
|
||||||
|
|
||||||
const handleSave = () => {
|
|
||||||
if (draft !== message) {
|
|
||||||
setMessage(draft)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog>
|
|
||||||
<DialogTrigger>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div className="flex outline-0 items-center gap-1 hover:text-accent transition-colors cursor-pointer group relative">
|
|
||||||
<IconPencil size={16} />
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>{t('edit')}</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="w-3/4">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>{t('common:dialogs.editMessage.title')}</DialogTitle>
|
|
||||||
<Textarea
|
|
||||||
value={draft}
|
|
||||||
onChange={(e) => setDraft(e.target.value)}
|
|
||||||
className="mt-2 resize-none w-full"
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
// Prevent key from being captured by parent components
|
|
||||||
e.stopPropagation()
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<DialogFooter className="mt-2 flex items-center">
|
|
||||||
<DialogClose asChild>
|
|
||||||
<Button variant="link" size="sm" className="hover:no-underline">
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</DialogClose>
|
|
||||||
<DialogClose asChild>
|
|
||||||
<Button
|
|
||||||
disabled={draft === message || !draft}
|
|
||||||
onClick={handleSave}
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</DialogClose>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogHeader>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use memo to prevent unnecessary re-renders, but allow re-renders when props change
|
// Use memo to prevent unnecessary re-renders, but allow re-renders when props change
|
||||||
export const ThreadContent = memo(
|
export const ThreadContent = memo(
|
||||||
(
|
(
|
||||||
@ -349,32 +271,20 @@ export const ThreadContent = memo(
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center justify-end gap-2 text-main-view-fg/60 text-xs mt-2">
|
<div className="flex items-center justify-end gap-2 text-main-view-fg/60 text-xs mt-2">
|
||||||
<EditDialog
|
<EditMessageDialog
|
||||||
message={
|
message={
|
||||||
item.content?.find((c) => c.type === 'text')?.text?.value ||
|
item.content?.find((c) => c.type === 'text')?.text?.value ||
|
||||||
''
|
''
|
||||||
}
|
}
|
||||||
setMessage={(message) => {
|
onSave={(message) => {
|
||||||
if (item.updateMessage) {
|
if (item.updateMessage) {
|
||||||
item.updateMessage(item, message)
|
item.updateMessage(item, message)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Tooltip>
|
<DeleteMessageDialog
|
||||||
<TooltipTrigger asChild>
|
onDelete={() => deleteMessage(item.thread_id, item.id)}
|
||||||
<button
|
/>
|
||||||
className="flex items-center gap-1 hover:text-accent transition-colors cursor-pointer group relative"
|
|
||||||
onClick={() => {
|
|
||||||
deleteMessage(item.thread_id, item.id)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconTrash size={16} />
|
|
||||||
</button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>{t('delete')}</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -456,68 +366,15 @@ export const ThreadContent = memo(
|
|||||||
'hidden'
|
'hidden'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<EditDialog
|
<EditMessageDialog
|
||||||
message={item.content?.[0]?.text.value}
|
message={item.content?.[0]?.text.value || ''}
|
||||||
setMessage={(message) =>
|
onSave={(message) =>
|
||||||
item.updateMessage && item.updateMessage(item, message)
|
item.updateMessage && item.updateMessage(item, message)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<CopyButton text={item.content?.[0]?.text.value || ''} />
|
<CopyButton text={item.content?.[0]?.text.value || ''} />
|
||||||
<Tooltip>
|
<DeleteMessageDialog onDelete={removeMessage} />
|
||||||
<TooltipTrigger asChild>
|
<MessageMetadataDialog metadata={item.metadata} />
|
||||||
<button
|
|
||||||
className="flex items-center gap-1 hover:text-accent transition-colors cursor-pointer group relative"
|
|
||||||
onClick={() => {
|
|
||||||
removeMessage()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconTrash size={16} />
|
|
||||||
</button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>{t('delete')}</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
<Dialog>
|
|
||||||
<DialogTrigger>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div className="outline-0 focus:outline-0 flex items-center gap-1 hover:text-accent transition-colors cursor-pointer group relative">
|
|
||||||
<IconInfoCircle size={16} />
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>{t('metadata')}</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>
|
|
||||||
{t('common:dialogs.messageMetadata.title')}
|
|
||||||
</DialogTitle>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="border border-main-view-fg/10 rounded-md overflow-hidden">
|
|
||||||
<CodeEditor
|
|
||||||
value={JSON.stringify(
|
|
||||||
item.metadata || {},
|
|
||||||
null,
|
|
||||||
2
|
|
||||||
)}
|
|
||||||
language="json"
|
|
||||||
readOnly
|
|
||||||
style={{
|
|
||||||
fontFamily: 'ui-monospace',
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
height: '100%',
|
|
||||||
}}
|
|
||||||
className="w-full h-full !text-sm"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DialogHeader>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
{item.isLastMessage && selectedModel && (
|
{item.isLastMessage && selectedModel && (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
|
|||||||
@ -15,14 +15,11 @@ import { CSS } from '@dnd-kit/utilities'
|
|||||||
import {
|
import {
|
||||||
IconDots,
|
IconDots,
|
||||||
IconStarFilled,
|
IconStarFilled,
|
||||||
IconTrash,
|
|
||||||
IconEdit,
|
|
||||||
IconStar,
|
IconStar,
|
||||||
} from '@tabler/icons-react'
|
} from '@tabler/icons-react'
|
||||||
import { useThreads } from '@/hooks/useThreads'
|
import { useThreads } from '@/hooks/useThreads'
|
||||||
import { useLeftPanel } from '@/hooks/useLeftPanel'
|
import { useLeftPanel } from '@/hooks/useLeftPanel'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { route } from '@/constants/routes'
|
|
||||||
import { useSmallScreen } from '@/hooks/useMediaQuery'
|
import { useSmallScreen } from '@/hooks/useMediaQuery'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -33,19 +30,10 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu'
|
} from '@/components/ui/dropdown-menu'
|
||||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||||
import { DialogClose, DialogFooter, DialogHeader } from '@/components/ui/dialog'
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogTrigger,
|
|
||||||
DialogContent,
|
|
||||||
DialogTitle,
|
|
||||||
DialogDescription,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { memo, useMemo, useState } from 'react'
|
import { memo, useMemo, useState } from 'react'
|
||||||
import { useNavigate, useMatches } from '@tanstack/react-router'
|
import { useNavigate, useMatches } from '@tanstack/react-router'
|
||||||
import { toast } from 'sonner'
|
import { RenameThreadDialog, DeleteThreadDialog } from '@/containers/dialogs'
|
||||||
import { Input } from '@/components/ui/input'
|
import { route } from '@/constants/routes'
|
||||||
|
|
||||||
const SortableItem = memo(({ thread }: { thread: Thread }) => {
|
const SortableItem = memo(({ thread }: { thread: Thread }) => {
|
||||||
const {
|
const {
|
||||||
@ -94,9 +82,6 @@ const SortableItem = memo(({ thread }: { thread: Thread }) => {
|
|||||||
return (thread.title || '').replace(/<span[^>]*>|<\/span>/g, '')
|
return (thread.title || '').replace(/<span[^>]*>|<\/span>/g, '')
|
||||||
}, [thread.title])
|
}, [thread.title])
|
||||||
|
|
||||||
const [title, setTitle] = useState(
|
|
||||||
plainTitleForRename || t('common:newThread')
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -156,116 +141,19 @@ const SortableItem = memo(({ thread }: { thread: Thread }) => {
|
|||||||
<span>{t('common:star')}</span>
|
<span>{t('common:star')}</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
<Dialog
|
<RenameThreadDialog
|
||||||
onOpenChange={(open) => {
|
thread={thread}
|
||||||
if (open) {
|
plainTitleForRename={plainTitleForRename}
|
||||||
setTitle(plainTitleForRename || t('common:newThread'))
|
onRename={renameThread}
|
||||||
} else {
|
onDropdownClose={() => setOpenDropdown(false)}
|
||||||
setOpenDropdown(false)
|
/>
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
|
||||||
<IconEdit />
|
|
||||||
<span>{t('common:rename')}</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>{t('common:threadTitle')}</DialogTitle>
|
|
||||||
<Input
|
|
||||||
value={title}
|
|
||||||
onChange={(e) => {
|
|
||||||
setTitle(e.target.value)
|
|
||||||
}}
|
|
||||||
className="mt-2"
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
// Prevent key from being captured by parent components
|
|
||||||
e.stopPropagation()
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<DialogFooter className="mt-2 flex items-center">
|
|
||||||
<DialogClose asChild>
|
|
||||||
<Button
|
|
||||||
variant="link"
|
|
||||||
size="sm"
|
|
||||||
className="hover:no-underline"
|
|
||||||
>
|
|
||||||
{t('common:cancel')}
|
|
||||||
</Button>
|
|
||||||
</DialogClose>
|
|
||||||
<Button
|
|
||||||
disabled={!title}
|
|
||||||
onClick={() => {
|
|
||||||
renameThread(thread.id, title)
|
|
||||||
setOpenDropdown(false)
|
|
||||||
toast.success(t('common:toast.renameThread.title'), {
|
|
||||||
id: 'rename-thread',
|
|
||||||
description: t(
|
|
||||||
'common:toast.renameThread.description',
|
|
||||||
{ title }
|
|
||||||
),
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('common:rename')}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogHeader>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<Dialog
|
<DeleteThreadDialog
|
||||||
onOpenChange={(open) => {
|
thread={thread}
|
||||||
if (!open) setOpenDropdown(false)
|
onDelete={deleteThread}
|
||||||
}}
|
onDropdownClose={() => setOpenDropdown(false)}
|
||||||
>
|
/>
|
||||||
<DialogTrigger asChild>
|
|
||||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
|
||||||
<IconTrash />
|
|
||||||
<span>{t('common:delete')}</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>{t('common:deleteThread')}</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
{t('common:dialogs.deleteThread.description')}
|
|
||||||
</DialogDescription>
|
|
||||||
<DialogFooter className="mt-2 flex items-center">
|
|
||||||
<DialogClose asChild>
|
|
||||||
<Button
|
|
||||||
variant="link"
|
|
||||||
size="sm"
|
|
||||||
className="hover:no-underline"
|
|
||||||
>
|
|
||||||
{t('common:cancel')}
|
|
||||||
</Button>
|
|
||||||
</DialogClose>
|
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
onClick={() => {
|
|
||||||
deleteThread(thread.id)
|
|
||||||
setOpenDropdown(false)
|
|
||||||
toast.success(t('common:toast.deleteThread.title'), {
|
|
||||||
id: 'delete-thread',
|
|
||||||
description: t(
|
|
||||||
'common:toast.deleteThread.description'
|
|
||||||
),
|
|
||||||
})
|
|
||||||
setTimeout(() => {
|
|
||||||
navigate({ to: route.home })
|
|
||||||
}, 0)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('common:delete')}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogHeader>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,16 +1,11 @@
|
|||||||
import { ChevronDown, ChevronUp, Loader } from 'lucide-react'
|
import { ChevronDown, ChevronUp, Loader } from 'lucide-react'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { create } from 'zustand'
|
import { create } from 'zustand'
|
||||||
import { RenderMarkdown } from './RenderMarkdown'
|
import { RenderMarkdown } from '@/containers/RenderMarkdown'
|
||||||
import { useMemo, useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { twMerge } from 'tailwind-merge'
|
import { twMerge } from 'tailwind-merge'
|
||||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||||
|
import ImageModal from '@/containers/dialogs/ImageModal'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
result: string
|
result: string
|
||||||
@ -108,14 +103,6 @@ const ContentItemRenderer = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (item.type === 'text' && item.text) {
|
|
||||||
// return (
|
|
||||||
// <div key={index} className="mt-3">
|
|
||||||
// <RenderMarkdown content={item.text} />
|
|
||||||
// </div>
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
|
|
||||||
// For any other types, render as JSON
|
// For any other types, render as JSON
|
||||||
return (
|
return (
|
||||||
<div key={index} className="mt-3">
|
<div key={index} className="mt-3">
|
||||||
@ -243,29 +230,7 @@ const ToolCallBlock = ({ id, name, result, loading, args }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Image Modal */}
|
<ImageModal image={modalImage} onClose={closeModal} />
|
||||||
<Dialog
|
|
||||||
open={!!modalImage}
|
|
||||||
onOpenChange={(open) => !open && closeModal()}
|
|
||||||
>
|
|
||||||
<DialogContent className="max-w-4xl max-h-[90vh] p-0">
|
|
||||||
<DialogHeader className="p-6 pb-2">
|
|
||||||
<DialogTitle>{modalImage?.alt || t('common:image')}</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="flex justify-center items-center p-6 pt-2">
|
|
||||||
{modalImage && (
|
|
||||||
<img
|
|
||||||
src={modalImage.url}
|
|
||||||
alt={modalImage.alt}
|
|
||||||
className="max-w-full max-h-[70vh] object-contain rounded-md"
|
|
||||||
onError={(e) => {
|
|
||||||
e.currentTarget.style.display = 'none'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -45,7 +45,7 @@ export function TrustedHostsInput({
|
|||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
placeholder={t('common:enterTrustedHosts')}
|
placeholder={t('common:enterTrustedHosts')}
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-24 h-8 text-sm',
|
'h-8 text-sm',
|
||||||
isServerRunning && 'opacity-50 pointer-events-none'
|
isServerRunning && 'opacity-50 pointer-events-none'
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
105
web-app/src/containers/dialogs/AddProviderDialog.tsx
Normal file
105
web-app/src/containers/dialogs/AddProviderDialog.tsx
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import { useState, useRef } from 'react'
|
||||||
|
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogTrigger,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
DialogClose,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
} from '@/components/ui/dialog'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
|
||||||
|
interface AddProviderDialogProps {
|
||||||
|
onCreateProvider: (name: string) => void
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AddProviderDialog({
|
||||||
|
onCreateProvider,
|
||||||
|
children,
|
||||||
|
}: AddProviderDialogProps) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [name, setName] = useState('')
|
||||||
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
|
const createButtonRef = useRef<HTMLButtonElement>(null)
|
||||||
|
|
||||||
|
const handleCreate = () => {
|
||||||
|
if (name.trim()) {
|
||||||
|
onCreateProvider(name.trim())
|
||||||
|
setName('')
|
||||||
|
setIsOpen(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setName('')
|
||||||
|
setIsOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter' && name.trim()) {
|
||||||
|
e.preventDefault()
|
||||||
|
handleCreate()
|
||||||
|
}
|
||||||
|
// Prevent key from being captured by parent components
|
||||||
|
e.stopPropagation()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOpenChange = (open: boolean) => {
|
||||||
|
setIsOpen(open)
|
||||||
|
if (!open) {
|
||||||
|
setName('')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
|
||||||
|
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||||
|
<DialogContent
|
||||||
|
className="sm:max-w-[425px] max-w-[90vw]"
|
||||||
|
onOpenAutoFocus={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
createButtonRef.current?.focus()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{t('provider:addOpenAIProvider')}</DialogTitle>
|
||||||
|
<Input
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
className="mt-2"
|
||||||
|
placeholder={t('provider:enterNameForProvider')}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
/>
|
||||||
|
<DialogFooter className="mt-4 flex flex-col-reverse sm:flex-row sm:justify-end gap-2">
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
size="sm"
|
||||||
|
className="hover:no-underline w-full sm:w-auto"
|
||||||
|
onClick={handleCancel}
|
||||||
|
>
|
||||||
|
{t('common:cancel')}
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button
|
||||||
|
ref={createButtonRef}
|
||||||
|
disabled={!name.trim()}
|
||||||
|
onClick={handleCreate}
|
||||||
|
className="w-full sm:w-auto"
|
||||||
|
size="sm"
|
||||||
|
aria-label={t('common:create')}
|
||||||
|
>
|
||||||
|
{t('common:create')}
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogHeader>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -33,7 +33,7 @@ export default function ChangeDataFolderLocation({
|
|||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||||
<DialogContent className="max-w-2xl">
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle className="flex items-center gap-2">
|
||||||
<IconFolder size={20} />
|
<IconFolder size={20} />
|
||||||
|
|||||||
103
web-app/src/containers/dialogs/DeleteAllThreadsDialog.tsx
Normal file
103
web-app/src/containers/dialogs/DeleteAllThreadsDialog.tsx
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import { useState, useRef } from 'react'
|
||||||
|
import { useNavigate } from '@tanstack/react-router'
|
||||||
|
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogTrigger,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
DialogDescription,
|
||||||
|
DialogClose,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
} from '@/components/ui/dialog'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { IconTrash } from '@tabler/icons-react'
|
||||||
|
import { DropdownMenuItem } from '@/components/ui/dropdown-menu'
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
import { route } from '@/constants/routes'
|
||||||
|
|
||||||
|
interface DeleteAllThreadsDialogProps {
|
||||||
|
onDeleteAll: () => void
|
||||||
|
onDropdownClose?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeleteAllThreadsDialog({
|
||||||
|
onDeleteAll,
|
||||||
|
onDropdownClose,
|
||||||
|
}: DeleteAllThreadsDialogProps) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
|
const deleteButtonRef = useRef<HTMLButtonElement>(null)
|
||||||
|
|
||||||
|
const handleOpenChange = (open: boolean) => {
|
||||||
|
setIsOpen(open)
|
||||||
|
if (!open && onDropdownClose) {
|
||||||
|
onDropdownClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDeleteAll = () => {
|
||||||
|
onDeleteAll()
|
||||||
|
setIsOpen(false)
|
||||||
|
if (onDropdownClose) onDropdownClose()
|
||||||
|
toast.success(t('common:toast.deleteAllThreads.title'), {
|
||||||
|
id: 'delete-all-threads',
|
||||||
|
description: t('common:toast.deleteAllThreads.description'),
|
||||||
|
})
|
||||||
|
setTimeout(() => {
|
||||||
|
navigate({ to: route.home })
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
handleDeleteAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
||||||
|
<IconTrash size={16} />
|
||||||
|
<span>{t('common:deleteAll')}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent
|
||||||
|
onOpenAutoFocus={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
deleteButtonRef.current?.focus()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
{t('common:dialogs.deleteAllThreads.title')}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{t('common:dialogs.deleteAllThreads.description')}
|
||||||
|
</DialogDescription>
|
||||||
|
<DialogFooter className="mt-4 flex flex-col-reverse sm:flex-row sm:justify-end gap-2">
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button variant="link" size="sm" className="w-full sm:w-auto">
|
||||||
|
{t('common:cancel')}
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
<Button
|
||||||
|
ref={deleteButtonRef}
|
||||||
|
variant="destructive"
|
||||||
|
onClick={handleDeleteAll}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
size="sm"
|
||||||
|
className="w-full sm:w-auto"
|
||||||
|
aria-label={t('common:deleteAll')}
|
||||||
|
>
|
||||||
|
{t('common:deleteAll')}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogHeader>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
78
web-app/src/containers/dialogs/DeleteAssistantDialog.tsx
Normal file
78
web-app/src/containers/dialogs/DeleteAssistantDialog.tsx
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { useRef } from 'react'
|
||||||
|
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
} from '@/components/ui/dialog'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
|
||||||
|
interface DeleteAssistantDialogProps {
|
||||||
|
open: boolean
|
||||||
|
onOpenChange: (open: boolean) => void
|
||||||
|
onConfirm: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeleteAssistantDialog({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
onConfirm,
|
||||||
|
}: DeleteAssistantDialogProps) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const deleteButtonRef = useRef<HTMLButtonElement>(null)
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
onConfirm()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
onOpenChange(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
handleConfirm()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent
|
||||||
|
className="sm:max-w-[425px] max-w-[90vw]"
|
||||||
|
onOpenAutoFocus={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
deleteButtonRef.current?.focus()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{t('assistants:deleteConfirmation')}</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{t('assistants:deleteConfirmationDesc')}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter className="flex flex-col-reverse sm:flex-row sm:justify-end gap-2">
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
onClick={handleCancel}
|
||||||
|
className="w-full sm:w-auto"
|
||||||
|
>
|
||||||
|
{t('assistants:cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
ref={deleteButtonRef}
|
||||||
|
variant="destructive"
|
||||||
|
onClick={handleConfirm}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
className="w-full sm:w-auto"
|
||||||
|
aria-label={t('assistants:delete')}
|
||||||
|
>
|
||||||
|
{t('assistants:delete')}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -30,12 +30,15 @@ export default function DeleteMCPServerConfirm({
|
|||||||
<DialogTitle>{t('mcp-servers:deleteServer.title')}</DialogTitle>
|
<DialogTitle>{t('mcp-servers:deleteServer.title')}</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{t('mcp-servers:deleteServer.description', { serverName })}
|
{t('mcp-servers:deleteServer.description', { serverName })}
|
||||||
<span className="font-medium text-main-view-fg">{serverName}</span>?
|
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
|
<Button variant="link" onClick={() => onOpenChange(false)}>
|
||||||
|
{t('common:cancel')}
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
|
autoFocus
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onConfirm()
|
onConfirm()
|
||||||
onOpenChange(false)
|
onOpenChange(false)
|
||||||
|
|||||||
91
web-app/src/containers/dialogs/DeleteMessageDialog.tsx
Normal file
91
web-app/src/containers/dialogs/DeleteMessageDialog.tsx
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import { useState, useRef } from 'react'
|
||||||
|
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogTrigger,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
DialogDescription,
|
||||||
|
DialogClose,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
} from '@/components/ui/dialog'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { IconTrash } from '@tabler/icons-react'
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@/components/ui/tooltip'
|
||||||
|
|
||||||
|
interface DeleteMessageDialogProps {
|
||||||
|
onDelete: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeleteMessageDialog({ onDelete }: DeleteMessageDialogProps) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
|
const deleteButtonRef = useRef<HTMLButtonElement>(null)
|
||||||
|
|
||||||
|
const handleDelete = () => {
|
||||||
|
onDelete()
|
||||||
|
setIsOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
handleDelete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const trigger = (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<button className="flex items-center gap-1 hover:text-accent transition-colors cursor-pointer group relative">
|
||||||
|
<IconTrash size={16} />
|
||||||
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{t('delete')}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger>{trigger}</DialogTrigger>
|
||||||
|
<DialogContent
|
||||||
|
onOpenAutoFocus={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
deleteButtonRef.current?.focus()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{t('common:deleteMessage')}</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Are you sure you want to delete this message? This action cannot be
|
||||||
|
undone.
|
||||||
|
</DialogDescription>
|
||||||
|
<DialogFooter className="mt-4 flex flex-col-reverse sm:flex-row sm:justify-end gap-2">
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button variant="link" size="sm" className="w-full sm:w-auto">
|
||||||
|
{t('common:cancel')}
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
<Button
|
||||||
|
ref={deleteButtonRef}
|
||||||
|
variant="destructive"
|
||||||
|
onClick={handleDelete}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
size="sm"
|
||||||
|
className="w-full sm:w-auto"
|
||||||
|
aria-label={t('common:deleteMessage')}
|
||||||
|
>
|
||||||
|
{t('common:delete')}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogHeader>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -39,27 +39,33 @@ export const DialogDeleteModel = ({
|
|||||||
removeFavorite(selectedModelId)
|
removeFavorite(selectedModelId)
|
||||||
|
|
||||||
deleteModelCache(selectedModelId)
|
deleteModelCache(selectedModelId)
|
||||||
serviceHub.models().deleteModel(selectedModelId).then(() => {
|
serviceHub
|
||||||
serviceHub.providers().getProviders().then((providers) => {
|
.models()
|
||||||
// Filter out the deleted model from all providers
|
.deleteModel(selectedModelId)
|
||||||
const filteredProviders = providers.map((provider) => ({
|
.then(() => {
|
||||||
...provider,
|
serviceHub
|
||||||
models: provider.models.filter(
|
.providers()
|
||||||
(model) => model.id !== selectedModelId
|
.getProviders()
|
||||||
),
|
.then((providers) => {
|
||||||
}))
|
// Filter out the deleted model from all providers
|
||||||
setProviders(filteredProviders)
|
const filteredProviders = providers.map((provider) => ({
|
||||||
|
...provider,
|
||||||
|
models: provider.models.filter(
|
||||||
|
(model) => model.id !== selectedModelId
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
setProviders(filteredProviders)
|
||||||
|
})
|
||||||
|
toast.success(
|
||||||
|
t('providers:deleteModel.title', { modelId: selectedModel?.id }),
|
||||||
|
{
|
||||||
|
id: `delete-model-${selectedModel?.id}`,
|
||||||
|
description: t('providers:deleteModel.success', {
|
||||||
|
modelId: selectedModel?.id,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
)
|
||||||
})
|
})
|
||||||
toast.success(
|
|
||||||
t('providers:deleteModel.title', { modelId: selectedModel?.id }),
|
|
||||||
{
|
|
||||||
id: `delete-model-${selectedModel?.id}`,
|
|
||||||
description: t('providers:deleteModel.success', {
|
|
||||||
modelId: selectedModel?.id,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize with the provided model ID or the first model if available
|
// Initialize with the provided model ID or the first model if available
|
||||||
@ -105,7 +111,7 @@ export const DialogDeleteModel = ({
|
|||||||
</Button>
|
</Button>
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
<DialogClose asChild>
|
<DialogClose asChild>
|
||||||
<Button variant="destructive" size="sm" onClick={removeModel}>
|
<Button variant="destructive" size="sm" onClick={removeModel} autoFocus>
|
||||||
{t('providers:deleteModel.delete')}
|
{t('providers:deleteModel.delete')}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
|
|||||||
103
web-app/src/containers/dialogs/DeleteThreadDialog.tsx
Normal file
103
web-app/src/containers/dialogs/DeleteThreadDialog.tsx
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import { useState, useRef } from 'react'
|
||||||
|
import { useNavigate } from '@tanstack/react-router'
|
||||||
|
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogTrigger,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
DialogDescription,
|
||||||
|
DialogClose,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
} from '@/components/ui/dialog'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { IconTrash } from '@tabler/icons-react'
|
||||||
|
import { DropdownMenuItem } from '@/components/ui/dropdown-menu'
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
import { route } from '@/constants/routes'
|
||||||
|
|
||||||
|
interface DeleteThreadDialogProps {
|
||||||
|
thread: Thread
|
||||||
|
onDelete: (threadId: string) => void
|
||||||
|
onDropdownClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeleteThreadDialog({
|
||||||
|
thread,
|
||||||
|
onDelete,
|
||||||
|
onDropdownClose,
|
||||||
|
}: DeleteThreadDialogProps) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
|
const deleteButtonRef = useRef<HTMLButtonElement>(null)
|
||||||
|
|
||||||
|
const handleOpenChange = (open: boolean) => {
|
||||||
|
setIsOpen(open)
|
||||||
|
if (!open) {
|
||||||
|
onDropdownClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = () => {
|
||||||
|
onDelete(thread.id)
|
||||||
|
setIsOpen(false)
|
||||||
|
onDropdownClose()
|
||||||
|
toast.success(t('common:toast.deleteThread.title'), {
|
||||||
|
id: 'delete-thread',
|
||||||
|
description: t('common:toast.deleteThread.description'),
|
||||||
|
})
|
||||||
|
setTimeout(() => {
|
||||||
|
navigate({ to: route.home })
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
handleDelete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
||||||
|
<IconTrash />
|
||||||
|
<span>{t('common:delete')}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent
|
||||||
|
onOpenAutoFocus={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
deleteButtonRef.current?.focus()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{t('common:deleteThread')}</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{t('common:dialogs.deleteThread.description')}
|
||||||
|
</DialogDescription>
|
||||||
|
<DialogFooter className="mt-4 flex flex-col-reverse sm:flex-row sm:justify-end gap-2">
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button variant="link" size="sm" className="w-full sm:w-auto">
|
||||||
|
{t('common:cancel')}
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
<Button
|
||||||
|
ref={deleteButtonRef}
|
||||||
|
variant="destructive"
|
||||||
|
onClick={handleDelete}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
size="sm"
|
||||||
|
className="w-full sm:w-auto"
|
||||||
|
aria-label={`${t('common:delete')} ${thread.title || t('common:newThread')}`}
|
||||||
|
>
|
||||||
|
{t('common:delete')}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogHeader>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
111
web-app/src/containers/dialogs/EditMessageDialog.tsx
Normal file
111
web-app/src/containers/dialogs/EditMessageDialog.tsx
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import { useState, useEffect, useRef } from 'react'
|
||||||
|
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogTrigger,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
DialogClose,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
} from '@/components/ui/dialog'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
|
import { IconPencil } from '@tabler/icons-react'
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@/components/ui/tooltip'
|
||||||
|
|
||||||
|
interface EditMessageDialogProps {
|
||||||
|
message: string
|
||||||
|
onSave: (message: string) => void
|
||||||
|
triggerElement?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EditMessageDialog({
|
||||||
|
message,
|
||||||
|
onSave,
|
||||||
|
triggerElement,
|
||||||
|
}: EditMessageDialogProps) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
|
const [draft, setDraft] = useState(message)
|
||||||
|
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setDraft(message)
|
||||||
|
}, [message])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen && textareaRef.current) {
|
||||||
|
setTimeout(() => {
|
||||||
|
textareaRef.current?.focus()
|
||||||
|
textareaRef.current?.select()
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
}, [isOpen])
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
if (draft !== message && draft.trim()) {
|
||||||
|
onSave(draft)
|
||||||
|
setIsOpen(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
if (e.key === 'Enter' && e.ctrlKey) {
|
||||||
|
handleSave()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultTrigger = (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<button className="flex outline-0 items-center gap-1 hover:text-accent transition-colors cursor-pointer group relative">
|
||||||
|
<IconPencil size={16} />
|
||||||
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{t('edit')}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger>{triggerElement || defaultTrigger}</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{t('common:dialogs.editMessage.title')}</DialogTitle>
|
||||||
|
<Textarea
|
||||||
|
ref={textareaRef}
|
||||||
|
value={draft}
|
||||||
|
onChange={(e) => setDraft(e.target.value)}
|
||||||
|
className="mt-2 resize-none w-full min-h-24"
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder={t('common:dialogs.editMessage.title')}
|
||||||
|
aria-label={t('common:dialogs.editMessage.title')}
|
||||||
|
/>
|
||||||
|
<DialogFooter className="mt-4 flex flex-col-reverse sm:flex-row sm:justify-end gap-2">
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button variant="link" size="sm" className="w-full sm:w-auto">
|
||||||
|
{t('common:cancel')}
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
<Button
|
||||||
|
disabled={draft === message || !draft.trim()}
|
||||||
|
onClick={handleSave}
|
||||||
|
size="sm"
|
||||||
|
className="w-full sm:w-auto"
|
||||||
|
>
|
||||||
|
{t('common:save')}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogHeader>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
80
web-app/src/containers/dialogs/FactoryResetDialog.tsx
Normal file
80
web-app/src/containers/dialogs/FactoryResetDialog.tsx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import { useRef } from 'react'
|
||||||
|
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogTrigger,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
DialogDescription,
|
||||||
|
DialogClose,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
} from '@/components/ui/dialog'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
|
||||||
|
interface FactoryResetDialogProps {
|
||||||
|
onReset: () => void
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FactoryResetDialog({
|
||||||
|
onReset,
|
||||||
|
children,
|
||||||
|
}: FactoryResetDialogProps) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const resetButtonRef = useRef<HTMLButtonElement>(null)
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
onReset()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
handleReset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||||
|
<DialogContent
|
||||||
|
className="sm:max-w-[425px] max-w-[90vw]"
|
||||||
|
onOpenAutoFocus={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
resetButtonRef.current?.focus()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{t('settings:general.factoryResetTitle')}</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{t('settings:general.factoryResetDesc')}
|
||||||
|
</DialogDescription>
|
||||||
|
<DialogFooter className="mt-4 flex flex-col-reverse sm:flex-row sm:justify-end gap-2">
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
size="sm"
|
||||||
|
className="hover:no-underline w-full sm:w-auto"
|
||||||
|
>
|
||||||
|
{t('settings:general.cancel')}
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button
|
||||||
|
ref={resetButtonRef}
|
||||||
|
variant="destructive"
|
||||||
|
onClick={handleReset}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
size="sm"
|
||||||
|
className="w-full sm:w-auto"
|
||||||
|
aria-label={t('settings:general.reset')}
|
||||||
|
>
|
||||||
|
{t('settings:general.reset')}
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogHeader>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
40
web-app/src/containers/dialogs/ImageModal.tsx
Normal file
40
web-app/src/containers/dialogs/ImageModal.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog'
|
||||||
|
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||||
|
|
||||||
|
interface ImageModalProps {
|
||||||
|
image: { url: string; alt: string } | null
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ImageModal = ({ image, onClose }: ImageModalProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={!!image} onOpenChange={(open) => !open && onClose()}>
|
||||||
|
<DialogContent className="max-w-4xl max-h-[90vh] p-0">
|
||||||
|
<DialogHeader className="p-6 pb-2">
|
||||||
|
<DialogTitle>{image?.alt || t('common:image')}</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="flex justify-center items-center p-6 pt-2">
|
||||||
|
{image && (
|
||||||
|
<img
|
||||||
|
src={image.url}
|
||||||
|
alt={image.alt}
|
||||||
|
className="max-w-full max-h-[70vh] object-contain rounded-md"
|
||||||
|
onError={(e) => {
|
||||||
|
e.currentTarget.style.display = 'none'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ImageModal
|
||||||
71
web-app/src/containers/dialogs/MessageMetadataDialog.tsx
Normal file
71
web-app/src/containers/dialogs/MessageMetadataDialog.tsx
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogTrigger,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
DialogHeader,
|
||||||
|
} from '@/components/ui/dialog'
|
||||||
|
import { IconInfoCircle } from '@tabler/icons-react'
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@/components/ui/tooltip'
|
||||||
|
import CodeEditor from '@uiw/react-textarea-code-editor'
|
||||||
|
import '@uiw/react-textarea-code-editor/dist.css'
|
||||||
|
|
||||||
|
interface MessageMetadataDialogProps {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
metadata: any
|
||||||
|
triggerElement?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MessageMetadataDialog({
|
||||||
|
metadata,
|
||||||
|
triggerElement,
|
||||||
|
}: MessageMetadataDialogProps) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
|
|
||||||
|
const defaultTrigger = (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<button className="outline-0 focus:outline-0 flex items-center gap-1 hover:text-accent transition-colors cursor-pointer group relative">
|
||||||
|
<IconInfoCircle size={16} />
|
||||||
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{t('metadata')}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger>{triggerElement || defaultTrigger}</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{t('common:dialogs.messageMetadata.title')}</DialogTitle>
|
||||||
|
<div className="space-y-2 mt-4">
|
||||||
|
<div className="border border-main-view-fg/10 rounded-md">
|
||||||
|
<CodeEditor
|
||||||
|
value={JSON.stringify(metadata || {}, null, 2)}
|
||||||
|
language="json"
|
||||||
|
readOnly
|
||||||
|
data-color-mode="dark"
|
||||||
|
style={{
|
||||||
|
fontSize: 12,
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
}}
|
||||||
|
className="w-full h-full !text-sm "
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogHeader>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
112
web-app/src/containers/dialogs/RenameThreadDialog.tsx
Normal file
112
web-app/src/containers/dialogs/RenameThreadDialog.tsx
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogTrigger,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
DialogClose,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
} from '@/components/ui/dialog'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { IconEdit } from '@tabler/icons-react'
|
||||||
|
import { DropdownMenuItem } from '@/components/ui/dropdown-menu'
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
|
interface RenameThreadDialogProps {
|
||||||
|
thread: Thread
|
||||||
|
plainTitleForRename: string
|
||||||
|
onRename: (threadId: string, title: string) => void
|
||||||
|
onDropdownClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RenameThreadDialog({
|
||||||
|
thread,
|
||||||
|
plainTitleForRename,
|
||||||
|
onRename,
|
||||||
|
onDropdownClose,
|
||||||
|
}: RenameThreadDialogProps) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [title, setTitle] = useState('')
|
||||||
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen && inputRef.current) {
|
||||||
|
setTimeout(() => {
|
||||||
|
inputRef.current?.focus()
|
||||||
|
inputRef.current?.select()
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
}, [isOpen])
|
||||||
|
|
||||||
|
const handleOpenChange = (open: boolean) => {
|
||||||
|
setIsOpen(open)
|
||||||
|
if (open) {
|
||||||
|
setTitle(plainTitleForRename || t('common:newThread'))
|
||||||
|
} else {
|
||||||
|
onDropdownClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRename = () => {
|
||||||
|
if (title.trim()) {
|
||||||
|
onRename(thread.id, title.trim())
|
||||||
|
setIsOpen(false)
|
||||||
|
onDropdownClose()
|
||||||
|
toast.success(t('common:toast.renameThread.title'), {
|
||||||
|
id: 'rename-thread',
|
||||||
|
description: t('common:toast.renameThread.description', { title }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
if (e.key === 'Enter' && title.trim()) {
|
||||||
|
handleRename()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
||||||
|
<IconEdit />
|
||||||
|
<span>{t('common:rename')}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{t('common:threadTitle')}</DialogTitle>
|
||||||
|
<Input
|
||||||
|
ref={inputRef}
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
|
className="mt-2"
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder={t('common:threadTitle')}
|
||||||
|
aria-label={t('common:threadTitle')}
|
||||||
|
/>
|
||||||
|
<DialogFooter className="mt-4 flex flex-col-reverse sm:flex-row sm:justify-end gap-2">
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button variant="link" size="sm" className="w-full sm:w-auto">
|
||||||
|
{t('common:cancel')}
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
<Button
|
||||||
|
disabled={!title.trim()}
|
||||||
|
onClick={handleRename}
|
||||||
|
size="sm"
|
||||||
|
className="w-full sm:w-auto"
|
||||||
|
>
|
||||||
|
{t('common:rename')}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogHeader>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
9
web-app/src/containers/dialogs/index.ts
Normal file
9
web-app/src/containers/dialogs/index.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export { RenameThreadDialog } from './RenameThreadDialog'
|
||||||
|
export { DeleteThreadDialog } from './DeleteThreadDialog'
|
||||||
|
export { DeleteAllThreadsDialog } from './DeleteAllThreadsDialog'
|
||||||
|
export { EditMessageDialog } from './EditMessageDialog'
|
||||||
|
export { MessageMetadataDialog } from './MessageMetadataDialog'
|
||||||
|
export { DeleteMessageDialog } from './DeleteMessageDialog'
|
||||||
|
export { FactoryResetDialog } from './FactoryResetDialog'
|
||||||
|
export { DeleteAssistantDialog } from './DeleteAssistantDialog'
|
||||||
|
export { AddProviderDialog } from './AddProviderDialog'
|
||||||
@ -7,15 +7,7 @@ import { useAssistant } from '@/hooks/useAssistant'
|
|||||||
import HeaderPage from '@/containers/HeaderPage'
|
import HeaderPage from '@/containers/HeaderPage'
|
||||||
import { IconCirclePlus, IconPencil, IconTrash } from '@tabler/icons-react'
|
import { IconCirclePlus, IconPencil, IconTrash } from '@tabler/icons-react'
|
||||||
import AddEditAssistant from '@/containers/dialogs/AddEditAssistant'
|
import AddEditAssistant from '@/containers/dialogs/AddEditAssistant'
|
||||||
import {
|
import { DeleteAssistantDialog } from '@/containers/dialogs'
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { AvatarEmoji } from '@/containers/AvatarEmoji'
|
import { AvatarEmoji } from '@/containers/AvatarEmoji'
|
||||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||||
|
|
||||||
@ -64,7 +56,8 @@ function Assistant() {
|
|||||||
<div className="h-full p-4 overflow-y-auto">
|
<div className="h-full p-4 overflow-y-auto">
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||||
{assistants
|
{assistants
|
||||||
.slice().sort((a, b) => a.created_at - b.created_at)
|
.slice()
|
||||||
|
.sort((a, b) => a.created_at - b.created_at)
|
||||||
.map((assistant) => (
|
.map((assistant) => (
|
||||||
<div
|
<div
|
||||||
className="bg-main-view-fg/3 p-3 rounded-md"
|
className="bg-main-view-fg/3 p-3 rounded-md"
|
||||||
@ -134,27 +127,11 @@ function Assistant() {
|
|||||||
}
|
}
|
||||||
onSave={handleSave}
|
onSave={handleSave}
|
||||||
/>
|
/>
|
||||||
<Dialog open={deleteConfirmOpen} onOpenChange={setDeleteConfirmOpen}>
|
<DeleteAssistantDialog
|
||||||
<DialogContent>
|
open={deleteConfirmOpen}
|
||||||
<DialogHeader>
|
onOpenChange={setDeleteConfirmOpen}
|
||||||
<DialogTitle>{t('assistants:deleteConfirmation')}</DialogTitle>
|
onConfirm={confirmDelete}
|
||||||
<DialogDescription>
|
/>
|
||||||
{t('assistants:deleteConfirmationDesc')}
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
variant="link"
|
|
||||||
onClick={() => setDeleteConfirmOpen(false)}
|
|
||||||
>
|
|
||||||
{t('assistants:cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button variant="destructive" onClick={confirmDelete} autoFocus>
|
|
||||||
{t('assistants:delete')}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -10,17 +10,7 @@ import { useGeneralSetting } from '@/hooks/useGeneralSetting'
|
|||||||
import { useAppUpdater } from '@/hooks/useAppUpdater'
|
import { useAppUpdater } from '@/hooks/useAppUpdater'
|
||||||
import { useEffect, useState, useCallback } from 'react'
|
import { useEffect, useState, useCallback } from 'react'
|
||||||
import ChangeDataFolderLocation from '@/containers/dialogs/ChangeDataFolderLocation'
|
import ChangeDataFolderLocation from '@/containers/dialogs/ChangeDataFolderLocation'
|
||||||
|
import { FactoryResetDialog } from '@/containers/dialogs'
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogClose,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { useServiceHub } from '@/hooks/useServiceHub'
|
import { useServiceHub } from '@/hooks/useServiceHub'
|
||||||
import {
|
import {
|
||||||
IconBrandDiscord,
|
IconBrandDiscord,
|
||||||
@ -372,42 +362,11 @@ function General() {
|
|||||||
ns: 'settings',
|
ns: 'settings',
|
||||||
})}
|
})}
|
||||||
actions={
|
actions={
|
||||||
<Dialog>
|
<FactoryResetDialog onReset={resetApp}>
|
||||||
<DialogTrigger asChild>
|
<Button variant="destructive" size="sm">
|
||||||
<Button variant="destructive" size="sm">
|
{t('common:reset')}
|
||||||
{t('common:reset')}
|
</Button>
|
||||||
</Button>
|
</FactoryResetDialog>
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>
|
|
||||||
{t('settings:general.factoryResetTitle')}
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
{t('settings:general.factoryResetDesc')}
|
|
||||||
</DialogDescription>
|
|
||||||
<DialogFooter className="mt-2 flex items-center">
|
|
||||||
<DialogClose asChild>
|
|
||||||
<Button
|
|
||||||
variant="link"
|
|
||||||
size="sm"
|
|
||||||
className="hover:no-underline"
|
|
||||||
>
|
|
||||||
{t('settings:general.cancel')}
|
|
||||||
</Button>
|
|
||||||
</DialogClose>
|
|
||||||
<DialogClose asChild>
|
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
onClick={() => resetApp()}
|
|
||||||
>
|
|
||||||
{t('settings:general.reset')}
|
|
||||||
</Button>
|
|
||||||
</DialogClose>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogHeader>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@ -10,18 +10,9 @@ import { useNavigate } from '@tanstack/react-router'
|
|||||||
import { IconCirclePlus, IconSettings } from '@tabler/icons-react'
|
import { IconCirclePlus, IconSettings } from '@tabler/icons-react'
|
||||||
import { getProviderTitle } from '@/lib/utils'
|
import { getProviderTitle } from '@/lib/utils'
|
||||||
import ProvidersAvatar from '@/containers/ProvidersAvatar'
|
import ProvidersAvatar from '@/containers/ProvidersAvatar'
|
||||||
import {
|
import { AddProviderDialog } from '@/containers/dialogs'
|
||||||
Dialog,
|
|
||||||
DialogClose,
|
|
||||||
DialogContent,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { Input } from '@/components/ui/input'
|
|
||||||
import { Switch } from '@/components/ui/switch'
|
import { Switch } from '@/components/ui/switch'
|
||||||
import { useCallback, useState } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { openAIProviderSettings } from '@/consts/providers'
|
import { openAIProviderSettings } from '@/consts/providers'
|
||||||
import cloneDeep from 'lodash/cloneDeep'
|
import cloneDeep from 'lodash/cloneDeep'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
@ -39,33 +30,35 @@ function ModelProviders() {
|
|||||||
const serviceHub = useServiceHub()
|
const serviceHub = useServiceHub()
|
||||||
const { providers, addProvider, updateProvider } = useModelProvider()
|
const { providers, addProvider, updateProvider } = useModelProvider()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [name, setName] = useState('')
|
|
||||||
|
|
||||||
const createProvider = useCallback(() => {
|
const createProvider = useCallback(
|
||||||
if (
|
(name: string) => {
|
||||||
providers.some((e) => e.provider.toLowerCase() === name.toLowerCase())
|
if (
|
||||||
) {
|
providers.some((e) => e.provider.toLowerCase() === name.toLowerCase())
|
||||||
toast.error(t('providerAlreadyExists', { name }))
|
) {
|
||||||
return
|
toast.error(t('providerAlreadyExists', { name }))
|
||||||
}
|
return
|
||||||
const newProvider = {
|
}
|
||||||
provider: name,
|
const newProvider = {
|
||||||
active: true,
|
provider: name,
|
||||||
models: [],
|
active: true,
|
||||||
settings: cloneDeep(openAIProviderSettings) as ProviderSetting[],
|
models: [],
|
||||||
api_key: '',
|
settings: cloneDeep(openAIProviderSettings) as ProviderSetting[],
|
||||||
base_url: 'https://api.openai.com/v1',
|
api_key: '',
|
||||||
}
|
base_url: 'https://api.openai.com/v1',
|
||||||
addProvider(newProvider)
|
}
|
||||||
setTimeout(() => {
|
addProvider(newProvider)
|
||||||
navigate({
|
setTimeout(() => {
|
||||||
to: route.settings.providers,
|
navigate({
|
||||||
params: {
|
to: route.settings.providers,
|
||||||
providerName: name,
|
params: {
|
||||||
},
|
providerName: name,
|
||||||
})
|
},
|
||||||
}, 0)
|
})
|
||||||
}, [providers, name, addProvider, t, navigate])
|
}, 0)
|
||||||
|
},
|
||||||
|
[providers, addProvider, t, navigate]
|
||||||
|
)
|
||||||
|
|
||||||
// Check if model provider settings are enabled for this platform
|
// Check if model provider settings are enabled for this platform
|
||||||
if (!PlatformFeatures[PlatformFeature.MODEL_PROVIDER_SETTINGS]) {
|
if (!PlatformFeatures[PlatformFeature.MODEL_PROVIDER_SETTINGS]) {
|
||||||
@ -107,53 +100,18 @@ function ModelProviders() {
|
|||||||
<span className="text-main-view-fg font-medium text-base">
|
<span className="text-main-view-fg font-medium text-base">
|
||||||
{t('common:modelProviders')}
|
{t('common:modelProviders')}
|
||||||
</span>
|
</span>
|
||||||
<Dialog>
|
<AddProviderDialog onCreateProvider={createProvider}>
|
||||||
<DialogTrigger asChild>
|
<Button
|
||||||
<Button
|
variant="link"
|
||||||
variant="link"
|
size="sm"
|
||||||
size="sm"
|
className="flex items-center gap-2"
|
||||||
className="flex items-center gap-2"
|
>
|
||||||
>
|
<div className="cursor-pointer flex items-center justify-center rounded hover:bg-main-view-fg/15 bg-main-view-fg/10 transition-all duration-200 ease-in-out p-1.5 py-1 gap-1 -mr-2">
|
||||||
<div className="cursor-pointer flex items-center justify-center rounded hover:bg-main-view-fg/15 bg-main-view-fg/10 transition-all duration-200 ease-in-out p-1.5 py-1 gap-1 -mr-2">
|
<IconCirclePlus size={16} />
|
||||||
<IconCirclePlus size={16} />
|
<span>{t('provider:addProvider')}</span>
|
||||||
<span>{t('provider:addProvider')}</span>
|
</div>
|
||||||
</div>
|
</Button>
|
||||||
</Button>
|
</AddProviderDialog>
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>
|
|
||||||
{t('provider:addOpenAIProvider')}
|
|
||||||
</DialogTitle>
|
|
||||||
<Input
|
|
||||||
value={name}
|
|
||||||
onChange={(e) => setName(e.target.value)}
|
|
||||||
className="mt-2"
|
|
||||||
placeholder={t('provider:enterNameForProvider')}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
// Prevent key from being captured by parent components
|
|
||||||
e.stopPropagation()
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<DialogFooter className="mt-2 flex items-center">
|
|
||||||
<DialogClose asChild>
|
|
||||||
<Button
|
|
||||||
variant="link"
|
|
||||||
size="sm"
|
|
||||||
className="hover:no-underline"
|
|
||||||
>
|
|
||||||
{t('common:cancel')}
|
|
||||||
</Button>
|
|
||||||
</DialogClose>
|
|
||||||
<DialogClose asChild>
|
|
||||||
<Button disabled={!name} onClick={createProvider}>
|
|
||||||
{t('common:create')}
|
|
||||||
</Button>
|
|
||||||
</DialogClose>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogHeader>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@ -198,7 +156,10 @@ function ModelProviders() {
|
|||||||
<Switch
|
<Switch
|
||||||
checked={provider.active}
|
checked={provider.active}
|
||||||
onCheckedChange={async (e) => {
|
onCheckedChange={async (e) => {
|
||||||
if (!e && provider.provider.toLowerCase() === 'llamacpp') {
|
if (
|
||||||
|
!e &&
|
||||||
|
provider.provider.toLowerCase() === 'llamacpp'
|
||||||
|
) {
|
||||||
await serviceHub.models().stopAllModels()
|
await serviceHub.models().stopAllModels()
|
||||||
}
|
}
|
||||||
updateProvider(provider.provider, {
|
updateProvider(provider.provider, {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user