Merge branch 'dev' into feat/model-selector
This commit is contained in:
commit
dbcc1db0b9
2
.github/workflows/jan-server-web-ci.yml
vendored
2
.github/workflows/jan-server-web-ci.yml
vendored
@ -8,7 +8,6 @@ on:
|
||||
- '.github/workflows/jan-server-web-ci.yml'
|
||||
- 'core/**'
|
||||
- 'web-app/**'
|
||||
- 'extensions/**'
|
||||
- 'extensions-web/**'
|
||||
- 'Makefile'
|
||||
- 'package.json'
|
||||
@ -20,7 +19,6 @@ on:
|
||||
- '.github/workflows/jan-server-web-ci.yml'
|
||||
- 'core/**'
|
||||
- 'web-app/**'
|
||||
- 'extensions/**'
|
||||
- 'extensions-web/**'
|
||||
- 'Makefile'
|
||||
- 'package.json'
|
||||
|
||||
@ -19,7 +19,6 @@ RUN yarn --version
|
||||
WORKDIR /app
|
||||
|
||||
# Copy source code
|
||||
COPY ./extensions ./extensions
|
||||
COPY ./extensions-web ./extensions-web
|
||||
COPY ./web-app ./web-app
|
||||
COPY ./Makefile ./Makefile
|
||||
|
||||
@ -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 { cn } from '@/lib/utils'
|
||||
import {
|
||||
@ -6,7 +6,6 @@ import {
|
||||
IconDots,
|
||||
IconCirclePlusFilled,
|
||||
IconSettingsFilled,
|
||||
IconTrash,
|
||||
IconStar,
|
||||
IconMessageFilled,
|
||||
IconAppsFilled,
|
||||
@ -27,17 +26,6 @@ import { useThreads } from '@/hooks/useThreads'
|
||||
|
||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||
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 { DownloadManagement } from '@/containers/DownloadManegement'
|
||||
import { useSmallScreen } from '@/hooks/useMediaQuery'
|
||||
@ -45,6 +33,7 @@ import { useClickOutside } from '@/hooks/useClickOutside'
|
||||
import { useDownloadStore } from '@/hooks/useDownloadStore'
|
||||
import { PlatformFeatures } from '@/lib/platform/const'
|
||||
import { PlatformFeature } from '@/lib/platform/types'
|
||||
import { DeleteAllThreadsDialog } from '@/containers/dialogs'
|
||||
|
||||
const mainMenus = [
|
||||
{
|
||||
@ -76,7 +65,6 @@ const mainMenus = [
|
||||
const LeftPanel = () => {
|
||||
const { open, setLeftPanel } = useLeftPanel()
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
|
||||
const isSmallScreen = useSmallScreen()
|
||||
@ -362,80 +350,25 @@ const LeftPanel = () => {
|
||||
{t('common:recents')}
|
||||
</span>
|
||||
<div className="relative">
|
||||
<Dialog>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<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"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}}
|
||||
>
|
||||
<IconDots
|
||||
size={18}
|
||||
className="text-left-panel-fg/60"
|
||||
/>
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="bottom" align="end">
|
||||
<DialogTrigger asChild>
|
||||
<DropdownMenuItem
|
||||
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>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<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"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}}
|
||||
>
|
||||
<IconDots
|
||||
size={18}
|
||||
className="text-left-panel-fg/60"
|
||||
/>
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="bottom" align="end">
|
||||
<DeleteAllThreadsDialog onDeleteAll={deleteAllThreads} />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -2,14 +2,7 @@
|
||||
import { ThreadMessage } from '@janhq/core'
|
||||
import { RenderMarkdown } from './RenderMarkdown'
|
||||
import React, { Fragment, memo, useCallback, useMemo, useState } from 'react'
|
||||
import {
|
||||
IconCopy,
|
||||
IconCopyCheck,
|
||||
IconRefresh,
|
||||
IconTrash,
|
||||
IconPencil,
|
||||
IconInfoCircle,
|
||||
} from '@tabler/icons-react'
|
||||
import { IconCopy, IconCopyCheck, IconRefresh } from '@tabler/icons-react'
|
||||
import { useAppState } from '@/hooks/useAppState'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useMessages } from '@/hooks/useMessages'
|
||||
@ -17,16 +10,10 @@ import ThinkingBlock from '@/containers/ThinkingBlock'
|
||||
import ToolCallBlock from '@/containers/ToolCallBlock'
|
||||
import { useChat } from '@/hooks/useChat'
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
EditMessageDialog,
|
||||
MessageMetadataDialog,
|
||||
DeleteMessageDialog,
|
||||
} from '@/containers/dialogs'
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@ -37,8 +24,6 @@ import { AvatarEmoji } from '@/containers/AvatarEmoji'
|
||||
|
||||
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 { 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
|
||||
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">
|
||||
<EditDialog
|
||||
<EditMessageDialog
|
||||
message={
|
||||
item.content?.find((c) => c.type === 'text')?.text?.value ||
|
||||
''
|
||||
}
|
||||
setMessage={(message) => {
|
||||
onSave={(message) => {
|
||||
if (item.updateMessage) {
|
||||
item.updateMessage(item, message)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<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>
|
||||
<DeleteMessageDialog
|
||||
onDelete={() => deleteMessage(item.thread_id, item.id)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@ -456,68 +366,15 @@ export const ThreadContent = memo(
|
||||
'hidden'
|
||||
)}
|
||||
>
|
||||
<EditDialog
|
||||
message={item.content?.[0]?.text.value}
|
||||
setMessage={(message) =>
|
||||
<EditMessageDialog
|
||||
message={item.content?.[0]?.text.value || ''}
|
||||
onSave={(message) =>
|
||||
item.updateMessage && item.updateMessage(item, message)
|
||||
}
|
||||
/>
|
||||
<CopyButton text={item.content?.[0]?.text.value || ''} />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<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>
|
||||
<DeleteMessageDialog onDelete={removeMessage} />
|
||||
<MessageMetadataDialog metadata={item.metadata} />
|
||||
|
||||
{item.isLastMessage && selectedModel && (
|
||||
<Tooltip>
|
||||
|
||||
@ -15,14 +15,11 @@ import { CSS } from '@dnd-kit/utilities'
|
||||
import {
|
||||
IconDots,
|
||||
IconStarFilled,
|
||||
IconTrash,
|
||||
IconEdit,
|
||||
IconStar,
|
||||
} from '@tabler/icons-react'
|
||||
import { useThreads } from '@/hooks/useThreads'
|
||||
import { useLeftPanel } from '@/hooks/useLeftPanel'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { route } from '@/constants/routes'
|
||||
import { useSmallScreen } from '@/hooks/useMediaQuery'
|
||||
|
||||
import {
|
||||
@ -33,19 +30,10 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
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 { useNavigate, useMatches } from '@tanstack/react-router'
|
||||
import { toast } from 'sonner'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { RenameThreadDialog, DeleteThreadDialog } from '@/containers/dialogs'
|
||||
import { route } from '@/constants/routes'
|
||||
|
||||
const SortableItem = memo(({ thread }: { thread: Thread }) => {
|
||||
const {
|
||||
@ -94,9 +82,6 @@ const SortableItem = memo(({ thread }: { thread: Thread }) => {
|
||||
return (thread.title || '').replace(/<span[^>]*>|<\/span>/g, '')
|
||||
}, [thread.title])
|
||||
|
||||
const [title, setTitle] = useState(
|
||||
plainTitleForRename || t('common:newThread')
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -156,116 +141,19 @@ const SortableItem = memo(({ thread }: { thread: Thread }) => {
|
||||
<span>{t('common:star')}</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<Dialog
|
||||
onOpenChange={(open) => {
|
||||
if (open) {
|
||||
setTitle(plainTitleForRename || t('common:newThread'))
|
||||
} else {
|
||||
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>
|
||||
<RenameThreadDialog
|
||||
thread={thread}
|
||||
plainTitleForRename={plainTitleForRename}
|
||||
onRename={renameThread}
|
||||
onDropdownClose={() => setOpenDropdown(false)}
|
||||
/>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
<Dialog
|
||||
onOpenChange={(open) => {
|
||||
if (!open) 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>
|
||||
<DeleteThreadDialog
|
||||
thread={thread}
|
||||
onDelete={deleteThread}
|
||||
onDropdownClose={() => setOpenDropdown(false)}
|
||||
/>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
@ -1,16 +1,11 @@
|
||||
import { ChevronDown, ChevronUp, Loader } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { create } from 'zustand'
|
||||
import { RenderMarkdown } from './RenderMarkdown'
|
||||
import { RenderMarkdown } from '@/containers/RenderMarkdown'
|
||||
import { useMemo, useState } from 'react'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||
import ImageModal from '@/containers/dialogs/ImageModal'
|
||||
|
||||
interface Props {
|
||||
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
|
||||
return (
|
||||
<div key={index} className="mt-3">
|
||||
@ -243,29 +230,7 @@ const ToolCallBlock = ({ id, name, result, loading, args }: Props) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Image Modal */}
|
||||
<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>
|
||||
<ImageModal image={modalImage} onClose={closeModal} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -45,7 +45,7 @@ export function TrustedHostsInput({
|
||||
onBlur={handleBlur}
|
||||
placeholder={t('common:enterTrustedHosts')}
|
||||
className={cn(
|
||||
'w-24 h-8 text-sm',
|
||||
'h-8 text-sm',
|
||||
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 (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<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>
|
||||
<DialogDescription>
|
||||
{t('mcp-servers:deleteServer.description', { serverName })}
|
||||
<span className="font-medium text-main-view-fg">{serverName}</span>?
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="link" onClick={() => onOpenChange(false)}>
|
||||
{t('common:cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
autoFocus
|
||||
onClick={() => {
|
||||
onConfirm()
|
||||
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>
|
||||
)
|
||||
}
|
||||
@ -37,29 +37,35 @@ export const DialogDeleteModel = ({
|
||||
const removeModel = async () => {
|
||||
// Remove model from favorites if it exists
|
||||
removeFavorite(selectedModelId)
|
||||
|
||||
|
||||
deleteModelCache(selectedModelId)
|
||||
serviceHub.models().deleteModel(selectedModelId).then(() => {
|
||||
serviceHub.providers().getProviders().then((providers) => {
|
||||
// Filter out the deleted model from all providers
|
||||
const filteredProviders = providers.map((provider) => ({
|
||||
...provider,
|
||||
models: provider.models.filter(
|
||||
(model) => model.id !== selectedModelId
|
||||
),
|
||||
}))
|
||||
setProviders(filteredProviders)
|
||||
serviceHub
|
||||
.models()
|
||||
.deleteModel(selectedModelId)
|
||||
.then(() => {
|
||||
serviceHub
|
||||
.providers()
|
||||
.getProviders()
|
||||
.then((providers) => {
|
||||
// Filter out the deleted model from all providers
|
||||
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
|
||||
@ -105,7 +111,7 @@ export const DialogDeleteModel = ({
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<DialogClose asChild>
|
||||
<Button variant="destructive" size="sm" onClick={removeModel}>
|
||||
<Button variant="destructive" size="sm" onClick={removeModel} autoFocus>
|
||||
{t('providers:deleteModel.delete')}
|
||||
</Button>
|
||||
</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 { IconCirclePlus, IconPencil, IconTrash } from '@tabler/icons-react'
|
||||
import AddEditAssistant from '@/containers/dialogs/AddEditAssistant'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { DeleteAssistantDialog } from '@/containers/dialogs'
|
||||
import { AvatarEmoji } from '@/containers/AvatarEmoji'
|
||||
import { useTranslation } from '@/i18n/react-i18next-compat'
|
||||
|
||||
@ -64,7 +56,8 @@ function Assistant() {
|
||||
<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">
|
||||
{assistants
|
||||
.slice().sort((a, b) => a.created_at - b.created_at)
|
||||
.slice()
|
||||
.sort((a, b) => a.created_at - b.created_at)
|
||||
.map((assistant) => (
|
||||
<div
|
||||
className="bg-main-view-fg/3 p-3 rounded-md"
|
||||
@ -134,27 +127,11 @@ function Assistant() {
|
||||
}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
<Dialog open={deleteConfirmOpen} onOpenChange={setDeleteConfirmOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('assistants:deleteConfirmation')}</DialogTitle>
|
||||
<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>
|
||||
<DeleteAssistantDialog
|
||||
open={deleteConfirmOpen}
|
||||
onOpenChange={setDeleteConfirmOpen}
|
||||
onConfirm={confirmDelete}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -10,17 +10,7 @@ import { useGeneralSetting } from '@/hooks/useGeneralSetting'
|
||||
import { useAppUpdater } from '@/hooks/useAppUpdater'
|
||||
import { useEffect, useState, useCallback } from 'react'
|
||||
import ChangeDataFolderLocation from '@/containers/dialogs/ChangeDataFolderLocation'
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog'
|
||||
import { FactoryResetDialog } from '@/containers/dialogs'
|
||||
import { useServiceHub } from '@/hooks/useServiceHub'
|
||||
import {
|
||||
IconBrandDiscord,
|
||||
@ -372,42 +362,11 @@ function General() {
|
||||
ns: 'settings',
|
||||
})}
|
||||
actions={
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="destructive" size="sm">
|
||||
{t('common:reset')}
|
||||
</Button>
|
||||
</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>
|
||||
<FactoryResetDialog onReset={resetApp}>
|
||||
<Button variant="destructive" size="sm">
|
||||
{t('common:reset')}
|
||||
</Button>
|
||||
</FactoryResetDialog>
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
@ -10,18 +10,9 @@ import { useNavigate } from '@tanstack/react-router'
|
||||
import { IconCirclePlus, IconSettings } from '@tabler/icons-react'
|
||||
import { getProviderTitle } from '@/lib/utils'
|
||||
import ProvidersAvatar from '@/containers/ProvidersAvatar'
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { AddProviderDialog } from '@/containers/dialogs'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { openAIProviderSettings } from '@/consts/providers'
|
||||
import cloneDeep from 'lodash/cloneDeep'
|
||||
import { toast } from 'sonner'
|
||||
@ -39,33 +30,35 @@ function ModelProviders() {
|
||||
const serviceHub = useServiceHub()
|
||||
const { providers, addProvider, updateProvider } = useModelProvider()
|
||||
const navigate = useNavigate()
|
||||
const [name, setName] = useState('')
|
||||
|
||||
const createProvider = useCallback(() => {
|
||||
if (
|
||||
providers.some((e) => e.provider.toLowerCase() === name.toLowerCase())
|
||||
) {
|
||||
toast.error(t('providerAlreadyExists', { name }))
|
||||
return
|
||||
}
|
||||
const newProvider = {
|
||||
provider: name,
|
||||
active: true,
|
||||
models: [],
|
||||
settings: cloneDeep(openAIProviderSettings) as ProviderSetting[],
|
||||
api_key: '',
|
||||
base_url: 'https://api.openai.com/v1',
|
||||
}
|
||||
addProvider(newProvider)
|
||||
setTimeout(() => {
|
||||
navigate({
|
||||
to: route.settings.providers,
|
||||
params: {
|
||||
providerName: name,
|
||||
},
|
||||
})
|
||||
}, 0)
|
||||
}, [providers, name, addProvider, t, navigate])
|
||||
const createProvider = useCallback(
|
||||
(name: string) => {
|
||||
if (
|
||||
providers.some((e) => e.provider.toLowerCase() === name.toLowerCase())
|
||||
) {
|
||||
toast.error(t('providerAlreadyExists', { name }))
|
||||
return
|
||||
}
|
||||
const newProvider = {
|
||||
provider: name,
|
||||
active: true,
|
||||
models: [],
|
||||
settings: cloneDeep(openAIProviderSettings) as ProviderSetting[],
|
||||
api_key: '',
|
||||
base_url: 'https://api.openai.com/v1',
|
||||
}
|
||||
addProvider(newProvider)
|
||||
setTimeout(() => {
|
||||
navigate({
|
||||
to: route.settings.providers,
|
||||
params: {
|
||||
providerName: name,
|
||||
},
|
||||
})
|
||||
}, 0)
|
||||
},
|
||||
[providers, addProvider, t, navigate]
|
||||
)
|
||||
|
||||
// Check if model provider settings are enabled for this platform
|
||||
if (!PlatformFeatures[PlatformFeature.MODEL_PROVIDER_SETTINGS]) {
|
||||
@ -107,53 +100,18 @@ function ModelProviders() {
|
||||
<span className="text-main-view-fg font-medium text-base">
|
||||
{t('common:modelProviders')}
|
||||
</span>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
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">
|
||||
<IconCirclePlus size={16} />
|
||||
<span>{t('provider:addProvider')}</span>
|
||||
</div>
|
||||
</Button>
|
||||
</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>
|
||||
<AddProviderDialog onCreateProvider={createProvider}>
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
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">
|
||||
<IconCirclePlus size={16} />
|
||||
<span>{t('provider:addProvider')}</span>
|
||||
</div>
|
||||
</Button>
|
||||
</AddProviderDialog>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
@ -198,7 +156,10 @@ function ModelProviders() {
|
||||
<Switch
|
||||
checked={provider.active}
|
||||
onCheckedChange={async (e) => {
|
||||
if (!e && provider.provider.toLowerCase() === 'llamacpp') {
|
||||
if (
|
||||
!e &&
|
||||
provider.provider.toLowerCase() === 'llamacpp'
|
||||
) {
|
||||
await serviceHub.models().stopAllModels()
|
||||
}
|
||||
updateProvider(provider.provider, {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user