chore: remove lock
This commit is contained in:
parent
3fedc9231c
commit
1c81e5a95e
@ -25,7 +25,7 @@
|
|||||||
"@radix-ui/react-slot": "^1.2.0",
|
"@radix-ui/react-slot": "^1.2.0",
|
||||||
"@radix-ui/react-switch": "^1.2.2",
|
"@radix-ui/react-switch": "^1.2.2",
|
||||||
"@radix-ui/react-tooltip": "^1.2.4",
|
"@radix-ui/react-tooltip": "^1.2.4",
|
||||||
"@tabler/icons-react": "^3.31.0",
|
"@tabler/icons-react": "^3.33.0",
|
||||||
"@tailwindcss/vite": "^4.1.4",
|
"@tailwindcss/vite": "^4.1.4",
|
||||||
"@tanstack/react-router": "^1.116.0",
|
"@tanstack/react-router": "^1.116.0",
|
||||||
"@tanstack/react-router-devtools": "^1.116.0",
|
"@tanstack/react-router-devtools": "^1.116.0",
|
||||||
|
|||||||
@ -150,7 +150,10 @@ function DropdownMenuLabel({
|
|||||||
<DropdownMenuPrimitive.Label
|
<DropdownMenuPrimitive.Label
|
||||||
data-slot="dropdown-menu-label"
|
data-slot="dropdown-menu-label"
|
||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
className={cn('px-2 py-1.5 text-sm data-[inset]:pl-8', className)}
|
className={cn(
|
||||||
|
'px-2 py-1.5 text-xs text-main-view-fg/50 data-[inset]:pl-8',
|
||||||
|
className
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -7,7 +7,7 @@ function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) {
|
|||||||
<textarea
|
<textarea
|
||||||
data-slot="textarea"
|
data-slot="textarea"
|
||||||
className={cn(
|
className={cn(
|
||||||
'border-input placeholder:text-main-view-fg/40 border-main-view-fg/10 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 ',
|
'border-input placeholder:text-main-view-fg/40 border-main-view-fg/10 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 text-sm',
|
||||||
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[1px] ring-main-view-fg/10',
|
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[1px] ring-main-view-fg/10',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ export const localStoregeKey = {
|
|||||||
LeftPanel: 'left-panel',
|
LeftPanel: 'left-panel',
|
||||||
threads: 'threads',
|
threads: 'threads',
|
||||||
messages: 'messages',
|
messages: 'messages',
|
||||||
|
assistant: 'assistant',
|
||||||
theme: 'theme',
|
theme: 'theme',
|
||||||
modelProvider: 'model-provider',
|
modelProvider: 'model-provider',
|
||||||
settingAppearance: 'setting-appearance',
|
settingAppearance: 'setting-appearance',
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
export const route = {
|
export const route = {
|
||||||
// home as new chat or thread
|
// home as new chat or thread
|
||||||
home: '/',
|
home: '/',
|
||||||
|
assistant: '/assistant',
|
||||||
settings: {
|
settings: {
|
||||||
index: '/settings',
|
index: '/settings',
|
||||||
providers: '/settings/providers/$providerName',
|
providers: '/settings/providers/$providerName',
|
||||||
|
|||||||
@ -28,13 +28,19 @@ import { listen } from '@tauri-apps/api/event'
|
|||||||
import { SystemEvent } from '@/types/events'
|
import { SystemEvent } from '@/types/events'
|
||||||
import { getTools } from '@/services/mcp'
|
import { getTools } from '@/services/mcp'
|
||||||
import { useChat } from '@/hooks/useChat'
|
import { useChat } from '@/hooks/useChat'
|
||||||
|
import DropdownModelProvider from '@/containers/DropdownModelProvider'
|
||||||
|
|
||||||
type ChatInputProps = {
|
type ChatInputProps = {
|
||||||
className?: string
|
className?: string
|
||||||
showSpeedToken?: boolean
|
showSpeedToken?: boolean
|
||||||
|
model?: ThreadModel
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChatInput = ({ className, showSpeedToken = true }: ChatInputProps) => {
|
const ChatInput = ({
|
||||||
|
model,
|
||||||
|
className,
|
||||||
|
showSpeedToken = true,
|
||||||
|
}: ChatInputProps) => {
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||||
const [isFocused, setIsFocused] = useState(false)
|
const [isFocused, setIsFocused] = useState(false)
|
||||||
const [rows, setRows] = useState(1)
|
const [rows, setRows] = useState(1)
|
||||||
@ -160,6 +166,8 @@ const ChatInput = ({ className, showSpeedToken = true }: ChatInputProps) => {
|
|||||||
streamingContent && 'opacity-50 pointer-events-none'
|
streamingContent && 'opacity-50 pointer-events-none'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
<DropdownModelProvider model={model} />
|
||||||
|
|
||||||
{/* File attachment - always available */}
|
{/* File attachment - always available */}
|
||||||
<div className="h-6 p-1 flex items-center justify-center rounded-sm hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out gap-1">
|
<div className="h-6 p-1 flex items-center justify-center rounded-sm hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out gap-1">
|
||||||
<IconPaperclip size={18} className="text-main-view-fg/50" />
|
<IconPaperclip size={18} className="text-main-view-fg/50" />
|
||||||
|
|||||||
67
web-app/src/containers/DropdownAssistant.tsx
Normal file
67
web-app/src/containers/DropdownAssistant.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/components/ui/dropdown-menu'
|
||||||
|
import { useAssistant } from '@/hooks/useAssistant'
|
||||||
|
import AddEditAssistant from './dialogs/AddEditAssistant'
|
||||||
|
import { IconCirclePlus } from '@tabler/icons-react'
|
||||||
|
import type { Assistant } from '@/hooks/useAssistant'
|
||||||
|
|
||||||
|
const DropdownAssistant = () => {
|
||||||
|
const { assistants, addAssistant, updateAssistant } = useAssistant()
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const [editingKey, setEditingKey] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const handleSave = (assistant: Assistant) => {
|
||||||
|
addAssistant(assistant)
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<button className="rounded font-medium cursor-pointer flex items-center gap-1.5 relative z-20">
|
||||||
|
<span className="text-main-view-fg/80">
|
||||||
|
{assistants[0]?.name || 'Jan'}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
className="w-44 max-h-[320px]"
|
||||||
|
side="bottom"
|
||||||
|
sideOffset={10}
|
||||||
|
align="start"
|
||||||
|
>
|
||||||
|
{assistants.map((assistant) => (
|
||||||
|
<DropdownMenuItem key={assistant.id}>
|
||||||
|
<span className="truncate text-main-view-fg/70">
|
||||||
|
{assistant.name}
|
||||||
|
</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem onClick={() => setOpen(true)}>
|
||||||
|
<IconCirclePlus />
|
||||||
|
<span className="truncate text-main-view-fg/70">
|
||||||
|
Create Assistant
|
||||||
|
</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
<AddEditAssistant
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
editingKey={editingKey}
|
||||||
|
onSave={handleSave}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DropdownAssistant
|
||||||
@ -16,10 +16,7 @@ import { route } from '@/constants/routes'
|
|||||||
import { useThreads } from '@/hooks/useThreads'
|
import { useThreads } from '@/hooks/useThreads'
|
||||||
|
|
||||||
type DropdownModelProviderProps = {
|
type DropdownModelProviderProps = {
|
||||||
model?: {
|
model?: ThreadModel
|
||||||
id: string
|
|
||||||
provider: string
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const DropdownModelProvider = ({ model }: DropdownModelProviderProps) => {
|
const DropdownModelProvider = ({ model }: DropdownModelProviderProps) => {
|
||||||
@ -54,13 +51,23 @@ const DropdownModelProvider = ({ model }: DropdownModelProviderProps) => {
|
|||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<button className="bg-main-view-fg/5 hover:bg-main-view-fg/8 px-2 py-1 rounded font-medium cursor-pointer flex items-center gap-1.5 relative z-20">
|
<button
|
||||||
|
title={displayModel}
|
||||||
|
className="bg-main-view-fg/5 hover:bg-main-view-fg/8 px-2 py-1 rounded font-medium cursor-pointer flex items-center gap-1.5 relative z-20 max-w-40"
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
src={getProviderLogo(selectedProvider as string)}
|
src={getProviderLogo(selectedProvider as string)}
|
||||||
alt={`${selectedProvider} - Logo`}
|
alt={`${selectedProvider} - Logo`}
|
||||||
className="size-4"
|
className="size-4"
|
||||||
/>
|
/>
|
||||||
<span className="text-main-view-fg/80">{displayModel}</span>
|
<span
|
||||||
|
className={cn(
|
||||||
|
'text-main-view-fg/80 truncate leading-normal',
|
||||||
|
!selectedModel?.id && 'text-main-view-fg/50'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{displayModel}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
@ -115,6 +122,7 @@ const DropdownModelProvider = ({ model }: DropdownModelProviderProps) => {
|
|||||||
!provider.api_key?.length &&
|
!provider.api_key?.length &&
|
||||||
'hidden'
|
'hidden'
|
||||||
)}
|
)}
|
||||||
|
title={model.id}
|
||||||
key={`model-${modelIndex}`}
|
key={`model-${modelIndex}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
selectModelProvider(provider.provider, model.id)
|
selectModelProvider(provider.provider, model.id)
|
||||||
|
|||||||
@ -18,7 +18,7 @@ const HeaderPage = ({ children }: HeaderPageProps) => {
|
|||||||
platformName === 'macos' && !open ? 'pl-18' : 'pl-4'
|
platformName === 'macos' && !open ? 'pl-18' : 'pl-4'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center w-full gap-2">
|
||||||
{!open && (
|
{!open && (
|
||||||
<button
|
<button
|
||||||
className="size-5 cursor-pointer flex items-center justify-center rounded hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out data-[state=open]:bg-main-view-fg/10"
|
className="size-5 cursor-pointer flex items-center justify-center rounded hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out data-[state=open]:bg-main-view-fg/10"
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {
|
|||||||
IconAppsFilled,
|
IconAppsFilled,
|
||||||
IconX,
|
IconX,
|
||||||
IconSearch,
|
IconSearch,
|
||||||
|
IconClipboardSmileFilled,
|
||||||
} from '@tabler/icons-react'
|
} from '@tabler/icons-react'
|
||||||
import { route } from '@/constants/routes'
|
import { route } from '@/constants/routes'
|
||||||
import ThreadList from './ThreadList'
|
import ThreadList from './ThreadList'
|
||||||
@ -46,6 +47,11 @@ const mainMenus = [
|
|||||||
icon: IconCirclePlusFilled,
|
icon: IconCirclePlusFilled,
|
||||||
route: route.home,
|
route: route.home,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Assistant',
|
||||||
|
icon: IconClipboardSmileFilled,
|
||||||
|
route: route.assistant,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'common.hub',
|
title: 'common.hub',
|
||||||
icon: IconAppsFilled,
|
icon: IconAppsFilled,
|
||||||
@ -87,8 +93,6 @@ const LeftPanel = () => {
|
|||||||
|
|
||||||
const [openDropdown, setOpenDropdown] = useState(false)
|
const [openDropdown, setOpenDropdown] = useState(false)
|
||||||
|
|
||||||
console.log(threads)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|||||||
217
web-app/src/containers/dialogs/AddEditAssistant.tsx
Normal file
217
web-app/src/containers/dialogs/AddEditAssistant.tsx
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogFooter,
|
||||||
|
} from '@/components/ui/dialog'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { IconPlus, IconTrash } from '@tabler/icons-react'
|
||||||
|
import { Assistant } from '@/hooks/useAssistant'
|
||||||
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
|
|
||||||
|
interface AddEditAssistantProps {
|
||||||
|
open: boolean
|
||||||
|
onOpenChange: (open: boolean) => void
|
||||||
|
editingKey: string | null
|
||||||
|
initialData?: Assistant
|
||||||
|
onSave: (assistant: Assistant) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AddEditAssistant({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
editingKey,
|
||||||
|
initialData,
|
||||||
|
onSave,
|
||||||
|
}: AddEditAssistantProps) {
|
||||||
|
const [avatar, setAvatar] = useState<string | undefined>(initialData?.avatar)
|
||||||
|
|
||||||
|
const [name, setName] = useState(initialData?.name || '')
|
||||||
|
const [description, setDescription] = useState<string | undefined>(
|
||||||
|
initialData?.description
|
||||||
|
)
|
||||||
|
const [instructions, setInstructions] = useState(
|
||||||
|
initialData?.instructions || ''
|
||||||
|
)
|
||||||
|
const [paramsKeys, setParamsKeys] = useState<string[]>([''])
|
||||||
|
const [paramsValues, setParamsValues] = useState<string[]>([''])
|
||||||
|
|
||||||
|
// Reset form when modal opens/closes or editing key changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (open && editingKey && initialData) {
|
||||||
|
setAvatar(initialData.avatar)
|
||||||
|
setName(initialData.name)
|
||||||
|
setDescription(initialData.description)
|
||||||
|
setInstructions(initialData.instructions)
|
||||||
|
// Convert parameters object to arrays of keys and values
|
||||||
|
const keys = Object.keys(initialData.parameters || {})
|
||||||
|
const values = Object.values(initialData.parameters || {})
|
||||||
|
|
||||||
|
setParamsKeys(keys.length > 0 ? keys : [''])
|
||||||
|
setParamsValues(values.length > 0 ? values : [''])
|
||||||
|
} else if (open) {
|
||||||
|
// Add mode - reset form
|
||||||
|
resetForm()
|
||||||
|
}
|
||||||
|
}, [open, editingKey, initialData])
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
setAvatar(undefined)
|
||||||
|
setName('')
|
||||||
|
setDescription(undefined)
|
||||||
|
setInstructions('')
|
||||||
|
setParamsKeys([''])
|
||||||
|
setParamsValues([''])
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleParameterChange = (
|
||||||
|
index: number,
|
||||||
|
value: string,
|
||||||
|
isKey: boolean
|
||||||
|
) => {
|
||||||
|
if (isKey) {
|
||||||
|
const newKeys = [...paramsKeys]
|
||||||
|
newKeys[index] = value
|
||||||
|
setParamsKeys(newKeys)
|
||||||
|
} else {
|
||||||
|
const newValues = [...paramsValues]
|
||||||
|
newValues[index] = value
|
||||||
|
setParamsValues(newValues)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAddParameter = () => {
|
||||||
|
setParamsKeys([...paramsKeys, ''])
|
||||||
|
setParamsValues([...paramsValues, ''])
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRemoveParameter = (index: number) => {
|
||||||
|
const newKeys = [...paramsKeys]
|
||||||
|
const newValues = [...paramsValues]
|
||||||
|
newKeys.splice(index, 1)
|
||||||
|
newValues.splice(index, 1)
|
||||||
|
setParamsKeys(newKeys.length > 0 ? newKeys : [''])
|
||||||
|
setParamsValues(newValues.length > 0 ? newValues : [''])
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
// Convert parameters arrays to object
|
||||||
|
const parameters: Record<string, string> = {}
|
||||||
|
paramsKeys.forEach((key, index) => {
|
||||||
|
parameters[key] = paramsValues[index] || ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const assistant: Assistant = {
|
||||||
|
avatar,
|
||||||
|
id: initialData?.id || Math.random().toString(36).substring(7),
|
||||||
|
name,
|
||||||
|
created_at: initialData?.created_at || Date.now(),
|
||||||
|
description,
|
||||||
|
instructions,
|
||||||
|
parameters: parameters || {},
|
||||||
|
}
|
||||||
|
onSave(assistant)
|
||||||
|
onOpenChange(false)
|
||||||
|
resetForm()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
{editingKey ? 'Edit Assistant' : 'Add Assistant'}
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm mb-2 inline-block">Name</label>
|
||||||
|
<Input
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
placeholder="Enter name"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm mb-2 inline-block">
|
||||||
|
Avatar (optional)
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
value={avatar || ''}
|
||||||
|
onChange={(e) => setAvatar(e.target.value)}
|
||||||
|
placeholder="Enter avatar URL"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm mb-2 inline-block">
|
||||||
|
Description (optional)
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
value={description || ''}
|
||||||
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
placeholder="Enter description"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm mb-2 inline-block">Instructions</label>
|
||||||
|
<Textarea
|
||||||
|
value={instructions}
|
||||||
|
onChange={(e) => setInstructions(e.target.value)}
|
||||||
|
placeholder="Enter instructions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="text-sm">Parameters</label>
|
||||||
|
<div
|
||||||
|
className="size-6 cursor-pointer flex items-center justify-center rounded hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out"
|
||||||
|
onClick={handleAddParameter}
|
||||||
|
>
|
||||||
|
<IconPlus size={18} className="text-main-view-fg/60" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{paramsKeys.map((key, index) => (
|
||||||
|
<div key={index} className="flex items-center gap-2">
|
||||||
|
<Input
|
||||||
|
value={key}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleParameterChange(index, e.target.value, true)
|
||||||
|
}
|
||||||
|
placeholder="Key"
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
value={paramsValues[index] || ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleParameterChange(index, e.target.value, false)
|
||||||
|
}
|
||||||
|
placeholder="Value"
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="size-6 cursor-pointer flex items-center justify-center rounded hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out"
|
||||||
|
onClick={() => handleRemoveParameter(index)}
|
||||||
|
>
|
||||||
|
<IconTrash size={18} className="text-destructive" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button onClick={handleSave}>Save</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
51
web-app/src/hooks/useAssistant.ts
Normal file
51
web-app/src/hooks/useAssistant.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { localStoregeKey } from '@/constants/localStorage'
|
||||||
|
import { create } from 'zustand'
|
||||||
|
import { persist } from 'zustand/middleware'
|
||||||
|
|
||||||
|
export type Assistant = {
|
||||||
|
avatar?: string
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
created_at: number
|
||||||
|
description?: string
|
||||||
|
instructions: string
|
||||||
|
parameters: Record<string, string>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AssistantState {
|
||||||
|
assistants: Assistant[]
|
||||||
|
addAssistant: (assistant: Assistant) => void
|
||||||
|
updateAssistant: (assistant: Assistant) => void
|
||||||
|
deleteAssistant: (id: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultAssistant: Assistant = {
|
||||||
|
avatar: '',
|
||||||
|
id: 'jan',
|
||||||
|
name: 'Jan',
|
||||||
|
created_at: 1747029866.542,
|
||||||
|
description: 'A default assistant that can use all downloaded models.',
|
||||||
|
instructions: '',
|
||||||
|
parameters: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAssistant = create<AssistantState>()(
|
||||||
|
persist(
|
||||||
|
(set, get) => ({
|
||||||
|
assistants: [defaultAssistant],
|
||||||
|
addAssistant: (assistant) =>
|
||||||
|
set({ assistants: [...get().assistants, assistant] }),
|
||||||
|
updateAssistant: (assistant) =>
|
||||||
|
set({
|
||||||
|
assistants: get().assistants.map((a) =>
|
||||||
|
a.id === assistant.id ? assistant : a
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
deleteAssistant: (id) =>
|
||||||
|
set({ assistants: get().assistants.filter((a) => a.id !== id) }),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: localStoregeKey.assistant,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
import { Route as rootRoute } from './routes/__root'
|
import { Route as rootRoute } from './routes/__root'
|
||||||
import { Route as HubImport } from './routes/hub'
|
import { Route as HubImport } from './routes/hub'
|
||||||
|
import { Route as AssistantImport } from './routes/assistant'
|
||||||
import { Route as IndexImport } from './routes/index'
|
import { Route as IndexImport } from './routes/index'
|
||||||
import { Route as ThreadsThreadIdImport } from './routes/threads/$threadId'
|
import { Route as ThreadsThreadIdImport } from './routes/threads/$threadId'
|
||||||
import { Route as SettingsShortcutsImport } from './routes/settings/shortcuts'
|
import { Route as SettingsShortcutsImport } from './routes/settings/shortcuts'
|
||||||
@ -33,6 +34,12 @@ const HubRoute = HubImport.update({
|
|||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
|
const AssistantRoute = AssistantImport.update({
|
||||||
|
id: '/assistant',
|
||||||
|
path: '/assistant',
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
const IndexRoute = IndexImport.update({
|
const IndexRoute = IndexImport.update({
|
||||||
id: '/',
|
id: '/',
|
||||||
path: '/',
|
path: '/',
|
||||||
@ -117,6 +124,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof IndexImport
|
preLoaderRoute: typeof IndexImport
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute
|
||||||
}
|
}
|
||||||
|
'/assistant': {
|
||||||
|
id: '/assistant'
|
||||||
|
path: '/assistant'
|
||||||
|
fullPath: '/assistant'
|
||||||
|
preLoaderRoute: typeof AssistantImport
|
||||||
|
parentRoute: typeof rootRoute
|
||||||
|
}
|
||||||
'/hub': {
|
'/hub': {
|
||||||
id: '/hub'
|
id: '/hub'
|
||||||
path: '/hub'
|
path: '/hub'
|
||||||
@ -208,6 +222,7 @@ declare module '@tanstack/react-router' {
|
|||||||
|
|
||||||
export interface FileRoutesByFullPath {
|
export interface FileRoutesByFullPath {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
|
'/assistant': typeof AssistantRoute
|
||||||
'/hub': typeof HubRoute
|
'/hub': typeof HubRoute
|
||||||
'/local-api-server/logs': typeof LocalApiServerLogsRoute
|
'/local-api-server/logs': typeof LocalApiServerLogsRoute
|
||||||
'/settings/appearance': typeof SettingsAppearanceRoute
|
'/settings/appearance': typeof SettingsAppearanceRoute
|
||||||
@ -224,6 +239,7 @@ export interface FileRoutesByFullPath {
|
|||||||
|
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
|
'/assistant': typeof AssistantRoute
|
||||||
'/hub': typeof HubRoute
|
'/hub': typeof HubRoute
|
||||||
'/local-api-server/logs': typeof LocalApiServerLogsRoute
|
'/local-api-server/logs': typeof LocalApiServerLogsRoute
|
||||||
'/settings/appearance': typeof SettingsAppearanceRoute
|
'/settings/appearance': typeof SettingsAppearanceRoute
|
||||||
@ -241,6 +257,7 @@ export interface FileRoutesByTo {
|
|||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
__root__: typeof rootRoute
|
__root__: typeof rootRoute
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
|
'/assistant': typeof AssistantRoute
|
||||||
'/hub': typeof HubRoute
|
'/hub': typeof HubRoute
|
||||||
'/local-api-server/logs': typeof LocalApiServerLogsRoute
|
'/local-api-server/logs': typeof LocalApiServerLogsRoute
|
||||||
'/settings/appearance': typeof SettingsAppearanceRoute
|
'/settings/appearance': typeof SettingsAppearanceRoute
|
||||||
@ -259,6 +276,7 @@ export interface FileRouteTypes {
|
|||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
fullPaths:
|
fullPaths:
|
||||||
| '/'
|
| '/'
|
||||||
|
| '/assistant'
|
||||||
| '/hub'
|
| '/hub'
|
||||||
| '/local-api-server/logs'
|
| '/local-api-server/logs'
|
||||||
| '/settings/appearance'
|
| '/settings/appearance'
|
||||||
@ -274,6 +292,7 @@ export interface FileRouteTypes {
|
|||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to:
|
to:
|
||||||
| '/'
|
| '/'
|
||||||
|
| '/assistant'
|
||||||
| '/hub'
|
| '/hub'
|
||||||
| '/local-api-server/logs'
|
| '/local-api-server/logs'
|
||||||
| '/settings/appearance'
|
| '/settings/appearance'
|
||||||
@ -289,6 +308,7 @@ export interface FileRouteTypes {
|
|||||||
id:
|
id:
|
||||||
| '__root__'
|
| '__root__'
|
||||||
| '/'
|
| '/'
|
||||||
|
| '/assistant'
|
||||||
| '/hub'
|
| '/hub'
|
||||||
| '/local-api-server/logs'
|
| '/local-api-server/logs'
|
||||||
| '/settings/appearance'
|
| '/settings/appearance'
|
||||||
@ -306,6 +326,7 @@ export interface FileRouteTypes {
|
|||||||
|
|
||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
IndexRoute: typeof IndexRoute
|
IndexRoute: typeof IndexRoute
|
||||||
|
AssistantRoute: typeof AssistantRoute
|
||||||
HubRoute: typeof HubRoute
|
HubRoute: typeof HubRoute
|
||||||
LocalApiServerLogsRoute: typeof LocalApiServerLogsRoute
|
LocalApiServerLogsRoute: typeof LocalApiServerLogsRoute
|
||||||
SettingsAppearanceRoute: typeof SettingsAppearanceRoute
|
SettingsAppearanceRoute: typeof SettingsAppearanceRoute
|
||||||
@ -322,6 +343,7 @@ export interface RootRouteChildren {
|
|||||||
|
|
||||||
const rootRouteChildren: RootRouteChildren = {
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
IndexRoute: IndexRoute,
|
IndexRoute: IndexRoute,
|
||||||
|
AssistantRoute: AssistantRoute,
|
||||||
HubRoute: HubRoute,
|
HubRoute: HubRoute,
|
||||||
LocalApiServerLogsRoute: LocalApiServerLogsRoute,
|
LocalApiServerLogsRoute: LocalApiServerLogsRoute,
|
||||||
SettingsAppearanceRoute: SettingsAppearanceRoute,
|
SettingsAppearanceRoute: SettingsAppearanceRoute,
|
||||||
@ -347,6 +369,7 @@ export const routeTree = rootRoute
|
|||||||
"filePath": "__root.tsx",
|
"filePath": "__root.tsx",
|
||||||
"children": [
|
"children": [
|
||||||
"/",
|
"/",
|
||||||
|
"/assistant",
|
||||||
"/hub",
|
"/hub",
|
||||||
"/local-api-server/logs",
|
"/local-api-server/logs",
|
||||||
"/settings/appearance",
|
"/settings/appearance",
|
||||||
@ -364,6 +387,9 @@ export const routeTree = rootRoute
|
|||||||
"/": {
|
"/": {
|
||||||
"filePath": "index.tsx"
|
"filePath": "index.tsx"
|
||||||
},
|
},
|
||||||
|
"/assistant": {
|
||||||
|
"filePath": "assistant.tsx"
|
||||||
|
},
|
||||||
"/hub": {
|
"/hub": {
|
||||||
"filePath": "hub.tsx"
|
"filePath": "hub.tsx"
|
||||||
},
|
},
|
||||||
|
|||||||
109
web-app/src/routes/assistant.tsx
Normal file
109
web-app/src/routes/assistant.tsx
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
import { route } from '@/constants/routes'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
import { useAssistant } from '@/hooks/useAssistant'
|
||||||
|
import type { Assistant } from '@/hooks/useAssistant'
|
||||||
|
|
||||||
|
import HeaderPage from '@/containers/HeaderPage'
|
||||||
|
import {
|
||||||
|
IconCirclePlus,
|
||||||
|
IconCodeCircle,
|
||||||
|
IconPencil,
|
||||||
|
IconTrash,
|
||||||
|
} from '@tabler/icons-react'
|
||||||
|
import AddEditAssistant from '@/containers/dialogs/AddEditAssistant'
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export const Route = createFileRoute(route.assistant as any)({
|
||||||
|
component: Assistant,
|
||||||
|
})
|
||||||
|
|
||||||
|
function Assistant() {
|
||||||
|
const { assistants, addAssistant, updateAssistant } = useAssistant()
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const [editingKey, setEditingKey] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const handleSave = (assistant: Assistant) => {
|
||||||
|
if (editingKey) {
|
||||||
|
updateAssistant(assistant)
|
||||||
|
} else {
|
||||||
|
addAssistant(assistant)
|
||||||
|
}
|
||||||
|
setOpen(false)
|
||||||
|
setEditingKey(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full flex-col flex-justify-center">
|
||||||
|
<HeaderPage>
|
||||||
|
<span>Assistant</span>
|
||||||
|
</HeaderPage>
|
||||||
|
<div className="h-full p-4 overflow-y-auto">
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
{assistants.map((assistant) => (
|
||||||
|
<div
|
||||||
|
className="bg-main-view-fg/3 p-3 rounded-md"
|
||||||
|
key={assistant.id}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<h3 className="text-base font-medium text-main-view-fg/80">
|
||||||
|
{assistant.name}
|
||||||
|
</h3>
|
||||||
|
<div className="flex items-center gap-0.5">
|
||||||
|
<div
|
||||||
|
className="size-6 cursor-pointer flex items-center justify-center rounded hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out"
|
||||||
|
title="Edit Assistant in JSON"
|
||||||
|
>
|
||||||
|
<IconCodeCircle
|
||||||
|
size={18}
|
||||||
|
className="text-main-view-fg/50"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="size-6 cursor-pointer flex items-center justify-center rounded hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out"
|
||||||
|
title="Edit Assistant"
|
||||||
|
onClick={() => {
|
||||||
|
setEditingKey(assistant.id)
|
||||||
|
setOpen(true)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconPencil size={18} className="text-main-view-fg/50" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="size-6 cursor-pointer flex items-center justify-center rounded hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out"
|
||||||
|
title="Delete Assistant"
|
||||||
|
>
|
||||||
|
<IconTrash size={18} className="text-main-view-fg/50" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-main-view-fg/50 mt-1">
|
||||||
|
{assistant.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div
|
||||||
|
className="bg-main-view p-3 rounded-md border border-dashed border-main-view-fg/10 flex items-center justify-center cursor-pointer hover:bg-main-view-fg/1 transition-all duration-200 ease-in-out"
|
||||||
|
key="new-assistant"
|
||||||
|
onClick={() => {
|
||||||
|
setEditingKey(null)
|
||||||
|
setOpen(true)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconCirclePlus className="text-main-view-fg/50" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<AddEditAssistant
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
editingKey={editingKey}
|
||||||
|
initialData={
|
||||||
|
editingKey ? assistants.find((a) => a.id === editingKey) : undefined
|
||||||
|
}
|
||||||
|
onSave={handleSave}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -3,7 +3,7 @@ import { createFileRoute, useSearch } from '@tanstack/react-router'
|
|||||||
import ChatInput from '@/containers/ChatInput'
|
import ChatInput from '@/containers/ChatInput'
|
||||||
import HeaderPage from '@/containers/HeaderPage'
|
import HeaderPage from '@/containers/HeaderPage'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import DropdownModelProvider from '@/containers/DropdownModelProvider'
|
|
||||||
import { useModelProvider } from '@/hooks/useModelProvider'
|
import { useModelProvider } from '@/hooks/useModelProvider'
|
||||||
import SetupScreen from '@/containers/SetupScreen'
|
import SetupScreen from '@/containers/SetupScreen'
|
||||||
import { route } from '@/constants/routes'
|
import { route } from '@/constants/routes'
|
||||||
@ -14,6 +14,7 @@ type SearchParams = {
|
|||||||
provider: string
|
provider: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
import DropdownAssistant from '@/containers/DropdownAssistant'
|
||||||
|
|
||||||
export const Route = createFileRoute(route.home as any)({
|
export const Route = createFileRoute(route.home as any)({
|
||||||
component: Index,
|
component: Index,
|
||||||
@ -43,7 +44,7 @@ function Index() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col flex-justify-center">
|
<div className="flex h-full flex-col flex-justify-center">
|
||||||
<HeaderPage>
|
<HeaderPage>
|
||||||
<DropdownModelProvider model={selectedModel} />
|
<DropdownAssistant />
|
||||||
</HeaderPage>
|
</HeaderPage>
|
||||||
<div className="h-full px-8 overflow-y-auto flex flex-col gap-2 justify-center">
|
<div className="h-full px-8 overflow-y-auto flex flex-col gap-2 justify-center">
|
||||||
<div className="w-4/6 mx-auto">
|
<div className="w-4/6 mx-auto">
|
||||||
@ -56,7 +57,7 @@ function Index() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 shrink-0">
|
<div className="flex-1 shrink-0">
|
||||||
<ChatInput showSpeedToken={false} />
|
<ChatInput showSpeedToken={false} model={selectedModel} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,21 +1,21 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { createFileRoute, useParams } from '@tanstack/react-router'
|
import { createFileRoute, useParams } from '@tanstack/react-router'
|
||||||
import { UIEventHandler } from 'react'
|
import { UIEventHandler } from 'react'
|
||||||
import HeaderPage from '@/containers/HeaderPage'
|
|
||||||
|
|
||||||
import { useThreads } from '@/hooks/useThreads'
|
|
||||||
import ChatInput from '@/containers/ChatInput'
|
|
||||||
import DropdownModelProvider from '@/containers/DropdownModelProvider'
|
|
||||||
import { useShallow } from 'zustand/react/shallow'
|
|
||||||
import { ThreadContent } from '@/containers/ThreadContent'
|
|
||||||
import { StreamingContent } from '@/containers/StreamingContent'
|
|
||||||
import debounce from 'lodash.debounce'
|
import debounce from 'lodash.debounce'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { ArrowDown } from 'lucide-react'
|
import { ArrowDown } from 'lucide-react'
|
||||||
|
|
||||||
|
import HeaderPage from '@/containers/HeaderPage'
|
||||||
|
import { useThreads } from '@/hooks/useThreads'
|
||||||
|
import ChatInput from '@/containers/ChatInput'
|
||||||
|
import { useShallow } from 'zustand/react/shallow'
|
||||||
|
import { ThreadContent } from '@/containers/ThreadContent'
|
||||||
|
import { StreamingContent } from '@/containers/StreamingContent'
|
||||||
import { ModelLoader } from '@/containers/loaders/ModelLoader'
|
import { ModelLoader } from '@/containers/loaders/ModelLoader'
|
||||||
import { useMessages } from '@/hooks/useMessages'
|
import { useMessages } from '@/hooks/useMessages'
|
||||||
import { fetchMessages } from '@/services/messages'
|
import { fetchMessages } from '@/services/messages'
|
||||||
import { useAppState } from '@/hooks/useAppState'
|
import { useAppState } from '@/hooks/useAppState'
|
||||||
|
import DropdownAssistant from '@/containers/DropdownAssistant'
|
||||||
|
|
||||||
// as route.threadsDetail
|
// as route.threadsDetail
|
||||||
export const Route = createFileRoute('/threads/$threadId')({
|
export const Route = createFileRoute('/threads/$threadId')({
|
||||||
@ -159,7 +159,9 @@ function ThreadDetail() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<HeaderPage>
|
<HeaderPage>
|
||||||
<DropdownModelProvider model={threadModel} />
|
<div className="flex items-center justify-between w-full pr-2">
|
||||||
|
<DropdownAssistant />
|
||||||
|
</div>
|
||||||
{thread?.model?.provider === 'llama.cpp' && loadingModel && (
|
{thread?.model?.provider === 'llama.cpp' && loadingModel && (
|
||||||
<ModelLoader />
|
<ModelLoader />
|
||||||
)}
|
)}
|
||||||
@ -213,7 +215,7 @@ function ThreadDetail() {
|
|||||||
<ArrowDown size={12} />
|
<ArrowDown size={12} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ChatInput />
|
<ChatInput model={threadModel} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user