mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-24 10:40:07 +08:00
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:
parent
ae35d689ec
commit
f5acddbfeb
@ -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(
|
||||
|
||||
@ -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) => ({
|
||||
|
||||
42
src/renderer/src/components/Popups/agent/shared.tsx
Normal file
42
src/renderer/src/components/Popups/agent/shared.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@ -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(() => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user