mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-27 21:01:32 +08:00
Merge branch 'feat/agents-new' of https://github.com/CherryHQ/cherry-studio into feat/agents-new
This commit is contained in:
commit
36307abc30
@ -86,7 +86,8 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
},
|
||||
appendSystemPrompt: session.instructions,
|
||||
permissionMode: session.configuration?.permission_mode,
|
||||
maxTurns: session.configuration?.max_turns
|
||||
maxTurns: session.configuration?.max_turns,
|
||||
allowedTools: session.allowed_tools
|
||||
}
|
||||
|
||||
if (session.accessible_paths.length > 1) {
|
||||
|
||||
@ -166,7 +166,8 @@ const api = {
|
||||
openPath: (path: string) => ipcRenderer.invoke(IpcChannel.File_OpenPath, path),
|
||||
save: (path: string, content: string | NodeJS.ArrayBufferView, options?: any) =>
|
||||
ipcRenderer.invoke(IpcChannel.File_Save, path, content, options),
|
||||
selectFolder: (options?: OpenDialogOptions) => ipcRenderer.invoke(IpcChannel.File_SelectFolder, options),
|
||||
selectFolder: (options?: OpenDialogOptions): Promise<string | null> =>
|
||||
ipcRenderer.invoke(IpcChannel.File_SelectFolder, options),
|
||||
saveImage: (name: string, data: string) => ipcRenderer.invoke(IpcChannel.File_SaveImage, name, data),
|
||||
binaryImage: (fileId: string) => ipcRenderer.invoke(IpcChannel.File_BinaryImage, fileId),
|
||||
base64Image: (fileId: string): Promise<{ mime: string; base64: string; data: string }> =>
|
||||
|
||||
@ -11,12 +11,14 @@
|
||||
@import '../fonts/ubuntu/ubuntu.css';
|
||||
@import '../fonts/country-flag-fonts/flag.css';
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
/* margin: 0; */
|
||||
font-weight: normal;
|
||||
@layer base {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
/* margin: 0; */
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
*:focus {
|
||||
|
||||
19
src/renderer/src/components/ApiModelLabel.tsx
Normal file
19
src/renderer/src/components/ApiModelLabel.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import { Avatar, cn } from '@heroui/react'
|
||||
import { getModelLogo } from '@renderer/config/models'
|
||||
import { ApiModel } from '@renderer/types'
|
||||
import React from 'react'
|
||||
|
||||
export interface ModelLabelProps extends Omit<React.ComponentPropsWithRef<'div'>, 'children'> {
|
||||
model?: ApiModel
|
||||
}
|
||||
|
||||
export const ApiModelLabel: React.FC<ModelLabelProps> = ({ model, className, ...props }) => {
|
||||
return (
|
||||
<div className={cn('flex items-center gap-1', className)} {...props}>
|
||||
<Avatar src={model ? getModelLogo(model.id) : undefined} className="h-4 w-4" />
|
||||
<span>
|
||||
{model?.name} | {model?.provider_name}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -5,7 +5,7 @@ import { first } from 'lodash'
|
||||
import { FC } from 'react'
|
||||
|
||||
interface Props {
|
||||
model: Model
|
||||
model?: Model
|
||||
size: number
|
||||
props?: AvatarProps
|
||||
className?: string
|
||||
|
||||
@ -18,7 +18,7 @@ import { loggerService } from '@logger'
|
||||
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 { useApiModels } from '@renderer/hooks/agents/useModels'
|
||||
import { useUpdateAgent } from '@renderer/hooks/agents/useUpdateAgent'
|
||||
import { AddAgentForm, AgentEntity, AgentType, BaseAgentForm, isAgentType, UpdateAgentForm } from '@renderer/types'
|
||||
import { ChangeEvent, FormEvent, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
@ -82,7 +82,7 @@ export const AgentModal: React.FC<Props> = ({ agent, trigger, isOpen: _isOpen, o
|
||||
const { addAgent } = useAgents()
|
||||
const updateAgent = useUpdateAgent()
|
||||
// hard-coded. We only support anthropic for now.
|
||||
const { models } = useModels({ providerType: 'anthropic' })
|
||||
const { models } = useApiModels({ providerType: 'anthropic' })
|
||||
const isEditing = (agent?: AgentEntity) => agent !== undefined
|
||||
|
||||
const [form, setForm] = useState<BaseAgentForm>(() => buildAgentForm(agent))
|
||||
@ -317,7 +317,7 @@ export const AgentModal: React.FC<Props> = ({ agent, trigger, isOpen: _isOpen, o
|
||||
selectedKeys={[form.type]}
|
||||
onChange={onAgentTypeChange}
|
||||
items={agentOptions}
|
||||
label={t('agent.add.type.label')}
|
||||
label={t('agent.type.label')}
|
||||
placeholder={t('agent.add.type.placeholder')}
|
||||
renderValue={renderOption}>
|
||||
{(option) => (
|
||||
@ -327,7 +327,6 @@ export const AgentModal: React.FC<Props> = ({ agent, trigger, isOpen: _isOpen, o
|
||||
)}
|
||||
</Select>
|
||||
<Input isRequired value={form.name} onValueChange={onNameChange} label={t('common.name')} />
|
||||
{/* FIXME: Model type definition is string. It cannot be related to provider. Just mock a model now. */}
|
||||
<Select
|
||||
isRequired
|
||||
selectionMode="single"
|
||||
|
||||
@ -18,7 +18,7 @@ import {
|
||||
import { loggerService } from '@logger'
|
||||
import { getModelLogo } from '@renderer/config/models'
|
||||
import { useAgent } from '@renderer/hooks/agents/useAgent'
|
||||
import { useModels } from '@renderer/hooks/agents/useModels'
|
||||
import { useApiModels } 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'
|
||||
@ -80,7 +80,7 @@ export const SessionModal: React.FC<Props> = ({ agentId, session, trigger, isOpe
|
||||
// const { setTimeoutTimer } = useTimer()
|
||||
const { createSession, updateSession } = useSessions(agentId)
|
||||
// Only support claude code for now
|
||||
const { models } = useModels({ providerType: 'anthropic' })
|
||||
const { models } = useApiModels({ providerType: 'anthropic' })
|
||||
const { agent } = useAgent(agentId)
|
||||
const isEditing = (session?: AgentSessionEntity) => session !== undefined
|
||||
|
||||
|
||||
@ -34,7 +34,7 @@ interface MultipleSelectorProps<V> extends BaseSelectorProps<V> {
|
||||
onChange: (value: V[]) => void
|
||||
}
|
||||
|
||||
type SelectorProps<V> = SingleSelectorProps<V> | MultipleSelectorProps<V>
|
||||
export type SelectorProps<V> = SingleSelectorProps<V> | MultipleSelectorProps<V>
|
||||
|
||||
const Selector = <V extends string | number>({
|
||||
options,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import ClaudeAvatar from '@renderer/assets/images/models/claude.png'
|
||||
import { AgentBase, AgentEntity } from '@renderer/types'
|
||||
import { AgentBase, AgentType } from '@renderer/types'
|
||||
|
||||
// base agent config. no default config for now.
|
||||
const DEFAULT_AGENT_CONFIG: Omit<AgentBase, 'model'> = {
|
||||
@ -11,7 +11,7 @@ export const DEFAULT_CLAUDE_CODE_CONFIG: Omit<AgentBase, 'model'> = {
|
||||
...DEFAULT_AGENT_CONFIG
|
||||
} as const
|
||||
|
||||
export const getAgentAvatar = (type: AgentEntity['type']): string => {
|
||||
export const getAgentAvatar = (type: AgentType): string => {
|
||||
switch (type) {
|
||||
case 'claude-code':
|
||||
return ClaudeAvatar
|
||||
|
||||
@ -7,7 +7,7 @@ export const useAgent = (id: string | null) => {
|
||||
const client = useAgentClient()
|
||||
const key = id ? client.agentPaths.withId(id) : null
|
||||
const fetcher = useCallback(async () => {
|
||||
if (!id) {
|
||||
if (!id || id === 'fake') {
|
||||
return null
|
||||
}
|
||||
const result = await client.getAgent(id)
|
||||
|
||||
10
src/renderer/src/hooks/agents/useModel.ts
Normal file
10
src/renderer/src/hooks/agents/useModel.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { useApiModels } from './useModels'
|
||||
|
||||
export type UseModelProps = {
|
||||
id: string
|
||||
}
|
||||
|
||||
export const useApiModel = (id?: string) => {
|
||||
const { models } = useApiModels()
|
||||
return models.find((model) => model.id === id)
|
||||
}
|
||||
@ -1,18 +1,22 @@
|
||||
import { ApiModelsFilter } from '@renderer/types'
|
||||
import { merge } from 'lodash'
|
||||
import { useCallback } from 'react'
|
||||
import useSWR from 'swr'
|
||||
|
||||
import { useAgentClient } from './useAgentClient'
|
||||
|
||||
export const useModels = (filter?: ApiModelsFilter) => {
|
||||
export const useApiModels = (filter?: ApiModelsFilter) => {
|
||||
const client = useAgentClient()
|
||||
const path = client.getModelsPath(filter)
|
||||
// const defaultFilter = { limit: -1 } satisfies ApiModelsFilter
|
||||
const defaultFilter = {} satisfies ApiModelsFilter
|
||||
const finalFilter = merge(filter, defaultFilter)
|
||||
const path = client.getModelsPath(finalFilter)
|
||||
const fetcher = useCallback(() => {
|
||||
return client.getModels(filter)
|
||||
}, [client, filter])
|
||||
return client.getModels(finalFilter)
|
||||
}, [client, finalFilter])
|
||||
const { data, error, isLoading } = useSWR(path, fetcher)
|
||||
return {
|
||||
models: data?.data,
|
||||
models: data?.data ?? [],
|
||||
error,
|
||||
isLoading
|
||||
}
|
||||
|
||||
@ -5,7 +5,13 @@
|
||||
*/
|
||||
|
||||
import { loggerService } from '@logger'
|
||||
import { BuiltinMCPServerName, BuiltinMCPServerNames, BuiltinOcrProviderId, ThinkingOption } from '@renderer/types'
|
||||
import {
|
||||
AgentType,
|
||||
BuiltinMCPServerName,
|
||||
BuiltinMCPServerNames,
|
||||
BuiltinOcrProviderId,
|
||||
ThinkingOption
|
||||
} from '@renderer/types'
|
||||
|
||||
import i18n from './index'
|
||||
|
||||
@ -339,3 +345,12 @@ export const getBuiltinOcrProviderLabel = (key: BuiltinOcrProviderId) => {
|
||||
else if (key == 'paddleocr') return 'PaddleOCR'
|
||||
else return getLabel(builtinOcrProviderKeyMap, key)
|
||||
}
|
||||
|
||||
export const getAgentTypeLabel = (key: AgentType) => {
|
||||
switch (key) {
|
||||
case 'claude-code':
|
||||
return 'Claude Code'
|
||||
default:
|
||||
return 'Unknown Type'
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,7 +7,6 @@
|
||||
},
|
||||
"title": "Add Agent",
|
||||
"type": {
|
||||
"label": "Agent Type",
|
||||
"placeholder": "Select an agent type"
|
||||
}
|
||||
},
|
||||
@ -21,13 +20,25 @@
|
||||
"edit": {
|
||||
"title": "Edit Agent"
|
||||
},
|
||||
"get": {
|
||||
"error": {
|
||||
"failed": "Failed to get the agent."
|
||||
}
|
||||
},
|
||||
"list": {
|
||||
"error": {
|
||||
"failed": "Failed to list agents."
|
||||
}
|
||||
},
|
||||
"session": {
|
||||
"accessible_paths": {
|
||||
"add": "Add directory",
|
||||
"duplicate": "This directory is already included.",
|
||||
"empty": "Select at least one directory that the agent can access.",
|
||||
"error": {
|
||||
"at_least_one": "Please select at least one accessible directory."
|
||||
},
|
||||
"label": "Accessible directories",
|
||||
"required": "Please select at least one accessible directory.",
|
||||
"select_failed": "Failed to select directory."
|
||||
},
|
||||
"add": {
|
||||
@ -62,7 +73,12 @@
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"essential": "Essential Settings"
|
||||
"essential": "Essential Settings",
|
||||
"prompt": "Prompt Settings"
|
||||
},
|
||||
"type": {
|
||||
"label": "Agent Type",
|
||||
"unknown": "Unknown Type"
|
||||
},
|
||||
"update": {
|
||||
"error": {
|
||||
|
||||
@ -7,7 +7,6 @@
|
||||
},
|
||||
"title": "添加 Agent",
|
||||
"type": {
|
||||
"label": "Agent 类型",
|
||||
"placeholder": "选择 Agent 类型"
|
||||
}
|
||||
},
|
||||
@ -21,13 +20,25 @@
|
||||
"edit": {
|
||||
"title": "编辑 Agent"
|
||||
},
|
||||
"get": {
|
||||
"error": {
|
||||
"failed": "获取智能体失败"
|
||||
}
|
||||
},
|
||||
"list": {
|
||||
"error": {
|
||||
"failed": "获取智能体列表失败"
|
||||
}
|
||||
},
|
||||
"session": {
|
||||
"accessible_paths": {
|
||||
"add": "添加目录",
|
||||
"duplicate": "该目录已添加。",
|
||||
"empty": "请选择至少一个智能体可访问的目录。",
|
||||
"error": {
|
||||
"at_least_one": "请至少选择一个可访问的目录"
|
||||
},
|
||||
"label": "工作目录",
|
||||
"required": "请至少选择一个可访问的目录。",
|
||||
"select_failed": "选择目录失败"
|
||||
},
|
||||
"add": {
|
||||
@ -62,11 +73,16 @@
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"essential": "基础设置"
|
||||
"essential": "基础设置",
|
||||
"prompt": "提示词设置"
|
||||
},
|
||||
"type": {
|
||||
"label": "智能体类型",
|
||||
"unknown": "未知类型"
|
||||
},
|
||||
"update": {
|
||||
"error": {
|
||||
"failed": "更新 Agent 失败"
|
||||
"failed": "更新智能体失败"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -817,10 +833,10 @@
|
||||
"add": "添加",
|
||||
"add_success": "添加成功",
|
||||
"advanced_settings": "高级设置",
|
||||
"agent_one": "Agent",
|
||||
"agent_other": "Agents",
|
||||
"agent_one": "智能体",
|
||||
"agent_other": "智能体",
|
||||
"and": "和",
|
||||
"assistant": "智能体",
|
||||
"assistant": "助手",
|
||||
"assistant_one": "助手",
|
||||
"assistant_other": "助手",
|
||||
"avatar": "头像",
|
||||
|
||||
@ -7,7 +7,6 @@
|
||||
},
|
||||
"title": "新增代理",
|
||||
"type": {
|
||||
"label": "代理類型",
|
||||
"placeholder": "選擇 Agent 類型"
|
||||
}
|
||||
},
|
||||
@ -21,13 +20,25 @@
|
||||
"edit": {
|
||||
"title": "編輯 Agent"
|
||||
},
|
||||
"get": {
|
||||
"error": {
|
||||
"failed": "無法取得代理程式。"
|
||||
}
|
||||
},
|
||||
"list": {
|
||||
"error": {
|
||||
"failed": "無法列出代理程式。"
|
||||
}
|
||||
},
|
||||
"session": {
|
||||
"accessible_paths": {
|
||||
"add": "新增目錄",
|
||||
"duplicate": "此目錄已包含在內。",
|
||||
"empty": "選擇至少一個代理可以存取的目錄。",
|
||||
"error": {
|
||||
"at_least_one": "請至少選取一個可存取的目錄。"
|
||||
},
|
||||
"label": "可存取的目錄",
|
||||
"required": "請至少選擇一個可存取的目錄。",
|
||||
"select_failed": "無法選擇目錄。"
|
||||
},
|
||||
"add": {
|
||||
@ -62,7 +73,12 @@
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"essential": "必要設定"
|
||||
"essential": "必要設定",
|
||||
"prompt": "提示設定"
|
||||
},
|
||||
"type": {
|
||||
"label": "代理類型",
|
||||
"unknown": "未知類型"
|
||||
},
|
||||
"update": {
|
||||
"error": {
|
||||
|
||||
@ -7,7 +7,6 @@
|
||||
},
|
||||
"title": "Προσθήκη Agent",
|
||||
"type": {
|
||||
"label": "Τύπος πράκτορα",
|
||||
"placeholder": "Επιλέξτε τύπο Agent"
|
||||
}
|
||||
},
|
||||
@ -21,13 +20,25 @@
|
||||
"edit": {
|
||||
"title": "Επεξεργαστής Agent"
|
||||
},
|
||||
"get": {
|
||||
"error": {
|
||||
"failed": "[to be translated]:Failed to get the agent."
|
||||
}
|
||||
},
|
||||
"list": {
|
||||
"error": {
|
||||
"failed": "Αποτυχία καταχώρησης πρακτόρων."
|
||||
}
|
||||
},
|
||||
"session": {
|
||||
"accessible_paths": {
|
||||
"add": "Προσθήκη καταλόγου",
|
||||
"duplicate": "Αυτός ο κατάλογος έχει ήδη συμπεριληφθεί.",
|
||||
"empty": "Επιλέξτε τουλάχιστον έναν κατάλογο στον οποίο ο πράκτορας μπορεί να έχει πρόσβαση.",
|
||||
"error": {
|
||||
"at_least_one": "Παρακαλώ επιλέξτε τουλάχιστον έναν προσβάσιμο κατάλογο."
|
||||
},
|
||||
"label": "Προσβάσιμοι κατάλογοι",
|
||||
"required": "Παρακαλώ επιλέξτε τουλάχιστον έναν προσβάσιμο κατάλογο.",
|
||||
"select_failed": "Αποτυχία επιλογής καταλόγου."
|
||||
},
|
||||
"add": {
|
||||
@ -62,7 +73,12 @@
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"essential": "Βασικές Ρυθμίσεις"
|
||||
"essential": "Βασικές Ρυθμίσεις",
|
||||
"prompt": "Ρυθμίσεις Προτροπής"
|
||||
},
|
||||
"type": {
|
||||
"label": "Τύπος Πράκτορα",
|
||||
"unknown": "Άγνωστος Τύπος"
|
||||
},
|
||||
"update": {
|
||||
"error": {
|
||||
|
||||
@ -7,7 +7,6 @@
|
||||
},
|
||||
"title": "Agregar Agente",
|
||||
"type": {
|
||||
"label": "Tipo de agente",
|
||||
"placeholder": "Seleccionar tipo de Agente"
|
||||
}
|
||||
},
|
||||
@ -21,13 +20,25 @@
|
||||
"edit": {
|
||||
"title": "Agent de edición"
|
||||
},
|
||||
"get": {
|
||||
"error": {
|
||||
"failed": "No se pudo obtener el agente."
|
||||
}
|
||||
},
|
||||
"list": {
|
||||
"error": {
|
||||
"failed": "Error al listar agentes."
|
||||
}
|
||||
},
|
||||
"session": {
|
||||
"accessible_paths": {
|
||||
"add": "Agregar directorio",
|
||||
"duplicate": "Este directorio ya está incluido.",
|
||||
"empty": "Selecciona al menos un directorio al que el agente pueda acceder.",
|
||||
"error": {
|
||||
"at_least_one": "Por favor, seleccione al menos un directorio accesible."
|
||||
},
|
||||
"label": "Directorios accesibles",
|
||||
"required": "Por favor, seleccione al menos un directorio accesible.",
|
||||
"select_failed": "Error al seleccionar el directorio."
|
||||
},
|
||||
"add": {
|
||||
@ -62,7 +73,12 @@
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"essential": "Configuraciones esenciales"
|
||||
"essential": "Configuraciones esenciales",
|
||||
"prompt": "Configuración de indicaciones"
|
||||
},
|
||||
"type": {
|
||||
"label": "Tipo de Agente",
|
||||
"unknown": "Tipo desconocido"
|
||||
},
|
||||
"update": {
|
||||
"error": {
|
||||
|
||||
@ -7,7 +7,6 @@
|
||||
},
|
||||
"title": "Ajouter un agent",
|
||||
"type": {
|
||||
"label": "Type d'agent",
|
||||
"placeholder": "Sélectionner le type d'Agent"
|
||||
}
|
||||
},
|
||||
@ -21,13 +20,25 @@
|
||||
"edit": {
|
||||
"title": "Éditer Agent"
|
||||
},
|
||||
"get": {
|
||||
"error": {
|
||||
"failed": "Échec de l'obtention de l'agent."
|
||||
}
|
||||
},
|
||||
"list": {
|
||||
"error": {
|
||||
"failed": "[to be translated]:Failed to list agents."
|
||||
}
|
||||
},
|
||||
"session": {
|
||||
"accessible_paths": {
|
||||
"add": "Ajouter un répertoire",
|
||||
"duplicate": "Ce répertoire est déjà inclus.",
|
||||
"empty": "Sélectionnez au moins un répertoire auquel l'agent peut accéder.",
|
||||
"error": {
|
||||
"at_least_one": "Veuillez sélectionner au moins un répertoire accessible."
|
||||
},
|
||||
"label": "Répertoires accessibles",
|
||||
"required": "Veuillez sélectionner au moins un répertoire accessible.",
|
||||
"select_failed": "Échec de la sélection du répertoire."
|
||||
},
|
||||
"add": {
|
||||
@ -62,7 +73,12 @@
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"essential": "Paramètres essentiels"
|
||||
"essential": "Paramètres essentiels",
|
||||
"prompt": "Paramètres de l'invite"
|
||||
},
|
||||
"type": {
|
||||
"label": "Type d'agent",
|
||||
"unknown": "Type inconnu"
|
||||
},
|
||||
"update": {
|
||||
"error": {
|
||||
|
||||
@ -7,7 +7,6 @@
|
||||
},
|
||||
"title": "エージェントを追加",
|
||||
"type": {
|
||||
"label": "エージェントタイプ",
|
||||
"placeholder": "エージェントタイプを選択"
|
||||
}
|
||||
},
|
||||
@ -21,13 +20,25 @@
|
||||
"edit": {
|
||||
"title": "編集エージェント"
|
||||
},
|
||||
"get": {
|
||||
"error": {
|
||||
"failed": "エージェントの取得に失敗しました。"
|
||||
}
|
||||
},
|
||||
"list": {
|
||||
"error": {
|
||||
"failed": "エージェントの一覧取得に失敗しました。"
|
||||
}
|
||||
},
|
||||
"session": {
|
||||
"accessible_paths": {
|
||||
"add": "ディレクトリを追加",
|
||||
"duplicate": "このディレクトリは既に含まれています。",
|
||||
"empty": "エージェントがアクセスできるディレクトリを少なくとも1つ選択してください。",
|
||||
"error": {
|
||||
"at_least_one": "アクセス可能なディレクトリを少なくとも1つ選択してください。"
|
||||
},
|
||||
"label": "アクセス可能なディレクトリ",
|
||||
"required": "アクセス可能なディレクトリを少なくとも1つ選択してください。",
|
||||
"select_failed": "ディレクトリの選択に失敗しました。"
|
||||
},
|
||||
"add": {
|
||||
@ -62,7 +73,12 @@
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"essential": "必須設定"
|
||||
"essential": "必須設定",
|
||||
"prompt": "プロンプト設定"
|
||||
},
|
||||
"type": {
|
||||
"label": "エージェントタイプ",
|
||||
"unknown": "不明なタイプ"
|
||||
},
|
||||
"update": {
|
||||
"error": {
|
||||
|
||||
@ -7,7 +7,6 @@
|
||||
},
|
||||
"title": "Adicionar Agente",
|
||||
"type": {
|
||||
"label": "Tipo de Agente",
|
||||
"placeholder": "Selecionar tipo de Agente"
|
||||
}
|
||||
},
|
||||
@ -21,13 +20,25 @@
|
||||
"edit": {
|
||||
"title": "Agent Editor"
|
||||
},
|
||||
"get": {
|
||||
"error": {
|
||||
"failed": "Falha ao obter o agente."
|
||||
}
|
||||
},
|
||||
"list": {
|
||||
"error": {
|
||||
"failed": "[to be translated]:Failed to list agents."
|
||||
}
|
||||
},
|
||||
"session": {
|
||||
"accessible_paths": {
|
||||
"add": "Adicionar diretório",
|
||||
"duplicate": "Este diretório já está incluído.",
|
||||
"empty": "Selecione pelo menos um diretório ao qual o agente possa acessar.",
|
||||
"error": {
|
||||
"at_least_one": "Por favor, selecione pelo menos um diretório acessível."
|
||||
},
|
||||
"label": "Diretórios acessíveis",
|
||||
"required": "Por favor, selecione pelo menos um diretório acessível.",
|
||||
"select_failed": "Falha ao selecionar o diretório."
|
||||
},
|
||||
"add": {
|
||||
@ -62,7 +73,12 @@
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"essential": "Configurações Essenciais"
|
||||
"essential": "Configurações Essenciais",
|
||||
"prompt": "Configurações de Prompt"
|
||||
},
|
||||
"type": {
|
||||
"label": "Tipo de Agente",
|
||||
"unknown": "Tipo Desconhecido"
|
||||
},
|
||||
"update": {
|
||||
"error": {
|
||||
|
||||
@ -7,7 +7,6 @@
|
||||
},
|
||||
"title": "Добавить агента",
|
||||
"type": {
|
||||
"label": "Тип агента",
|
||||
"placeholder": "Выбор типа агента"
|
||||
}
|
||||
},
|
||||
@ -21,13 +20,25 @@
|
||||
"edit": {
|
||||
"title": "Редактировать агент"
|
||||
},
|
||||
"get": {
|
||||
"error": {
|
||||
"failed": "Не удалось получить агента."
|
||||
}
|
||||
},
|
||||
"list": {
|
||||
"error": {
|
||||
"failed": "Не удалось получить список агентов."
|
||||
}
|
||||
},
|
||||
"session": {
|
||||
"accessible_paths": {
|
||||
"add": "Добавить каталог",
|
||||
"duplicate": "Этот каталог уже включён.",
|
||||
"empty": "Выберите хотя бы один каталог, к которому агент имеет доступ.",
|
||||
"error": {
|
||||
"at_least_one": "Пожалуйста, выберите хотя бы один доступный каталог."
|
||||
},
|
||||
"label": "Доступные директории",
|
||||
"required": "Пожалуйста, выберите хотя бы один доступный каталог.",
|
||||
"select_failed": "Не удалось выбрать каталог."
|
||||
},
|
||||
"add": {
|
||||
@ -62,7 +73,12 @@
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"essential": "Основные настройки"
|
||||
"essential": "Основные настройки",
|
||||
"prompt": "Настройки подсказки"
|
||||
},
|
||||
"type": {
|
||||
"label": "Тип агента",
|
||||
"unknown": "Неизвестный тип"
|
||||
},
|
||||
"update": {
|
||||
"error": {
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
import { ApiModelLabel } from '@renderer/components/ApiModelLabel'
|
||||
import { NavbarHeader } from '@renderer/components/app/Navbar'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import SearchPopup from '@renderer/components/Popups/SearchPopup'
|
||||
import { useAgent } from '@renderer/hooks/agents/useAgent'
|
||||
import { useApiModel } from '@renderer/hooks/agents/useModel'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { modelGenerating } from '@renderer/hooks/useRuntime'
|
||||
import { modelGenerating, useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useShortcut } from '@renderer/hooks/useShortcuts'
|
||||
import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore'
|
||||
@ -35,6 +38,10 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTo
|
||||
const { topicPosition, narrowMode } = useSettings()
|
||||
const { showTopics, toggleShowTopics } = useShowTopics()
|
||||
const dispatch = useAppDispatch()
|
||||
const { chat } = useRuntime()
|
||||
const { activeTopicOrSession, activeAgentId } = chat
|
||||
const { agent } = useAgent(activeAgentId)
|
||||
const agentModel = useApiModel(agent?.model)
|
||||
|
||||
useShortcut('toggle_show_assistants', toggleShowAssistants)
|
||||
|
||||
@ -94,7 +101,10 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTo
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<SelectModelButton assistant={assistant} />
|
||||
{activeTopicOrSession === 'topic' && <SelectModelButton assistant={assistant} />}
|
||||
{/* TODO: Show a select model button for agent. */}
|
||||
{/* FIXME: models endpoint doesn't return all models, so cannot found. */}
|
||||
{activeTopicOrSession === 'session' && <ApiModelLabel model={agentModel} />}
|
||||
</HStack>
|
||||
<HStack alignItems="center" gap={8}>
|
||||
<UpdateAppButton />
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Button, Spinner } from '@heroui/react'
|
||||
import { Alert, Button, Spinner } from '@heroui/react'
|
||||
import { AgentModal } from '@renderer/components/Popups/agent/AgentModal'
|
||||
import { useAgents } from '@renderer/hooks/agents/useAgents'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
@ -14,7 +14,7 @@ import { SectionName } from './SectionName'
|
||||
interface AssistantsTabProps {}
|
||||
|
||||
export const Agents: FC<AssistantsTabProps> = () => {
|
||||
const { agents, deleteAgent, isLoading } = useAgents()
|
||||
const { agents, deleteAgent, isLoading, error } = useAgents()
|
||||
const { t } = useTranslation()
|
||||
const { chat } = useRuntime()
|
||||
const { activeAgentId } = chat
|
||||
@ -38,7 +38,9 @@ export const Agents: FC<AssistantsTabProps> = () => {
|
||||
<div className="agents-tab h-full w-full">
|
||||
<SectionName name={t('common.agent_other')} />
|
||||
{isLoading && <Spinner />}
|
||||
{error && <Alert color="danger" title={t('agent.list.error.failed')} />}
|
||||
{!isLoading &&
|
||||
!error &&
|
||||
agents.map((agent) => (
|
||||
<AgentItem
|
||||
key={agent.id}
|
||||
|
||||
@ -60,7 +60,6 @@ const Sessions: React.FC<SessionsProps> = ({ agentId }) => {
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="agents-tab h-full w-full p-2">
|
||||
{/* TODO: Add session button */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
|
||||
@ -1,18 +1,18 @@
|
||||
import CodeEditor from '@renderer/components/CodeEditor'
|
||||
import { Box, HSpaceBetweenStack, HStack } from '@renderer/components/Layout'
|
||||
import { RichEditorRef } from '@renderer/components/RichEditor/types'
|
||||
import { Button, Tooltip } from '@heroui/react'
|
||||
import { loggerService } from '@logger'
|
||||
import { ApiModelLabel } from '@renderer/components/ApiModelLabel'
|
||||
import { useApiModels } from '@renderer/hooks/agents/useModels'
|
||||
import { useUpdateAgent } from '@renderer/hooks/agents/useUpdateAgent'
|
||||
import { usePromptProcessor } from '@renderer/hooks/usePromptProcessor'
|
||||
import { estimateTextTokens } from '@renderer/services/TokenService'
|
||||
import { AgentEntity, UpdateAgentForm } from '@renderer/types'
|
||||
import { Button, Input, Popover } from 'antd'
|
||||
import { Edit, HelpCircle, Save } from 'lucide-react'
|
||||
import { FC, useEffect, useRef, useState } from 'react'
|
||||
import { Input, Select } from 'antd'
|
||||
import { DefaultOptionType } from 'antd/es/select'
|
||||
import { Plus } from 'lucide-react'
|
||||
import { FC, useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { SettingDivider } from '..'
|
||||
import { AgentLabel, SettingsContainer, SettingsItem, SettingsTitle } from './shared'
|
||||
|
||||
const logger = loggerService.withContext('AgentEssentialSettings')
|
||||
|
||||
interface AgentEssentialSettingsProps {
|
||||
agent: AgentEntity | undefined | null
|
||||
@ -22,157 +22,127 @@ interface AgentEssentialSettingsProps {
|
||||
const AgentEssentialSettings: FC<AgentEssentialSettingsProps> = ({ agent, update }) => {
|
||||
const { t } = useTranslation()
|
||||
const [name, setName] = useState<string>((agent?.name ?? '').trim())
|
||||
const [instructions, setInstructions] = useState<string>(agent?.instructions ?? '')
|
||||
const [showPreview, setShowPreview] = useState<boolean>(!!agent?.instructions?.length)
|
||||
const [tokenCount, setTokenCount] = useState(0)
|
||||
const { models } = useApiModels({ providerType: 'anthropic' })
|
||||
|
||||
useEffect(() => {
|
||||
const updateTokenCount = async () => {
|
||||
const count = estimateTextTokens(instructions)
|
||||
setTokenCount(count)
|
||||
}
|
||||
updateTokenCount()
|
||||
}, [instructions])
|
||||
|
||||
const editorRef = useRef<RichEditorRef>(null)
|
||||
|
||||
const processedPrompt = usePromptProcessor({
|
||||
prompt: instructions,
|
||||
modelName: agent?.model
|
||||
})
|
||||
|
||||
const onUpdate = () => {
|
||||
const updateName = (name: string) => {
|
||||
if (!agent) return
|
||||
const _agent = { ...agent, type: undefined, name: name.trim(), instructions } satisfies UpdateAgentForm
|
||||
update(_agent)
|
||||
update({ id: agent.id, name: name.trim() })
|
||||
}
|
||||
|
||||
const promptVarsContent = <pre>{t('agents.add.prompt.variables.tip.content')}</pre>
|
||||
const updateModel = (model: UpdateAgentForm['model']) => {
|
||||
if (!agent) return
|
||||
update({ id: agent.id, model })
|
||||
}
|
||||
|
||||
const updateAccessiblePaths = useCallback(
|
||||
(accessible_paths: UpdateAgentForm['accessible_paths']) => {
|
||||
if (!agent) return
|
||||
update({ id: agent.id, accessible_paths })
|
||||
},
|
||||
[agent, update]
|
||||
)
|
||||
|
||||
const modelOptions = useMemo(() => {
|
||||
return models.map((model) => ({
|
||||
value: model.id,
|
||||
label: <ApiModelLabel model={model} />
|
||||
})) satisfies DefaultOptionType[]
|
||||
}, [models])
|
||||
|
||||
const addAccessiblePath = useCallback(async () => {
|
||||
if (!agent) return
|
||||
|
||||
try {
|
||||
const selected = await window.api.file.selectFolder()
|
||||
if (!selected) {
|
||||
return
|
||||
}
|
||||
|
||||
if (agent.accessible_paths.includes(selected)) {
|
||||
window.toast.warning(t('agent.session.accessible_paths.duplicate'))
|
||||
return
|
||||
}
|
||||
|
||||
updateAccessiblePaths([...agent.accessible_paths, selected])
|
||||
} catch (error) {
|
||||
logger.error('Failed to select accessible path:', error as Error)
|
||||
window.toast.error(t('agent.session.accessible_paths.select_failed'))
|
||||
}
|
||||
}, [agent, t, updateAccessiblePaths])
|
||||
|
||||
const removeAccessiblePath = useCallback(
|
||||
(path: string) => {
|
||||
if (!agent) return
|
||||
const newPaths = agent.accessible_paths.filter((p) => p !== path)
|
||||
if (newPaths.length === 0) {
|
||||
window.toast.error(t('agent.session.accessible_paths.error.at_least_one'))
|
||||
return
|
||||
}
|
||||
updateAccessiblePaths(newPaths)
|
||||
},
|
||||
[agent, t, updateAccessiblePaths]
|
||||
)
|
||||
|
||||
if (!agent) return null
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Box mb={8} style={{ fontWeight: 'bold' }}>
|
||||
{t('common.name')}
|
||||
</Box>
|
||||
<HStack gap={8} alignItems="center">
|
||||
<SettingsContainer>
|
||||
<SettingsItem inline>
|
||||
<SettingsTitle>{t('agent.type.label')}</SettingsTitle>
|
||||
<AgentLabel type={agent.type} />
|
||||
</SettingsItem>
|
||||
<SettingsItem inline>
|
||||
<SettingsTitle>{t('common.name')}</SettingsTitle>
|
||||
<Input
|
||||
placeholder={t('common.assistant') + t('common.name')}
|
||||
placeholder={t('common.agent_one') + t('common.name')}
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
onBlur={() => {
|
||||
if (name !== agent.name) {
|
||||
onUpdate()
|
||||
updateName(name)
|
||||
}
|
||||
}}
|
||||
style={{ flex: 1 }}
|
||||
className="max-w-80 flex-1"
|
||||
/>
|
||||
</HStack>
|
||||
<SettingDivider />
|
||||
<HStack mb={8} alignItems="center" gap={4}>
|
||||
<Box style={{ fontWeight: 'bold' }}>{t('common.prompt')}</Box>
|
||||
<Popover title={t('agents.add.prompt.variables.tip.title')} content={promptVarsContent}>
|
||||
<HelpCircle size={14} color="var(--color-text-2)" />
|
||||
</Popover>
|
||||
</HStack>
|
||||
<TextAreaContainer>
|
||||
<RichEditorContainer>
|
||||
{showPreview ? (
|
||||
<MarkdownContainer
|
||||
onDoubleClick={() => {
|
||||
const currentScrollTop = editorRef.current?.getScrollTop?.() || 0
|
||||
setShowPreview(false)
|
||||
requestAnimationFrame(() => editorRef.current?.setScrollTop?.(currentScrollTop))
|
||||
}}>
|
||||
<ReactMarkdown>{processedPrompt || instructions}</ReactMarkdown>
|
||||
</MarkdownContainer>
|
||||
) : (
|
||||
<CodeEditor
|
||||
value={instructions}
|
||||
language="markdown"
|
||||
onChange={setInstructions}
|
||||
height="100%"
|
||||
expanded={false}
|
||||
style={{
|
||||
height: '100%'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</RichEditorContainer>
|
||||
</TextAreaContainer>
|
||||
<HSpaceBetweenStack width="100%" justifyContent="flex-end" mt="10px">
|
||||
<TokenCount>Tokens: {tokenCount}</TokenCount>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={showPreview ? <Edit size={14} /> : <Save size={14} />}
|
||||
onClick={() => {
|
||||
const currentScrollTop = editorRef.current?.getScrollTop?.() || 0
|
||||
if (showPreview) {
|
||||
setShowPreview(false)
|
||||
requestAnimationFrame(() => editorRef.current?.setScrollTop?.(currentScrollTop))
|
||||
} else {
|
||||
onUpdate()
|
||||
requestAnimationFrame(() => {
|
||||
setShowPreview(true)
|
||||
requestAnimationFrame(() => editorRef.current?.setScrollTop?.(currentScrollTop))
|
||||
})
|
||||
}
|
||||
}}>
|
||||
{showPreview ? t('common.edit') : t('common.save')}
|
||||
</Button>
|
||||
</HSpaceBetweenStack>
|
||||
</Container>
|
||||
</SettingsItem>
|
||||
<SettingsItem inline className="gap-8">
|
||||
<SettingsTitle>{t('common.model')}</SettingsTitle>
|
||||
<Select
|
||||
options={modelOptions}
|
||||
value={agent.model}
|
||||
onChange={(value) => {
|
||||
updateModel(value)
|
||||
}}
|
||||
className="max-w-80 flex-1"
|
||||
placeholder={t('common.placeholders.select.model')}
|
||||
/>
|
||||
</SettingsItem>
|
||||
<SettingsItem>
|
||||
<SettingsTitle
|
||||
actions={
|
||||
<Tooltip content={t('agent.session.accessible_paths.add')}>
|
||||
<Button size="sm" startContent={<Plus />} isIconOnly onPress={addAccessiblePath} />
|
||||
</Tooltip>
|
||||
}>
|
||||
{t('agent.session.accessible_paths.label')}
|
||||
</SettingsTitle>
|
||||
<ul className="mt-2 flex flex-col gap-2 rounded-xl border p-2">
|
||||
{agent.accessible_paths.map((path) => (
|
||||
<li
|
||||
key={path}
|
||||
className="flex items-center justify-between gap-2 rounded-medium border border-default-200 px-3 py-2">
|
||||
<span className="truncate text-sm" title={path}>
|
||||
{path}
|
||||
</span>
|
||||
<Button size="sm" variant="light" color="danger" onPress={() => removeAccessiblePath(path)}>
|
||||
{t('common.delete')}
|
||||
</Button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</SettingsItem>
|
||||
</SettingsContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
`
|
||||
|
||||
const TextAreaContainer = styled.div`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
const TokenCount = styled.div`
|
||||
padding: 2px 2px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
color: var(--color-text-2);
|
||||
user-select: none;
|
||||
`
|
||||
|
||||
const RichEditorContainer = styled.div`
|
||||
height: calc(80vh - 202px);
|
||||
border: 0.5px solid var(--color-border);
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
|
||||
.prompt-rich-editor {
|
||||
border: none;
|
||||
height: 100%;
|
||||
|
||||
.rich-editor-wrapper {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.rich-editor-content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const MarkdownContainer = styled.div.attrs({ className: 'markdown' })`
|
||||
height: 100%;
|
||||
padding: 0.5em;
|
||||
overflow: auto;
|
||||
`
|
||||
|
||||
export default AgentEssentialSettings
|
||||
|
||||
@ -0,0 +1,157 @@
|
||||
import CodeEditor from '@renderer/components/CodeEditor'
|
||||
import { HSpaceBetweenStack } from '@renderer/components/Layout'
|
||||
import { RichEditorRef } from '@renderer/components/RichEditor/types'
|
||||
import { useUpdateAgent } from '@renderer/hooks/agents/useUpdateAgent'
|
||||
import { usePromptProcessor } from '@renderer/hooks/usePromptProcessor'
|
||||
import { estimateTextTokens } from '@renderer/services/TokenService'
|
||||
import { AgentEntity, UpdateAgentForm } from '@renderer/types'
|
||||
import { Button, Popover } from 'antd'
|
||||
import { Edit, HelpCircle, Save } from 'lucide-react'
|
||||
import { FC, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { SettingsContainer, SettingsItem, SettingsTitle } from './shared'
|
||||
|
||||
interface AgentPromptSettingsProps {
|
||||
agent: AgentEntity | undefined | null
|
||||
update: ReturnType<typeof useUpdateAgent>
|
||||
}
|
||||
|
||||
const AgentPromptSettings: FC<AgentPromptSettingsProps> = ({ agent, update }) => {
|
||||
const { t } = useTranslation()
|
||||
const [instructions, setInstructions] = useState<string>(agent?.instructions ?? '')
|
||||
const [showPreview, setShowPreview] = useState<boolean>(!!agent?.instructions?.length)
|
||||
const [tokenCount, setTokenCount] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
const updateTokenCount = async () => {
|
||||
const count = estimateTextTokens(instructions)
|
||||
setTokenCount(count)
|
||||
}
|
||||
updateTokenCount()
|
||||
}, [instructions])
|
||||
|
||||
const editorRef = useRef<RichEditorRef>(null)
|
||||
|
||||
const processedPrompt = usePromptProcessor({
|
||||
prompt: instructions,
|
||||
modelName: agent?.model
|
||||
})
|
||||
|
||||
const onUpdate = () => {
|
||||
if (!agent) return
|
||||
const _agent = { ...agent, type: undefined, instructions } satisfies UpdateAgentForm
|
||||
update(_agent)
|
||||
}
|
||||
|
||||
const promptVarsContent = <pre>{t('agents.add.prompt.variables.tip.content')}</pre>
|
||||
|
||||
if (!agent) return null
|
||||
|
||||
return (
|
||||
<SettingsContainer>
|
||||
<SettingsItem divider={false} className="flex-1">
|
||||
<SettingsTitle>
|
||||
{t('common.prompt')}
|
||||
<Popover title={t('agents.add.prompt.variables.tip.title')} content={promptVarsContent}>
|
||||
<HelpCircle size={14} color="var(--color-text-2)" />
|
||||
</Popover>
|
||||
</SettingsTitle>
|
||||
<TextAreaContainer>
|
||||
<RichEditorContainer>
|
||||
{showPreview ? (
|
||||
<MarkdownContainer
|
||||
onDoubleClick={() => {
|
||||
const currentScrollTop = editorRef.current?.getScrollTop?.() || 0
|
||||
setShowPreview(false)
|
||||
requestAnimationFrame(() => editorRef.current?.setScrollTop?.(currentScrollTop))
|
||||
}}>
|
||||
<ReactMarkdown>{processedPrompt || instructions}</ReactMarkdown>
|
||||
</MarkdownContainer>
|
||||
) : (
|
||||
<CodeEditor
|
||||
value={instructions}
|
||||
language="markdown"
|
||||
onChange={setInstructions}
|
||||
height="100%"
|
||||
expanded={false}
|
||||
style={{
|
||||
height: '100%'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</RichEditorContainer>
|
||||
</TextAreaContainer>
|
||||
<HSpaceBetweenStack width="100%" justifyContent="flex-end" mt="10px">
|
||||
<TokenCount>Tokens: {tokenCount}</TokenCount>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={showPreview ? <Edit size={14} /> : <Save size={14} />}
|
||||
onClick={() => {
|
||||
const currentScrollTop = editorRef.current?.getScrollTop?.() || 0
|
||||
if (showPreview) {
|
||||
setShowPreview(false)
|
||||
requestAnimationFrame(() => editorRef.current?.setScrollTop?.(currentScrollTop))
|
||||
} else {
|
||||
onUpdate()
|
||||
requestAnimationFrame(() => {
|
||||
setShowPreview(true)
|
||||
requestAnimationFrame(() => editorRef.current?.setScrollTop?.(currentScrollTop))
|
||||
})
|
||||
}
|
||||
}}>
|
||||
{showPreview ? t('common.edit') : t('common.save')}
|
||||
</Button>
|
||||
</HSpaceBetweenStack>
|
||||
</SettingsItem>
|
||||
</SettingsContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const TextAreaContainer = styled.div`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
`
|
||||
|
||||
const TokenCount = styled.div`
|
||||
padding: 2px 2px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
color: var(--color-text-2);
|
||||
user-select: none;
|
||||
`
|
||||
|
||||
const RichEditorContainer = styled.div`
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
border: 0.5px solid var(--color-border);
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
|
||||
.prompt-rich-editor {
|
||||
border: none;
|
||||
height: 100%;
|
||||
|
||||
.rich-editor-wrapper {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.rich-editor-content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const MarkdownContainer = styled.div.attrs({ className: 'markdown' })`
|
||||
height: 100%;
|
||||
padding: 0.5em;
|
||||
overflow: auto;
|
||||
`
|
||||
|
||||
export default AgentPromptSettings
|
||||
@ -1,7 +1,5 @@
|
||||
import { Avatar } from '@heroui/react'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import { Alert, Spinner } from '@heroui/react'
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import { getAgentAvatar } from '@renderer/config/agent'
|
||||
import { useAgent } from '@renderer/hooks/agents/useAgent'
|
||||
import { useUpdateAgent } from '@renderer/hooks/agents/useUpdateAgent'
|
||||
import { Menu, Modal } from 'antd'
|
||||
@ -10,6 +8,8 @@ import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import AgentEssentialSettings from './AgentEssentialSettings'
|
||||
import AgentPromptSettings from './AgentPromptSettings'
|
||||
import { AgentLabel } from './shared'
|
||||
|
||||
interface AgentSettingPopupShowParams {
|
||||
agentId: string
|
||||
@ -27,7 +27,7 @@ const AgentSettingPopupContainer: React.FC<AgentSettingPopupParams> = ({ tab, ag
|
||||
const { t } = useTranslation()
|
||||
const [menu, setMenu] = useState<AgentSettingPopupTab>(tab || 'essential')
|
||||
|
||||
const { agent } = useAgent(agentId)
|
||||
const { agent, isLoading, error } = useAgent(agentId)
|
||||
const updateAgent = useUpdateAgent()
|
||||
|
||||
const onOk = () => {
|
||||
@ -47,9 +47,44 @@ const AgentSettingPopupContainer: React.FC<AgentSettingPopupParams> = ({ tab, ag
|
||||
{
|
||||
key: 'essential',
|
||||
label: t('agent.settings.essential')
|
||||
},
|
||||
{
|
||||
key: 'prompt',
|
||||
label: t('agent.settings.prompt')
|
||||
}
|
||||
] satisfies { key: AgentSettingPopupTab; label: string }[]
|
||||
).filter(Boolean) as { key: string; label: string }[]
|
||||
] as const satisfies { key: AgentSettingPopupTab; label: string }[]
|
||||
).filter(Boolean)
|
||||
|
||||
const ModalContent = () => {
|
||||
if (isLoading) {
|
||||
// TODO: use skeleton for better ux
|
||||
return <Spinner />
|
||||
}
|
||||
if (error) {
|
||||
return (
|
||||
<div>
|
||||
<Alert color="danger" title={t('agent.get.error.failed')} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className="flex w-full flex-1">
|
||||
<LeftMenu>
|
||||
<StyledMenu
|
||||
defaultSelectedKeys={[tab || 'essential'] satisfies AgentSettingPopupTab[]}
|
||||
mode="vertical"
|
||||
selectedKeys={[menu]}
|
||||
items={items}
|
||||
onSelect={({ key }) => setMenu(key as AgentSettingPopupTab)}
|
||||
/>
|
||||
</LeftMenu>
|
||||
<Settings>
|
||||
{menu === 'essential' && <AgentEssentialSettings agent={agent} update={updateAgent} />}
|
||||
{menu === 'prompt' && <AgentPromptSettings agent={agent} update={updateAgent} />}
|
||||
</Settings>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledModal
|
||||
@ -60,50 +95,46 @@ const AgentSettingPopupContainer: React.FC<AgentSettingPopupParams> = ({ tab, ag
|
||||
maskClosable={false}
|
||||
footer={null}
|
||||
title={
|
||||
<div className="flex items-center">
|
||||
<Avatar size="sm" className="mr-2 h-5 w-5" src={agent ? getAgentAvatar(agent.type) : undefined} />
|
||||
<span className="font-extrabold text-xl">{agent?.name ?? ''}</span>
|
||||
</div>
|
||||
<AgentLabel
|
||||
type={agent?.type ?? 'claude-code'}
|
||||
name={agent?.name}
|
||||
classNames={{ name: 'text-lg font-extrabold' }}
|
||||
avatarProps={{ size: 'sm' }}
|
||||
/>
|
||||
}
|
||||
transitionName="animation-move-down"
|
||||
styles={{
|
||||
content: {
|
||||
padding: 0,
|
||||
overflow: 'hidden'
|
||||
overflow: 'hidden',
|
||||
height: '80vh',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
},
|
||||
header: { padding: '10px 15px', borderBottom: '0.5px solid var(--color-border)', margin: 0, borderRadius: 0 },
|
||||
body: {
|
||||
padding: 0
|
||||
padding: 0,
|
||||
display: 'flex',
|
||||
flex: 1
|
||||
}
|
||||
}}
|
||||
width="min(800px, 70vw)"
|
||||
height="80vh"
|
||||
centered>
|
||||
<HStack>
|
||||
<LeftMenu>
|
||||
<StyledMenu
|
||||
defaultSelectedKeys={[tab || 'essential'] satisfies AgentSettingPopupTab[]}
|
||||
mode="vertical"
|
||||
items={items}
|
||||
onSelect={({ key }) => setMenu(key as AgentSettingPopupTab)}
|
||||
/>
|
||||
</LeftMenu>
|
||||
<Settings>{menu === 'essential' && <AgentEssentialSettings agent={agent} update={updateAgent} />}</Settings>
|
||||
</HStack>
|
||||
<ModalContent />
|
||||
</StyledModal>
|
||||
)
|
||||
}
|
||||
|
||||
const LeftMenu = styled.div`
|
||||
height: calc(80vh - 20px);
|
||||
height: 100%;
|
||||
border-right: 0.5px solid var(--color-border);
|
||||
`
|
||||
|
||||
const Settings = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
padding: 16px 16px;
|
||||
height: calc(80vh - 16px);
|
||||
overflow-y: scroll;
|
||||
`
|
||||
|
||||
const StyledModal = styled(Modal)`
|
||||
|
||||
74
src/renderer/src/pages/settings/AgentSettings/shared.tsx
Normal file
74
src/renderer/src/pages/settings/AgentSettings/shared.tsx
Normal file
@ -0,0 +1,74 @@
|
||||
import { Avatar, AvatarProps, cn } from '@heroui/react'
|
||||
import { getAgentAvatar } from '@renderer/config/agent'
|
||||
import { getAgentTypeLabel } from '@renderer/i18n/label'
|
||||
import { AgentType } from '@renderer/types'
|
||||
import React, { ReactNode } from 'react'
|
||||
|
||||
import { SettingDivider } from '..'
|
||||
|
||||
export interface SettingsTitleProps extends React.ComponentPropsWithRef<'div'> {
|
||||
actions?: ReactNode
|
||||
}
|
||||
|
||||
export const SettingsTitle: React.FC<SettingsTitleProps> = ({ children, actions }) => {
|
||||
return (
|
||||
<div className={cn(actions ? 'justify-between' : undefined, 'mb-1 flex items-center gap-2')}>
|
||||
<span className="flex items-center gap-1 font-bold">{children}</span>
|
||||
{actions !== undefined && actions}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export type AgentLabelProps = {
|
||||
type: AgentType
|
||||
name?: string
|
||||
classNames?: {
|
||||
container?: string
|
||||
avatar?: string
|
||||
name?: string
|
||||
}
|
||||
avatarProps?: AvatarProps
|
||||
}
|
||||
|
||||
export const AgentLabel: React.FC<AgentLabelProps> = ({ type, name, classNames, avatarProps }) => {
|
||||
return (
|
||||
<div className={cn('flex items-center gap-2', classNames?.container)}>
|
||||
<Avatar src={getAgentAvatar(type)} title={type} {...avatarProps} className={cn('h-5 w-5', classNames?.avatar)} />
|
||||
<span className={classNames?.name}>{name ?? getAgentTypeLabel(type)}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export interface SettingsItemProps extends React.ComponentPropsWithRef<'div'> {
|
||||
/** Add a divider beneath the item if true, defaults to true. */
|
||||
divider?: boolean
|
||||
/** Apply row direction flex or not, defaults to false. */
|
||||
inline?: boolean
|
||||
}
|
||||
|
||||
export const SettingsItem: React.FC<SettingsItemProps> = ({
|
||||
children,
|
||||
divider = true,
|
||||
inline = false,
|
||||
className,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
{...props}
|
||||
className={cn('flex flex-col', inline ? 'flex-row items-center justify-between gap-4' : undefined, className)}>
|
||||
{children}
|
||||
</div>
|
||||
{divider && <SettingDivider />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const SettingsContainer: React.FC<React.ComponentPropsWithRef<'div'>> = ({ children, className, ...props }) => {
|
||||
return (
|
||||
<div className={cn('flex flex-1 flex-col overflow-auto pr-2', className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -12,6 +12,15 @@ export function getProviderName(model?: Model) {
|
||||
return getFancyProviderName(provider)
|
||||
}
|
||||
|
||||
export function getProviderNameById(pid: string) {
|
||||
const provider = store.getState().llm.providers.find((p) => p.id === pid)
|
||||
if (provider) {
|
||||
return getFancyProviderName(provider)
|
||||
} else {
|
||||
return 'Unknown Provider'
|
||||
}
|
||||
}
|
||||
|
||||
export function getProviderByModel(model?: Model) {
|
||||
const id = model?.provider
|
||||
const provider = store.getState().llm.providers.find((p) => p.id === id)
|
||||
|
||||
@ -1,24 +1,17 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||
import { DEFAULT_CONTEXTCOUNT, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
|
||||
import { AgentEntity, AssistantPreset, AssistantSettings } from '@renderer/types'
|
||||
import { cloneDeep, mergeWith } from 'lodash'
|
||||
import { AssistantPreset, AssistantSettings } from '@renderer/types'
|
||||
|
||||
const logger = loggerService.withContext('Agents')
|
||||
// const logger = loggerService.withContext('Agents')
|
||||
export interface AgentsState {
|
||||
/** They are actually assistant presets.
|
||||
* They should not be in this slice. However, since redux will be removed
|
||||
* in the future, I just don't care where should they are. */
|
||||
agents: AssistantPreset[]
|
||||
/** For new autonomous agent feature. They are actual agent entities.
|
||||
* They won't be used anymore when sqlite api is ready.
|
||||
*/
|
||||
agentsNew: AgentEntity[]
|
||||
}
|
||||
|
||||
const initialState: AgentsState = {
|
||||
agents: [],
|
||||
agentsNew: []
|
||||
agents: []
|
||||
}
|
||||
|
||||
const assistantsSlice = createSlice({
|
||||
@ -58,28 +51,6 @@ const assistantsSlice = createSlice({
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
setAgents: (state, action: PayloadAction<AgentEntity[]>) => {
|
||||
state.agentsNew = action.payload
|
||||
},
|
||||
addAgent: (state, action: PayloadAction<AgentEntity>) => {
|
||||
state.agentsNew.push(action.payload)
|
||||
},
|
||||
removeAgent: (state, action: PayloadAction<{ id: string }>) => {
|
||||
state.agentsNew = state.agentsNew.filter((agent) => agent.id !== action.payload.id)
|
||||
},
|
||||
updateAgent: (state, action: PayloadAction<Partial<AgentEntity> & { id: string }>) => {
|
||||
const { id, ...update } = action.payload
|
||||
const agent = state.agentsNew.find((agent) => agent.id === id)
|
||||
if (agent) {
|
||||
mergeWith(agent, update, (_, srcVal) => {
|
||||
// cut reference
|
||||
if (Array.isArray(srcVal)) return cloneDeep(srcVal)
|
||||
else return undefined
|
||||
})
|
||||
} else {
|
||||
logger.warn('Agent not found when trying to update')
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -89,11 +60,7 @@ export const {
|
||||
addAssistantPreset,
|
||||
removeAssistantPreset,
|
||||
updateAssistantPreset,
|
||||
updateAssistantPresetSettings,
|
||||
setAgents,
|
||||
addAgent,
|
||||
removeAgent,
|
||||
updateAgent
|
||||
updateAssistantPresetSettings
|
||||
} = assistantsSlice.actions
|
||||
|
||||
export default assistantsSlice.reducer
|
||||
|
||||
@ -57,7 +57,7 @@ export const AgentBaseSchema = z.object({
|
||||
// Basic info
|
||||
name: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
accessible_paths: z.array(z.string()), // Array of directory paths the agent can access
|
||||
accessible_paths: z.array(z.string()).nonempty(), // Array of directory paths the agent can access
|
||||
|
||||
// Instructions for the agent
|
||||
instructions: z.string().optional(), // System prompt
|
||||
|
||||
Loading…
Reference in New Issue
Block a user