Merge pull request #5013 from menloresearch/feat/assistant-ui
chore: initial assistant feature
This commit is contained in:
commit
3dea599b9a
@ -25,7 +25,7 @@
|
||||
"@radix-ui/react-slot": "^1.2.0",
|
||||
"@radix-ui/react-switch": "^1.2.2",
|
||||
"@radix-ui/react-tooltip": "^1.2.4",
|
||||
"@tabler/icons-react": "^3.31.0",
|
||||
"@tabler/icons-react": "^3.33.0",
|
||||
"@tailwindcss/vite": "^4.1.4",
|
||||
"@tanstack/react-router": "^1.116.0",
|
||||
"@tanstack/react-router-devtools": "^1.116.0",
|
||||
|
||||
@ -150,7 +150,10 @@ function DropdownMenuLabel({
|
||||
<DropdownMenuPrimitive.Label
|
||||
data-slot="dropdown-menu-label"
|
||||
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}
|
||||
/>
|
||||
)
|
||||
|
||||
@ -7,7 +7,7 @@ function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) {
|
||||
<textarea
|
||||
data-slot="textarea"
|
||||
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',
|
||||
className
|
||||
)}
|
||||
|
||||
@ -2,6 +2,7 @@ export const localStoregeKey = {
|
||||
LeftPanel: 'left-panel',
|
||||
threads: 'threads',
|
||||
messages: 'messages',
|
||||
assistant: 'assistant',
|
||||
theme: 'theme',
|
||||
modelProvider: 'model-provider',
|
||||
settingAppearance: 'setting-appearance',
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
export const route = {
|
||||
// home as new chat or thread
|
||||
home: '/',
|
||||
assistant: '/assistant',
|
||||
settings: {
|
||||
index: '/settings',
|
||||
providers: '/settings/providers/$providerName',
|
||||
|
||||
@ -28,13 +28,19 @@ import { listen } from '@tauri-apps/api/event'
|
||||
import { SystemEvent } from '@/types/events'
|
||||
import { getTools } from '@/services/mcp'
|
||||
import { useChat } from '@/hooks/useChat'
|
||||
import DropdownModelProvider from '@/containers/DropdownModelProvider'
|
||||
|
||||
type ChatInputProps = {
|
||||
className?: string
|
||||
showSpeedToken?: boolean
|
||||
model?: ThreadModel
|
||||
}
|
||||
|
||||
const ChatInput = ({ className, showSpeedToken = true }: ChatInputProps) => {
|
||||
const ChatInput = ({
|
||||
model,
|
||||
className,
|
||||
showSpeedToken = true,
|
||||
}: ChatInputProps) => {
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||
const [isFocused, setIsFocused] = useState(false)
|
||||
const [rows, setRows] = useState(1)
|
||||
@ -160,6 +166,8 @@ const ChatInput = ({ className, showSpeedToken = true }: ChatInputProps) => {
|
||||
streamingContent && 'opacity-50 pointer-events-none'
|
||||
)}
|
||||
>
|
||||
<DropdownModelProvider model={model} />
|
||||
|
||||
{/* 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">
|
||||
<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'
|
||||
|
||||
type DropdownModelProviderProps = {
|
||||
model?: {
|
||||
id: string
|
||||
provider: string
|
||||
}
|
||||
model?: ThreadModel
|
||||
}
|
||||
|
||||
const DropdownModelProvider = ({ model }: DropdownModelProviderProps) => {
|
||||
@ -54,13 +51,23 @@ const DropdownModelProvider = ({ model }: DropdownModelProviderProps) => {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<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
|
||||
src={getProviderLogo(selectedProvider as string)}
|
||||
alt={`${selectedProvider} - Logo`}
|
||||
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>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
@ -115,6 +122,7 @@ const DropdownModelProvider = ({ model }: DropdownModelProviderProps) => {
|
||||
!provider.api_key?.length &&
|
||||
'hidden'
|
||||
)}
|
||||
title={model.id}
|
||||
key={`model-${modelIndex}`}
|
||||
onClick={() => {
|
||||
selectModelProvider(provider.provider, model.id)
|
||||
|
||||
@ -18,7 +18,7 @@ const HeaderPage = ({ children }: HeaderPageProps) => {
|
||||
platformName === 'macos' && !open ? 'pl-18' : 'pl-4'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center w-full gap-2">
|
||||
{!open && (
|
||||
<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"
|
||||
|
||||
@ -12,6 +12,7 @@ import {
|
||||
IconAppsFilled,
|
||||
IconX,
|
||||
IconSearch,
|
||||
IconClipboardSmileFilled,
|
||||
} from '@tabler/icons-react'
|
||||
import { route } from '@/constants/routes'
|
||||
import ThreadList from './ThreadList'
|
||||
@ -46,6 +47,11 @@ const mainMenus = [
|
||||
icon: IconCirclePlusFilled,
|
||||
route: route.home,
|
||||
},
|
||||
{
|
||||
title: 'Assistant',
|
||||
icon: IconClipboardSmileFilled,
|
||||
route: route.assistant,
|
||||
},
|
||||
{
|
||||
title: 'common.hub',
|
||||
icon: IconAppsFilled,
|
||||
@ -87,8 +93,6 @@ const LeftPanel = () => {
|
||||
|
||||
const [openDropdown, setOpenDropdown] = useState(false)
|
||||
|
||||
console.log(threads)
|
||||
|
||||
return (
|
||||
<aside
|
||||
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 HubImport } from './routes/hub'
|
||||
import { Route as AssistantImport } from './routes/assistant'
|
||||
import { Route as IndexImport } from './routes/index'
|
||||
import { Route as ThreadsThreadIdImport } from './routes/threads/$threadId'
|
||||
import { Route as SettingsShortcutsImport } from './routes/settings/shortcuts'
|
||||
@ -33,6 +34,12 @@ const HubRoute = HubImport.update({
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const AssistantRoute = AssistantImport.update({
|
||||
id: '/assistant',
|
||||
path: '/assistant',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const IndexRoute = IndexImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
@ -117,6 +124,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof IndexImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/assistant': {
|
||||
id: '/assistant'
|
||||
path: '/assistant'
|
||||
fullPath: '/assistant'
|
||||
preLoaderRoute: typeof AssistantImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/hub': {
|
||||
id: '/hub'
|
||||
path: '/hub'
|
||||
@ -208,6 +222,7 @@ declare module '@tanstack/react-router' {
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
'/assistant': typeof AssistantRoute
|
||||
'/hub': typeof HubRoute
|
||||
'/local-api-server/logs': typeof LocalApiServerLogsRoute
|
||||
'/settings/appearance': typeof SettingsAppearanceRoute
|
||||
@ -224,6 +239,7 @@ export interface FileRoutesByFullPath {
|
||||
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
'/assistant': typeof AssistantRoute
|
||||
'/hub': typeof HubRoute
|
||||
'/local-api-server/logs': typeof LocalApiServerLogsRoute
|
||||
'/settings/appearance': typeof SettingsAppearanceRoute
|
||||
@ -241,6 +257,7 @@ export interface FileRoutesByTo {
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRoute
|
||||
'/': typeof IndexRoute
|
||||
'/assistant': typeof AssistantRoute
|
||||
'/hub': typeof HubRoute
|
||||
'/local-api-server/logs': typeof LocalApiServerLogsRoute
|
||||
'/settings/appearance': typeof SettingsAppearanceRoute
|
||||
@ -259,6 +276,7 @@ export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths:
|
||||
| '/'
|
||||
| '/assistant'
|
||||
| '/hub'
|
||||
| '/local-api-server/logs'
|
||||
| '/settings/appearance'
|
||||
@ -274,6 +292,7 @@ export interface FileRouteTypes {
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to:
|
||||
| '/'
|
||||
| '/assistant'
|
||||
| '/hub'
|
||||
| '/local-api-server/logs'
|
||||
| '/settings/appearance'
|
||||
@ -289,6 +308,7 @@ export interface FileRouteTypes {
|
||||
id:
|
||||
| '__root__'
|
||||
| '/'
|
||||
| '/assistant'
|
||||
| '/hub'
|
||||
| '/local-api-server/logs'
|
||||
| '/settings/appearance'
|
||||
@ -306,6 +326,7 @@ export interface FileRouteTypes {
|
||||
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
AssistantRoute: typeof AssistantRoute
|
||||
HubRoute: typeof HubRoute
|
||||
LocalApiServerLogsRoute: typeof LocalApiServerLogsRoute
|
||||
SettingsAppearanceRoute: typeof SettingsAppearanceRoute
|
||||
@ -322,6 +343,7 @@ export interface RootRouteChildren {
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
AssistantRoute: AssistantRoute,
|
||||
HubRoute: HubRoute,
|
||||
LocalApiServerLogsRoute: LocalApiServerLogsRoute,
|
||||
SettingsAppearanceRoute: SettingsAppearanceRoute,
|
||||
@ -347,6 +369,7 @@ export const routeTree = rootRoute
|
||||
"filePath": "__root.tsx",
|
||||
"children": [
|
||||
"/",
|
||||
"/assistant",
|
||||
"/hub",
|
||||
"/local-api-server/logs",
|
||||
"/settings/appearance",
|
||||
@ -364,6 +387,9 @@ export const routeTree = rootRoute
|
||||
"/": {
|
||||
"filePath": "index.tsx"
|
||||
},
|
||||
"/assistant": {
|
||||
"filePath": "assistant.tsx"
|
||||
},
|
||||
"/hub": {
|
||||
"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 HeaderPage from '@/containers/HeaderPage'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import DropdownModelProvider from '@/containers/DropdownModelProvider'
|
||||
|
||||
import { useModelProvider } from '@/hooks/useModelProvider'
|
||||
import SetupScreen from '@/containers/SetupScreen'
|
||||
import { route } from '@/constants/routes'
|
||||
@ -14,6 +14,7 @@ type SearchParams = {
|
||||
provider: string
|
||||
}
|
||||
}
|
||||
import DropdownAssistant from '@/containers/DropdownAssistant'
|
||||
|
||||
export const Route = createFileRoute(route.home as any)({
|
||||
component: Index,
|
||||
@ -43,7 +44,7 @@ function Index() {
|
||||
return (
|
||||
<div className="flex h-full flex-col flex-justify-center">
|
||||
<HeaderPage>
|
||||
<DropdownModelProvider model={selectedModel} />
|
||||
<DropdownAssistant />
|
||||
</HeaderPage>
|
||||
<div className="h-full px-8 overflow-y-auto flex flex-col gap-2 justify-center">
|
||||
<div className="w-4/6 mx-auto">
|
||||
@ -56,7 +57,7 @@ function Index() {
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex-1 shrink-0">
|
||||
<ChatInput showSpeedToken={false} />
|
||||
<ChatInput showSpeedToken={false} model={selectedModel} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,21 +1,21 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { createFileRoute, useParams } from '@tanstack/react-router'
|
||||
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 { cn } from '@/lib/utils'
|
||||
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 { useMessages } from '@/hooks/useMessages'
|
||||
import { fetchMessages } from '@/services/messages'
|
||||
import { useAppState } from '@/hooks/useAppState'
|
||||
import DropdownAssistant from '@/containers/DropdownAssistant'
|
||||
|
||||
// as route.threadsDetail
|
||||
export const Route = createFileRoute('/threads/$threadId')({
|
||||
@ -159,7 +159,9 @@ function ThreadDetail() {
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<HeaderPage>
|
||||
<DropdownModelProvider model={threadModel} />
|
||||
<div className="flex items-center justify-between w-full pr-2">
|
||||
<DropdownAssistant />
|
||||
</div>
|
||||
{thread?.model?.provider === 'llama.cpp' && loadingModel && (
|
||||
<ModelLoader />
|
||||
)}
|
||||
@ -213,7 +215,7 @@ function ThreadDetail() {
|
||||
<ArrowDown size={12} />
|
||||
</div>
|
||||
</div>
|
||||
<ChatInput />
|
||||
<ChatInput model={threadModel} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user