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 {
Avatar,
Button,
cn,
Form,
@ -11,7 +10,6 @@ import {
ModalHeader,
Select,
SelectedItemProps,
SelectedItems,
SelectItem,
Textarea,
useDisclosure
@ -21,41 +19,21 @@ import ClaudeIcon from '@renderer/assets/images/models/claude.png'
import { getModelLogo } from '@renderer/config/models'
import { useAgents } from '@renderer/hooks/agents/useAgents'
import { useModels } from '@renderer/hooks/agents/useModels'
import { getProviderLabel } from '@renderer/i18n/label'
import { AddAgentForm, AgentEntity, AgentType, BaseAgentForm, isAgentType, UpdateAgentForm } from '@renderer/types'
import { ChangeEvent, FormEvent, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { ErrorBoundary } from '../../ErrorBoundary'
import { BaseOption, ModelOption, Option, renderOption } from './shared'
const logger = loggerService.withContext('AddAgentPopup')
interface BaseOption {
type: 'type' | 'model'
key: string
label: string
// img src
avatar: string
}
interface AgentTypeOption extends BaseOption {
type: 'type'
key: AgentEntity['type']
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
const buildAgentForm = (existing?: AgentEntity): BaseAgentForm => ({
@ -113,33 +91,6 @@ export const AgentModal: React.FC<Props> = ({ agent, trigger, isOpen: _isOpen, o
}
}, [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.
const agentConfig = useMemo(
() =>
@ -164,7 +115,7 @@ export const AgentModal: React.FC<Props> = ({ agent, trigger, isOpen: _isOpen, o
rendered: <Option option={option} />
}) as const satisfies SelectedItemProps
),
[Option, agentConfig]
[agentConfig]
)
const onAgentTypeChange = useCallback(

View File

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