refactor(agent): extract shared components and improve model handling

- Extract common option components to shared.tsx for reuse
- Make useModels filter parameter optional
- Update SessionModal to use real model data from API
This commit is contained in:
icarus 2025-09-20 00:48:17 +08:00
parent ae35d689ec
commit f5acddbfeb
4 changed files with 62 additions and 93 deletions

View File

@ -1,5 +1,4 @@
import { import {
Avatar,
Button, Button,
cn, cn,
Form, Form,
@ -11,7 +10,6 @@ import {
ModalHeader, ModalHeader,
Select, Select,
SelectedItemProps, SelectedItemProps,
SelectedItems,
SelectItem, SelectItem,
Textarea, Textarea,
useDisclosure useDisclosure
@ -21,41 +19,21 @@ import ClaudeIcon from '@renderer/assets/images/models/claude.png'
import { getModelLogo } from '@renderer/config/models' import { getModelLogo } from '@renderer/config/models'
import { useAgents } from '@renderer/hooks/agents/useAgents' import { useAgents } from '@renderer/hooks/agents/useAgents'
import { useModels } from '@renderer/hooks/agents/useModels' import { useModels } from '@renderer/hooks/agents/useModels'
import { getProviderLabel } from '@renderer/i18n/label'
import { AddAgentForm, AgentEntity, AgentType, BaseAgentForm, isAgentType, UpdateAgentForm } from '@renderer/types' import { AddAgentForm, AgentEntity, AgentType, BaseAgentForm, isAgentType, UpdateAgentForm } from '@renderer/types'
import { ChangeEvent, FormEvent, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { ChangeEvent, FormEvent, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { ErrorBoundary } from '../../ErrorBoundary' import { ErrorBoundary } from '../../ErrorBoundary'
import { BaseOption, ModelOption, Option, renderOption } from './shared'
const logger = loggerService.withContext('AddAgentPopup') const logger = loggerService.withContext('AddAgentPopup')
interface BaseOption {
type: 'type' | 'model'
key: string
label: string
// img src
avatar: string
}
interface AgentTypeOption extends BaseOption { interface AgentTypeOption extends BaseOption {
type: 'type' type: 'type'
key: AgentEntity['type'] key: AgentEntity['type']
name: AgentEntity['name'] name: AgentEntity['name']
} }
// function isAgentTypeOption(option: BaseOption): option is AgentTypeOption {
// return option.type === 'type'
// }
interface ModelOption extends BaseOption {
providerId?: string
}
function isModelOption(option: BaseOption): option is ModelOption {
return option.type === 'model'
}
type Option = AgentTypeOption | ModelOption type Option = AgentTypeOption | ModelOption
const buildAgentForm = (existing?: AgentEntity): BaseAgentForm => ({ const buildAgentForm = (existing?: AgentEntity): BaseAgentForm => ({
@ -113,33 +91,6 @@ export const AgentModal: React.FC<Props> = ({ agent, trigger, isOpen: _isOpen, o
} }
}, [agent, isOpen]) }, [agent, isOpen])
const Option = useCallback(
({ option }: { option?: Option | null }) => {
if (!option) {
return (
<div className="flex gap-2">
<Avatar name="?" className="h-5 w-5" />
{t('common.invalid_value')}
</div>
)
}
return (
<div className="flex gap-2">
<Avatar src={option.avatar} className="h-5 w-5" />
{option.label} {isModelOption(option) && option.providerId && `| ${getProviderLabel(option.providerId)}`}
</div>
)
},
[t]
)
const Item = useCallback(({ item }: { item: SelectedItemProps<Option> }) => <Option option={item.data} />, [Option])
const renderOption = useCallback(
(items: SelectedItems<Option>) => items.map((item) => <Item key={item.key} item={item} />),
[Item]
)
// add supported agents type here. // add supported agents type here.
const agentConfig = useMemo( const agentConfig = useMemo(
() => () =>
@ -164,7 +115,7 @@ export const AgentModal: React.FC<Props> = ({ agent, trigger, isOpen: _isOpen, o
rendered: <Option option={option} /> rendered: <Option option={option} />
}) as const satisfies SelectedItemProps }) as const satisfies SelectedItemProps
), ),
[Option, agentConfig] [agentConfig]
) )
const onAgentTypeChange = useCallback( const onAgentTypeChange = useCallback(

View File

@ -1,5 +1,4 @@
import { import {
Avatar,
Button, Button,
cn, cn,
Form, Form,
@ -17,30 +16,25 @@ import {
useDisclosure useDisclosure
} from '@heroui/react' } from '@heroui/react'
import { loggerService } from '@logger' import { loggerService } from '@logger'
import ClaudeIcon from '@renderer/assets/images/models/claude.png' import { getModelLogo } from '@renderer/config/models'
import { useModels } from '@renderer/hooks/agents/useModels'
import { useSessions } from '@renderer/hooks/agents/useSessions' import { useSessions } from '@renderer/hooks/agents/useSessions'
import { AgentEntity, AgentSessionEntity, BaseSessionForm, CreateSessionForm, UpdateSessionForm } from '@renderer/types' import { AgentEntity, AgentSessionEntity, BaseSessionForm, CreateSessionForm, UpdateSessionForm } from '@renderer/types'
import { ChangeEvent, FormEvent, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { ChangeEvent, FormEvent, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { ErrorBoundary } from '../../ErrorBoundary' import { ErrorBoundary } from '../../ErrorBoundary'
import { BaseOption, ModelOption, Option } from './shared'
const logger = loggerService.withContext('SessionAgentPopup') const logger = loggerService.withContext('SessionAgentPopup')
interface Option { type Option = ModelOption
key: string
label: string
// img src
avatar: string
}
type ModelOption = Option
const buildSessionForm = (existing?: AgentSessionEntity, agent?: AgentEntity): BaseSessionForm => ({ const buildSessionForm = (existing?: AgentSessionEntity, agent?: AgentEntity): BaseSessionForm => ({
name: existing?.name ?? agent?.name ?? 'Claude Code', name: existing?.name ?? agent?.name ?? 'Claude Code',
description: existing?.description ?? agent?.description, description: existing?.description ?? agent?.description,
instructions: existing?.instructions ?? agent?.instructions, instructions: existing?.instructions ?? agent?.instructions,
model: existing?.model ?? agent?.model ?? 'claude-4-sonnet', model: existing?.model ?? agent?.model ?? '',
accessible_paths: existing?.accessible_paths accessible_paths: existing?.accessible_paths
? [...existing.accessible_paths] ? [...existing.accessible_paths]
: agent?.accessible_paths : agent?.accessible_paths
@ -84,6 +78,8 @@ export const SessionModal: React.FC<Props> = ({ agentId, session, trigger, isOpe
const loadingRef = useRef(false) const loadingRef = useRef(false)
// const { setTimeoutTimer } = useTimer() // const { setTimeoutTimer } = useTimer()
const { createSession, updateSession } = useSessions(agentId) const { createSession, updateSession } = useSessions(agentId)
// Only support claude code for now
const { models } = useModels({ providerType: 'anthropic' })
const isEditing = (session?: AgentSessionEntity) => session !== undefined const isEditing = (session?: AgentSessionEntity) => session !== undefined
const [form, setForm] = useState<BaseSessionForm>(() => buildSessionForm(session)) const [form, setForm] = useState<BaseSessionForm>(() => buildSessionForm(session))
@ -94,30 +90,10 @@ export const SessionModal: React.FC<Props> = ({ agentId, session, trigger, isOpe
} }
}, [session, isOpen]) }, [session, isOpen])
const Option = useCallback( const Item = useCallback(({ item }: { item: SelectedItemProps<BaseOption> }) => <Option option={item.data} />, [])
({ option }: { option?: Option | null }) => {
if (!option) {
return (
<div className="flex gap-2">
<Avatar name="?" className="h-5 w-5" />
{t('common.invalid_value')}
</div>
)
}
return (
<div className="flex gap-2">
<Avatar src={option.avatar} className="h-5 w-5" />
{option.label}
</div>
)
},
[t]
)
const Item = useCallback(({ item }: { item: SelectedItemProps<Option> }) => <Option option={item.data} />, [Option])
const renderOption = useCallback( const renderOption = useCallback(
(items: SelectedItems<Option>) => items.map((item) => <Item key={item.key} item={item} />), (items: SelectedItems<BaseOption>) => items.map((item) => <Item key={item.key} item={item} />),
[Item] [Item]
) )
@ -144,14 +120,14 @@ export const SessionModal: React.FC<Props> = ({ agentId, session, trigger, isOpe
const modelOptions = useMemo(() => { const modelOptions = useMemo(() => {
// mocked data. not final version // mocked data. not final version
return [ return (models ?? []).map((model) => ({
{ type: 'model',
key: 'claude-4-sonnet', key: model.id,
label: 'Claude 4 Sonnet', label: model.name,
avatar: ClaudeIcon avatar: getModelLogo(model.id),
} providerId: model.provider
] satisfies ModelOption[] })) satisfies ModelOption[]
}, []) }, [models])
const onModelChange = useCallback((e: ChangeEvent<HTMLSelectElement>) => { const onModelChange = useCallback((e: ChangeEvent<HTMLSelectElement>) => {
setForm((prev) => ({ setForm((prev) => ({

View File

@ -0,0 +1,42 @@
import { Avatar, SelectedItemProps, SelectedItems } from '@heroui/react'
import { getProviderLabel } from '@renderer/i18n/label'
import { useTranslation } from 'react-i18next'
export interface BaseOption {
type: 'type' | 'model'
key: string
label: string
// img src
avatar: string
}
export interface ModelOption extends BaseOption {
providerId?: string
}
export function isModelOption(option: BaseOption): option is ModelOption {
return option.type === 'model'
}
export const Item = ({ item }: { item: SelectedItemProps<BaseOption> }) => <Option option={item.data} />
export const renderOption = (items: SelectedItems<BaseOption>) =>
items.map((item) => <Item key={item.key} item={item} />)
export const Option = ({ option }: { option?: BaseOption | null }) => {
const { t } = useTranslation()
if (!option) {
return (
<div className="flex gap-2">
<Avatar name="?" className="h-5 w-5" />
{t('common.invalid_value')}
</div>
)
}
return (
<div className="flex gap-2">
<Avatar src={option.avatar} className="h-5 w-5" />
{option.label} {isModelOption(option) && option.providerId && `| ${getProviderLabel(option.providerId)}`}
</div>
)
}

View File

@ -4,7 +4,7 @@ import useSWR from 'swr'
import { useAgentClient } from './useAgentClient' import { useAgentClient } from './useAgentClient'
export const useModels = (filter: ApiModelsFilter) => { export const useModels = (filter?: ApiModelsFilter) => {
const client = useAgentClient() const client = useAgentClient()
const path = client.getModelsPath(filter) const path = client.getModelsPath(filter)
const fetcher = useCallback(() => { const fetcher = useCallback(() => {