mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-06 05:09:09 +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 {
|
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(
|
||||||
|
|||||||
@ -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) => ({
|
||||||
|
|||||||
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'
|
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(() => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user