chore: add functiond delete and params type assistant

This commit is contained in:
Faisal Amir 2025-05-19 22:47:57 +07:00
parent 2ae7417e10
commit afb8ff716a
5 changed files with 323 additions and 62 deletions

View File

@ -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 text-sm', '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
)} )}

View File

@ -8,31 +8,50 @@ import {
} from '@/components/ui/dropdown-menu' } from '@/components/ui/dropdown-menu'
import { useAssistant } from '@/hooks/useAssistant' import { useAssistant } from '@/hooks/useAssistant'
import AddEditAssistant from './dialogs/AddEditAssistant' import AddEditAssistant from './dialogs/AddEditAssistant'
import { IconCirclePlus } from '@tabler/icons-react' import { IconCirclePlus, IconSettings } from '@tabler/icons-react'
import type { Assistant } from '@/hooks/useAssistant'
const DropdownAssistant = () => { const DropdownAssistant = () => {
// @ts-ignore
const { assistants, addAssistant, updateAssistant } = useAssistant() const { assistants, addAssistant, updateAssistant } = useAssistant()
const [open, setOpen] = useState(false) const [dropdownOpen, setDropdownOpen] = useState(false)
// @ts-ignore const [dialogOpen, setDialogOpen] = useState(false)
const [editingKey, setEditingKey] = useState<string | null>(null) const [editingAssistantId, setEditingAssistantId] = useState<string | null>(
null
)
const [selectedAssistantId, setSelectedAssistantId] = useState<string | null>(
assistants[0]?.id || null
)
const handleSave = (assistant: Assistant) => { const selectedAssistant =
addAssistant(assistant) assistants.find((a) => a.id === selectedAssistantId) || assistants[0]
setOpen(false)
}
return ( return (
<> <>
<DropdownMenu> <DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}>
<DropdownMenuTrigger asChild> <div className="flex items-center justify-between gap-1">
<button className="rounded font-medium cursor-pointer flex items-center gap-1.5 relative z-20"> <DropdownMenuTrigger asChild>
<span className="text-main-view-fg/80"> <button className="bg-main-view-fg/5 py-0.5 hover:bg-main-view-fg/8 px-2 rounded font-medium cursor-pointer flex items-center gap-1.5 relative z-20 max-w-40">
{assistants[0]?.name || 'Jan'} <span className="text-main-view-fg/80 truncate">
</span> {selectedAssistant?.name || 'Jan'}
</button> </span>
</DropdownMenuTrigger> </button>
</DropdownMenuTrigger>
<div
className="size-5 cursor-pointer relative z-10 flex items-center justify-center rounded hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out"
onClick={() => {
console.log('edit clicked', selectedAssistant)
if (selectedAssistant) {
setEditingAssistantId(selectedAssistant.id)
setDialogOpen(true)
}
}}
>
<IconSettings
size={16}
className="text-main-view-fg/50"
title="Edit Assistant"
/>
</div>
</div>
<DropdownMenuContent <DropdownMenuContent
className="w-44 max-h-[320px]" className="w-44 max-h-[320px]"
side="bottom" side="bottom"
@ -40,15 +59,36 @@ const DropdownAssistant = () => {
align="start" align="start"
> >
{assistants.map((assistant) => ( {assistants.map((assistant) => (
<DropdownMenuItem key={assistant.id}> <div className="relative" key={assistant.id}>
<span className="truncate text-main-view-fg/70"> <DropdownMenuItem className="flex justify-between items-center">
{assistant.name} <span
</span> className="truncate text-main-view-fg/70 flex-1 cursor-pointer"
</DropdownMenuItem> onClick={() => setSelectedAssistantId(assistant.id)}
>
{assistant.name}
</span>
</DropdownMenuItem>
<div className="absolute top-1/2 -translate-y-1/2 right-2">
<div className="size-5 text-main-view-fg/50 cursor-pointer relative z-10 flex items-center justify-center rounded hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out">
<IconSettings
size={16}
onClick={() => {
setEditingAssistantId(assistant.id)
setDialogOpen(true)
}}
/>
</div>
</div>
</div>
))} ))}
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem onClick={() => setOpen(true)}> <DropdownMenuItem
onClick={() => {
setEditingAssistantId(null)
setDialogOpen(true)
}}
>
<IconCirclePlus /> <IconCirclePlus />
<span className="truncate text-main-view-fg/70"> <span className="truncate text-main-view-fg/70">
Create Assistant Create Assistant
@ -57,10 +97,23 @@ const DropdownAssistant = () => {
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
<AddEditAssistant <AddEditAssistant
open={open} open={dialogOpen}
onOpenChange={setOpen} onOpenChange={setDialogOpen}
editingKey={editingKey} editingKey={editingAssistantId}
onSave={handleSave} initialData={
editingAssistantId
? assistants.find((a) => a.id === editingAssistantId)
: undefined
}
onSave={(assistant) => {
if (editingAssistantId) {
updateAssistant(assistant)
} else {
addAssistant(assistant)
}
setEditingAssistantId(null)
setDialogOpen(false)
}}
/> />
</> </>
) )

View File

@ -8,10 +8,17 @@ import {
} from '@/components/ui/dialog' } from '@/components/ui/dialog'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { IconPlus, IconTrash } from '@tabler/icons-react' import { IconPlus, IconTrash, IconChevronDown } from '@tabler/icons-react'
import { Assistant } from '@/hooks/useAssistant' import { Assistant } from '@/hooks/useAssistant'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
interface AddEditAssistantProps { interface AddEditAssistantProps {
open: boolean open: boolean
onOpenChange: (open: boolean) => void onOpenChange: (open: boolean) => void
@ -37,7 +44,8 @@ export default function AddEditAssistant({
initialData?.instructions || '' initialData?.instructions || ''
) )
const [paramsKeys, setParamsKeys] = useState<string[]>(['']) const [paramsKeys, setParamsKeys] = useState<string[]>([''])
const [paramsValues, setParamsValues] = useState<string[]>(['']) const [paramsValues, setParamsValues] = useState<unknown[]>([''])
const [paramsTypes, setParamsTypes] = useState<string[]>(['string'])
// Reset form when modal opens/closes or editing key changes // Reset form when modal opens/closes or editing key changes
useEffect(() => { useEffect(() => {
@ -50,8 +58,17 @@ export default function AddEditAssistant({
const keys = Object.keys(initialData.parameters || {}) const keys = Object.keys(initialData.parameters || {})
const values = Object.values(initialData.parameters || {}) const values = Object.values(initialData.parameters || {})
// Determine parameter types based on values
const types = values.map((value) => {
if (typeof value === 'boolean') return 'boolean'
if (typeof value === 'number') return 'number'
if (typeof value === 'object') return 'json'
return 'string'
})
setParamsKeys(keys.length > 0 ? keys : ['']) setParamsKeys(keys.length > 0 ? keys : [''])
setParamsValues(values.length > 0 ? values : ['']) setParamsValues(values.length > 0 ? values : [''])
setParamsTypes(types.length > 0 ? types : ['string'])
} else if (open) { } else if (open) {
// Add mode - reset form // Add mode - reset form
resetForm() resetForm()
@ -65,43 +82,88 @@ export default function AddEditAssistant({
setInstructions('') setInstructions('')
setParamsKeys(['']) setParamsKeys([''])
setParamsValues(['']) setParamsValues([''])
setParamsTypes(['string'])
} }
const handleParameterChange = ( const handleParameterChange = (
index: number, index: number,
value: string, value: unknown,
isKey: boolean field: 'key' | 'value' | 'type'
) => { ) => {
if (isKey) { if (field === 'key') {
const newKeys = [...paramsKeys] const newKeys = [...paramsKeys]
newKeys[index] = value newKeys[index] = value as string
setParamsKeys(newKeys) setParamsKeys(newKeys)
} else { } else if (field === 'value') {
const newValues = [...paramsValues] const newValues = [...paramsValues]
newValues[index] = value
// Convert value based on parameter type
if (paramsTypes[index] === 'number' && typeof value === 'string') {
newValues[index] = value === '' ? '' : Number(value)
} else if (
paramsTypes[index] === 'boolean' &&
typeof value === 'boolean'
) {
newValues[index] = value
} else if (paramsTypes[index] === 'json' && typeof value === 'string') {
try {
newValues[index] = value === '' ? {} : JSON.parse(value)
} catch {
// If JSON is invalid, keep as string
newValues[index] = value
}
} else {
newValues[index] = value
}
setParamsValues(newValues) setParamsValues(newValues)
} else {
const newTypes = [...paramsTypes]
newTypes[index] = value as string
// Reset value based on the new type
const newValues = [...paramsValues]
if (value === 'string') {
newValues[index] = ''
} else if (value === 'number') {
newValues[index] = ''
} else if (value === 'boolean') {
newValues[index] = false
} else if (value === 'json') {
newValues[index] = {}
}
setParamsValues(newValues)
setParamsTypes(newTypes)
} }
} }
const handleAddParameter = () => { const handleAddParameter = () => {
setParamsKeys([...paramsKeys, '']) setParamsKeys([...paramsKeys, ''])
setParamsValues([...paramsValues, '']) setParamsValues([...paramsValues, ''])
setParamsTypes([...paramsTypes, 'string'])
} }
const handleRemoveParameter = (index: number) => { const handleRemoveParameter = (index: number) => {
const newKeys = [...paramsKeys] const newKeys = [...paramsKeys]
const newValues = [...paramsValues] const newValues = [...paramsValues]
const newTypes = [...paramsTypes]
newKeys.splice(index, 1) newKeys.splice(index, 1)
newValues.splice(index, 1) newValues.splice(index, 1)
newTypes.splice(index, 1)
setParamsKeys(newKeys.length > 0 ? newKeys : ['']) setParamsKeys(newKeys.length > 0 ? newKeys : [''])
setParamsValues(newValues.length > 0 ? newValues : ['']) setParamsValues(newValues.length > 0 ? newValues : [''])
setParamsTypes(newTypes.length > 0 ? newTypes : ['string'])
} }
const handleSave = () => { const handleSave = () => {
// Convert parameters arrays to object // Convert parameters arrays to object
const parameters: Record<string, string> = {} const parameters: Record<string, unknown> = {}
paramsKeys.forEach((key, index) => { paramsKeys.forEach((key, index) => {
parameters[key] = paramsValues[index] || '' if (key) {
parameters[key] = paramsValues[index]
}
}) })
const assistant: Assistant = { const assistant: Assistant = {
@ -126,7 +188,7 @@ export default function AddEditAssistant({
{editingKey ? 'Edit Assistant' : 'Add Assistant'} {editingKey ? 'Edit Assistant' : 'Add Assistant'}
</DialogTitle> </DialogTitle>
</DialogHeader> </DialogHeader>
<div className="space-y-4"> <div className="space-y-2">
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm mb-2 inline-block">Name</label> <label className="text-sm mb-2 inline-block">Name</label>
<Input <Input
@ -152,10 +214,11 @@ export default function AddEditAssistant({
<label className="text-sm mb-2 inline-block"> <label className="text-sm mb-2 inline-block">
Description (optional) Description (optional)
</label> </label>
<Input <Textarea
value={description || ''} value={description || ''}
onChange={(e) => setDescription(e.target.value)} onChange={(e) => setDescription(e.target.value)}
placeholder="Enter description" placeholder="Enter description"
className="resize-none"
/> />
</div> </div>
@ -165,6 +228,8 @@ export default function AddEditAssistant({
value={instructions} value={instructions}
onChange={(e) => setInstructions(e.target.value)} onChange={(e) => setInstructions(e.target.value)}
placeholder="Enter instructions" placeholder="Enter instructions"
className="resize-none"
rows={4}
/> />
</div> </div>
@ -184,19 +249,116 @@ export default function AddEditAssistant({
<Input <Input
value={key} value={key}
onChange={(e) => onChange={(e) =>
handleParameterChange(index, e.target.value, true) handleParameterChange(index, e.target.value, 'key')
} }
placeholder="Key" placeholder="Key"
className="flex-1" className="w-24"
/>
<Input
value={paramsValues[index] || ''}
onChange={(e) =>
handleParameterChange(index, e.target.value, false)
}
placeholder="Value"
className="flex-1"
/> />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className="relative w-30">
<Input
value={
paramsTypes[index].charAt(0).toUpperCase() +
paramsTypes[index].slice(1)
}
readOnly
/>
<IconChevronDown
size={14}
className="text-main-view-fg/50 absolute right-2 top-1/2 -translate-y-1/2"
/>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-32" align="start">
<DropdownMenuItem
onClick={() =>
handleParameterChange(index, 'string', 'type')
}
>
String
</DropdownMenuItem>
<DropdownMenuItem
onClick={() =>
handleParameterChange(index, 'number', 'type')
}
>
Number
</DropdownMenuItem>
<DropdownMenuItem
onClick={() =>
handleParameterChange(index, 'boolean', 'type')
}
>
Boolean
</DropdownMenuItem>
<DropdownMenuItem
onClick={() =>
handleParameterChange(index, 'json', 'type')
}
>
JSON
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{paramsTypes[index] === 'boolean' ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className="relative flex-1">
<Input
value={paramsValues[index] ? 'True' : 'False'}
readOnly
/>
<IconChevronDown
size={14}
className="text-main-view-fg/50 absolute right-2 top-1/2 -translate-y-1/2"
/>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-24" align="start">
<DropdownMenuItem
onClick={() =>
handleParameterChange(index, true, 'value')
}
>
True
</DropdownMenuItem>
<DropdownMenuItem
onClick={() =>
handleParameterChange(index, false, 'value')
}
>
False
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
) : paramsTypes[index] === 'json' ? (
<Input
value={
typeof paramsValues[index] === 'object'
? JSON.stringify(paramsValues[index], null, 2)
: paramsValues[index]?.toString() || ''
}
onChange={(e) =>
handleParameterChange(index, e.target.value, 'value')
}
placeholder="JSON Value"
className="flex-1"
/>
) : (
<Input
value={paramsValues[index]?.toString() || ''}
onChange={(e) =>
handleParameterChange(index, e.target.value, 'value')
}
type={paramsTypes[index] === 'number' ? 'number' : 'text'}
placeholder="Value"
className="flex-1"
/>
)}
<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" 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)} onClick={() => handleRemoveParameter(index)}

View File

@ -9,7 +9,7 @@ export type Assistant = {
created_at: number created_at: number
description?: string description?: string
instructions: string instructions: string
parameters: Record<string, string> parameters: Record<string, unknown>
} }
interface AssistantState { interface AssistantState {

View File

@ -6,13 +6,17 @@ import { useAssistant } from '@/hooks/useAssistant'
import type { Assistant } from '@/hooks/useAssistant' import type { Assistant } from '@/hooks/useAssistant'
import HeaderPage from '@/containers/HeaderPage' import HeaderPage from '@/containers/HeaderPage'
import { import { IconCirclePlus, IconPencil, IconTrash } from '@tabler/icons-react'
IconCirclePlus,
IconCodeCircle,
IconPencil,
IconTrash,
} from '@tabler/icons-react'
import AddEditAssistant from '@/containers/dialogs/AddEditAssistant' import AddEditAssistant from '@/containers/dialogs/AddEditAssistant'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export const Route = createFileRoute(route.assistant as any)({ export const Route = createFileRoute(route.assistant as any)({
@ -20,9 +24,25 @@ export const Route = createFileRoute(route.assistant as any)({
}) })
function Assistant() { function Assistant() {
const { assistants, addAssistant, updateAssistant } = useAssistant() const { assistants, addAssistant, updateAssistant, deleteAssistant } =
useAssistant()
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [editingKey, setEditingKey] = useState<string | null>(null) const [editingKey, setEditingKey] = useState<string | null>(null)
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false)
const [deletingId, setDeletingId] = useState<string | null>(null)
const handleDelete = (id: string) => {
setDeletingId(id)
setDeleteConfirmOpen(true)
}
const confirmDelete = () => {
if (deletingId) {
deleteAssistant(deletingId)
setDeleteConfirmOpen(false)
setDeletingId(null)
}
}
const handleSave = (assistant: Assistant) => { const handleSave = (assistant: Assistant) => {
if (editingKey) { if (editingKey) {
@ -51,7 +71,7 @@ function Assistant() {
{assistant.name} {assistant.name}
</h3> </h3>
<div className="flex items-center gap-0.5"> <div className="flex items-center gap-0.5">
<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" 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" title="Edit Assistant in JSON"
> >
@ -59,7 +79,7 @@ function Assistant() {
size={18} size={18}
className="text-main-view-fg/50" className="text-main-view-fg/50"
/> />
</div> </div> */}
<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" 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" title="Edit Assistant"
@ -73,12 +93,16 @@ function Assistant() {
<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" 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" title="Delete Assistant"
onClick={() => handleDelete(assistant.id)}
> >
<IconTrash size={18} className="text-main-view-fg/50" /> <IconTrash size={18} className="text-main-view-fg/50" />
</div> </div>
</div> </div>
</div> </div>
<p className="text-main-view-fg/50 mt-1"> <p
className="text-main-view-fg/50 mt-1 line-clamp-2"
title={assistant.description}
>
{assistant.description} {assistant.description}
</p> </p>
</div> </div>
@ -103,6 +127,28 @@ function Assistant() {
} }
onSave={handleSave} onSave={handleSave}
/> />
<Dialog open={deleteConfirmOpen} onOpenChange={setDeleteConfirmOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete Assistant</DialogTitle>
<DialogDescription>
Are you sure you want to delete this assistant? This action
cannot be undone.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
variant="link"
onClick={() => setDeleteConfirmOpen(false)}
>
Cancel
</Button>
<Button variant="destructive" onClick={confirmDelete} autoFocus>
Delete
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div> </div>
</div> </div>
) )