feat(dashboard): clone profiles from any source
This commit is contained in:
parent
3380563d94
commit
28bf8fb47d
@ -284,6 +284,7 @@ export function ProfileRail() {
|
|||||||
selectProfile(name)
|
selectProfile(name)
|
||||||
}}
|
}}
|
||||||
open={createOpen}
|
open={createOpen}
|
||||||
|
profiles={profiles}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<RenameProfileDialog
|
<RenameProfileDialog
|
||||||
|
|||||||
@ -2,14 +2,15 @@ import { useEffect, useState } from 'react'
|
|||||||
|
|
||||||
import { ActionStatus } from '@/components/ui/action-status'
|
import { ActionStatus } from '@/components/ui/action-status'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Checkbox } from '@/components/ui/checkbox'
|
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
import { createProfile, updateProfileSoul } from '@/hermes'
|
import { createProfile, updateProfileSoul } from '@/hermes'
|
||||||
import { useI18n } from '@/i18n'
|
import { useI18n } from '@/i18n'
|
||||||
import { AlertTriangle } from '@/lib/icons'
|
import { AlertTriangle } from '@/lib/icons'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
import type { ProfileInfo } from '@/types/hermes'
|
||||||
|
|
||||||
const PROFILE_NAME_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/
|
const PROFILE_NAME_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/
|
||||||
|
|
||||||
@ -23,16 +24,18 @@ export function isValidProfileName(name: string): boolean {
|
|||||||
export function CreateProfileDialog({
|
export function CreateProfileDialog({
|
||||||
onClose,
|
onClose,
|
||||||
onCreated,
|
onCreated,
|
||||||
open
|
open,
|
||||||
|
profiles = []
|
||||||
}: {
|
}: {
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
onCreated?: (name: string) => Promise<void> | void
|
onCreated?: (name: string) => Promise<void> | void
|
||||||
open: boolean
|
open: boolean
|
||||||
|
profiles?: ProfileInfo[]
|
||||||
}) {
|
}) {
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const p = t.profiles
|
const p = t.profiles
|
||||||
const [name, setName] = useState('')
|
const [name, setName] = useState('')
|
||||||
const [cloneFromDefault, setCloneFromDefault] = useState(true)
|
const [cloneFrom, setCloneFrom] = useState<null | string>('default')
|
||||||
const [soul, setSoul] = useState('')
|
const [soul, setSoul] = useState('')
|
||||||
const [status, setStatus] = useState<'done' | 'idle' | 'saving'>('idle')
|
const [status, setStatus] = useState<'done' | 'idle' | 'saving'>('idle')
|
||||||
const [error, setError] = useState<null | string>(null)
|
const [error, setError] = useState<null | string>(null)
|
||||||
@ -43,7 +46,7 @@ export function CreateProfileDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
setName('')
|
setName('')
|
||||||
setCloneFromDefault(true)
|
setCloneFrom('default')
|
||||||
setSoul('')
|
setSoul('')
|
||||||
setError(null)
|
setError(null)
|
||||||
setStatus('idle')
|
setStatus('idle')
|
||||||
@ -66,7 +69,7 @@ export function CreateProfileDialog({
|
|||||||
setError(null)
|
setError(null)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await createProfile({ name: trimmed, clone_from_default: cloneFromDefault })
|
await createProfile({ name: trimmed, clone_from: cloneFrom })
|
||||||
|
|
||||||
if (soul.trim()) {
|
if (soul.trim()) {
|
||||||
await updateProfileSoul(trimmed, soul)
|
await updateProfileSoul(trimmed, soul)
|
||||||
@ -107,17 +110,25 @@ export function CreateProfileDialog({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label className="flex cursor-pointer select-none items-start gap-2.5 px-0.5 py-1">
|
<div className="grid gap-1.5">
|
||||||
<Checkbox
|
<label className="text-xs font-medium" htmlFor="new-profile-clone-from">
|
||||||
checked={cloneFromDefault}
|
{p.cloneFrom}
|
||||||
className="mt-0.5 shrink-0"
|
|
||||||
onCheckedChange={checked => setCloneFromDefault(checked === true)}
|
|
||||||
/>
|
|
||||||
<span className="grid gap-0.5 leading-snug">
|
|
||||||
<span className="text-sm font-medium">{p.cloneFromDefault}</span>
|
|
||||||
<span className="text-xs text-muted-foreground">{p.cloneFromDefaultDesc}</span>
|
|
||||||
</span>
|
|
||||||
</label>
|
</label>
|
||||||
|
<Select onValueChange={value => setCloneFrom(value === '__none__' ? null : value)} value={cloneFrom ?? '__none__'}>
|
||||||
|
<SelectTrigger className="h-9 rounded-md" id="new-profile-clone-from">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="__none__">{p.cloneFromNone}</SelectItem>
|
||||||
|
{profiles.map(profile => (
|
||||||
|
<SelectItem key={profile.name} value={profile.name}>
|
||||||
|
{profile.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-xs text-muted-foreground">{p.cloneFromDesc}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-1.5">
|
<div className="grid gap-1.5">
|
||||||
<label className="text-xs font-medium" htmlFor="new-profile-soul">
|
<label className="text-xs font-medium" htmlFor="new-profile-soul">
|
||||||
@ -127,7 +138,7 @@ export function CreateProfileDialog({
|
|||||||
className="min-h-28 font-mono text-xs leading-5"
|
className="min-h-28 font-mono text-xs leading-5"
|
||||||
id="new-profile-soul"
|
id="new-profile-soul"
|
||||||
onChange={event => setSoul(event.target.value)}
|
onChange={event => setSoul(event.target.value)}
|
||||||
placeholder={p.soulPlaceholder(cloneFromDefault ? p.soulPlaceholderCloned : p.soulPlaceholderEmpty)}
|
placeholder={p.soulPlaceholder(cloneFrom ? p.soulPlaceholderCloned : p.soulPlaceholderEmpty)}
|
||||||
value={soul}
|
value={soul}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {
|
|||||||
DialogTitle
|
DialogTitle
|
||||||
} from '@/components/ui/dialog'
|
} from '@/components/ui/dialog'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
import {
|
import {
|
||||||
createProfile,
|
createProfile,
|
||||||
@ -82,14 +83,14 @@ export function ProfilesView({ onClose }: ProfilesViewProps) {
|
|||||||
}, [profiles, selectedName])
|
}, [profiles, selectedName])
|
||||||
|
|
||||||
const handleCreate = useCallback(
|
const handleCreate = useCallback(
|
||||||
async (name: string, cloneFromDefault: boolean) => {
|
async (name: string, cloneFrom: null | string) => {
|
||||||
const trimmed = name.trim()
|
const trimmed = name.trim()
|
||||||
|
|
||||||
if (!isValidProfileName(trimmed)) {
|
if (!isValidProfileName(trimmed)) {
|
||||||
throw new Error(p.nameHint)
|
throw new Error(p.nameHint)
|
||||||
}
|
}
|
||||||
|
|
||||||
await createProfile({ name: trimmed, clone_from_default: cloneFromDefault })
|
await createProfile({ name: trimmed, clone_from: cloneFrom })
|
||||||
notify({ kind: 'success', title: p.created, message: trimmed })
|
notify({ kind: 'success', title: p.created, message: trimmed })
|
||||||
setSelectedName(trimmed)
|
setSelectedName(trimmed)
|
||||||
await refresh()
|
await refresh()
|
||||||
@ -180,8 +181,9 @@ export function ProfilesView({ onClose }: ProfilesViewProps) {
|
|||||||
|
|
||||||
<CreateProfileDialog
|
<CreateProfileDialog
|
||||||
onClose={() => setCreateOpen(false)}
|
onClose={() => setCreateOpen(false)}
|
||||||
onCreate={async (name, cloneFromDefault) => handleCreate(name, cloneFromDefault)}
|
onCreate={async (name, cloneFrom) => handleCreate(name, cloneFrom)}
|
||||||
open={createOpen}
|
open={createOpen}
|
||||||
|
profiles={profiles ?? []}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Dialog onOpenChange={open => !open && !deleting && setPendingDelete(null)} open={pendingDelete !== null}>
|
<Dialog onOpenChange={open => !open && !deleting && setPendingDelete(null)} open={pendingDelete !== null}>
|
||||||
@ -453,16 +455,18 @@ function SoulEditor({ profileName }: { profileName: string }) {
|
|||||||
function CreateProfileDialog({
|
function CreateProfileDialog({
|
||||||
onClose,
|
onClose,
|
||||||
onCreate,
|
onCreate,
|
||||||
open
|
open,
|
||||||
|
profiles
|
||||||
}: {
|
}: {
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
onCreate: (name: string, cloneFromDefault: boolean) => Promise<void>
|
onCreate: (name: string, cloneFrom: null | string) => Promise<void>
|
||||||
open: boolean
|
open: boolean
|
||||||
|
profiles: ProfileInfo[]
|
||||||
}) {
|
}) {
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const p = t.profiles
|
const p = t.profiles
|
||||||
const [name, setName] = useState('')
|
const [name, setName] = useState('')
|
||||||
const [cloneFromDefault, setCloneFromDefault] = useState(true)
|
const [cloneFrom, setCloneFrom] = useState<null | string>('default')
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
const [error, setError] = useState<null | string>(null)
|
const [error, setError] = useState<null | string>(null)
|
||||||
|
|
||||||
@ -472,7 +476,7 @@ function CreateProfileDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
setName('')
|
setName('')
|
||||||
setCloneFromDefault(true)
|
setCloneFrom('default')
|
||||||
setError(null)
|
setError(null)
|
||||||
setSaving(false)
|
setSaving(false)
|
||||||
}, [open])
|
}, [open])
|
||||||
@ -493,7 +497,7 @@ function CreateProfileDialog({
|
|||||||
setError(null)
|
setError(null)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await onCreate(trimmed, cloneFromDefault)
|
await onCreate(trimmed, cloneFrom)
|
||||||
onClose()
|
onClose()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : p.failedCreate)
|
setError(err instanceof Error ? err.message : p.failedCreate)
|
||||||
@ -528,18 +532,25 @@ function CreateProfileDialog({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label className="flex cursor-pointer items-center gap-2 rounded-md border border-border/40 bg-background/50 px-3 py-2 text-sm">
|
<div className="grid gap-1.5">
|
||||||
<input
|
<label className="text-xs font-medium" htmlFor="new-profile-clone-from">
|
||||||
checked={cloneFromDefault}
|
{p.cloneFrom}
|
||||||
className="size-4 accent-primary"
|
|
||||||
onChange={event => setCloneFromDefault(event.target.checked)}
|
|
||||||
type="checkbox"
|
|
||||||
/>
|
|
||||||
<span>
|
|
||||||
<span className="font-medium">{p.cloneFromDefault}</span>
|
|
||||||
<span className="ml-2 text-xs text-muted-foreground">{p.cloneFromDefaultDesc}</span>
|
|
||||||
</span>
|
|
||||||
</label>
|
</label>
|
||||||
|
<Select onValueChange={value => setCloneFrom(value === '__none__' ? null : value)} value={cloneFrom ?? '__none__'}>
|
||||||
|
<SelectTrigger className="h-9 rounded-md" id="new-profile-clone-from">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="__none__">{p.cloneFromNone}</SelectItem>
|
||||||
|
{profiles.map(profile => (
|
||||||
|
<SelectItem key={profile.name} value={profile.name}>
|
||||||
|
{profile.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-xs text-muted-foreground">{p.cloneFromDesc}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="flex items-start gap-2 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive">
|
<div className="flex items-start gap-2 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive">
|
||||||
|
|||||||
@ -903,6 +903,9 @@ export const en: Translations = {
|
|||||||
deleting: 'Deleting...',
|
deleting: 'Deleting...',
|
||||||
createDesc: 'Profiles are independent Hermes environments: separate config, skills, and SOUL.md.',
|
createDesc: 'Profiles are independent Hermes environments: separate config, skills, and SOUL.md.',
|
||||||
nameLabel: 'Name',
|
nameLabel: 'Name',
|
||||||
|
cloneFrom: 'Clone from',
|
||||||
|
cloneFromNone: 'None (blank)',
|
||||||
|
cloneFromDesc: 'Copies config, skills, and SOUL.md from the selected source profile.',
|
||||||
cloneFromDefault: 'Clone from default',
|
cloneFromDefault: 'Clone from default',
|
||||||
cloneFromDefaultDesc: 'Copy config, skills, and SOUL.md from your default profile.',
|
cloneFromDefaultDesc: 'Copy config, skills, and SOUL.md from your default profile.',
|
||||||
invalidName: hint => `Invalid name. ${hint}`,
|
invalidName: hint => `Invalid name. ${hint}`,
|
||||||
|
|||||||
@ -1041,6 +1041,9 @@ export const ja = defineLocale({
|
|||||||
deleting: '削除中...',
|
deleting: '削除中...',
|
||||||
createDesc: 'プロファイルは独立した Hermes 環境です:設定、スキル、SOUL.md が別々になります。',
|
createDesc: 'プロファイルは独立した Hermes 環境です:設定、スキル、SOUL.md が別々になります。',
|
||||||
nameLabel: '名前',
|
nameLabel: '名前',
|
||||||
|
cloneFrom: '複製元',
|
||||||
|
cloneFromNone: 'なし(空)',
|
||||||
|
cloneFromDesc: '選択したプロファイルから設定、スキル、SOUL.md をコピーします。',
|
||||||
cloneFromDefault: 'デフォルトプロファイルから設定を複製',
|
cloneFromDefault: 'デフォルトプロファイルから設定を複製',
|
||||||
cloneFromDefaultDesc: 'デフォルトプロファイルから設定、スキル、SOUL.md をコピーします。',
|
cloneFromDefaultDesc: 'デフォルトプロファイルから設定、スキル、SOUL.md をコピーします。',
|
||||||
invalidName: hint => `無効なプロファイル名。${hint}`,
|
invalidName: hint => `無効なプロファイル名。${hint}`,
|
||||||
|
|||||||
@ -695,6 +695,9 @@ export interface Translations {
|
|||||||
deleting: string
|
deleting: string
|
||||||
createDesc: string
|
createDesc: string
|
||||||
nameLabel: string
|
nameLabel: string
|
||||||
|
cloneFrom: string
|
||||||
|
cloneFromNone: string
|
||||||
|
cloneFromDesc: string
|
||||||
cloneFromDefault: string
|
cloneFromDefault: string
|
||||||
cloneFromDefaultDesc: string
|
cloneFromDefaultDesc: string
|
||||||
invalidName: (hint: string) => string
|
invalidName: (hint: string) => string
|
||||||
|
|||||||
@ -999,6 +999,9 @@ export const zhHant = defineLocale({
|
|||||||
deleting: '刪除中…',
|
deleting: '刪除中…',
|
||||||
createDesc: '設定檔是獨立的 Hermes 環境:各自擁有獨立的設定、技能和 SOUL.md。',
|
createDesc: '設定檔是獨立的 Hermes 環境:各自擁有獨立的設定、技能和 SOUL.md。',
|
||||||
nameLabel: '名稱',
|
nameLabel: '名稱',
|
||||||
|
cloneFrom: '複製來源',
|
||||||
|
cloneFromNone: '無(空白)',
|
||||||
|
cloneFromDesc: '從選取的來源設定檔複製設定、技能和 SOUL.md。',
|
||||||
cloneFromDefault: '從預設設定檔複製設定',
|
cloneFromDefault: '從預設設定檔複製設定',
|
||||||
cloneFromDefaultDesc: '從您的預設設定檔複製設定、技能和 SOUL.md。',
|
cloneFromDefaultDesc: '從您的預設設定檔複製設定、技能和 SOUL.md。',
|
||||||
invalidName: hint => `設定檔名稱無效。${hint}`,
|
invalidName: hint => `設定檔名稱無效。${hint}`,
|
||||||
|
|||||||
@ -1092,6 +1092,9 @@ export const zh: Translations = {
|
|||||||
deleting: '删除中…',
|
deleting: '删除中…',
|
||||||
createDesc: '配置档案是相互独立的 Hermes 环境:各自拥有独立的配置、技能和 SOUL.md。',
|
createDesc: '配置档案是相互独立的 Hermes 环境:各自拥有独立的配置、技能和 SOUL.md。',
|
||||||
nameLabel: '名称',
|
nameLabel: '名称',
|
||||||
|
cloneFrom: '克隆来源',
|
||||||
|
cloneFromNone: '无(空白)',
|
||||||
|
cloneFromDesc: '从选中的来源配置档案复制配置、技能和 SOUL.md。',
|
||||||
cloneFromDefault: '从默认档案克隆',
|
cloneFromDefault: '从默认档案克隆',
|
||||||
cloneFromDefaultDesc: '从你的默认配置档案复制配置、技能和 SOUL.md。',
|
cloneFromDefaultDesc: '从你的默认配置档案复制配置、技能和 SOUL.md。',
|
||||||
invalidName: hint => `名称无效。${hint}`,
|
invalidName: hint => `名称无效。${hint}`,
|
||||||
|
|||||||
@ -470,7 +470,7 @@ export interface CronJobUpdates {
|
|||||||
|
|
||||||
export interface ProfileCreatePayload {
|
export interface ProfileCreatePayload {
|
||||||
clone_all?: boolean
|
clone_all?: boolean
|
||||||
clone_from?: string
|
clone_from?: null | string
|
||||||
clone_from_default?: boolean
|
clone_from_default?: boolean
|
||||||
name: string
|
name: string
|
||||||
no_skills?: boolean
|
no_skills?: boolean
|
||||||
|
|||||||
@ -8518,15 +8518,13 @@ async def scan_skill_hub(identifier: str = ""):
|
|||||||
|
|
||||||
class ProfileCreate(BaseModel):
|
class ProfileCreate(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
|
clone_from: Optional[str] = None
|
||||||
|
# Backward compatibility for older dashboard/desktop clients. New clients
|
||||||
|
# send clone_from="default" (or another profile name) explicitly.
|
||||||
clone_from_default: bool = False
|
clone_from_default: bool = False
|
||||||
clone_all: bool = False
|
clone_all: bool = False
|
||||||
no_skills: bool = False
|
no_skills: bool = False
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
# Explicit source profile to clone from (e.g. duplicating an existing
|
|
||||||
# profile). When set, it takes precedence over ``clone_from_default``,
|
|
||||||
# which always sources from "default". ``clone_all`` still selects a full
|
|
||||||
# state copytree vs. a config/skills/SOUL copy.
|
|
||||||
clone_from: Optional[str] = None
|
|
||||||
provider: Optional[str] = None
|
provider: Optional[str] = None
|
||||||
model: Optional[str] = None
|
model: Optional[str] = None
|
||||||
# Profile-builder additions — all optional, all applied best-effort AFTER
|
# Profile-builder additions — all optional, all applied best-effort AFTER
|
||||||
@ -8798,10 +8796,16 @@ async def create_profile_endpoint(body: ProfileCreate):
|
|||||||
clone = True
|
clone = True
|
||||||
clone_from = explicit_source
|
clone_from = explicit_source
|
||||||
clone_config = not body.clone_all
|
clone_config = not body.clone_all
|
||||||
|
elif body.clone_all:
|
||||||
|
# Preserve the dashboard's historical clone-all behavior: a full-copy
|
||||||
|
# request with no explicit dropdown source copies from default.
|
||||||
|
clone = True
|
||||||
|
clone_from = "default"
|
||||||
|
clone_config = False
|
||||||
else:
|
else:
|
||||||
clone = body.clone_from_default or body.clone_all
|
clone = body.clone_from_default
|
||||||
clone_from = "default" if clone else None
|
clone_from = "default" if clone else None
|
||||||
clone_config = body.clone_from_default and not body.clone_all
|
clone_config = clone
|
||||||
try:
|
try:
|
||||||
path = profiles_mod.create_profile(
|
path = profiles_mod.create_profile(
|
||||||
name=body.name,
|
name=body.name,
|
||||||
|
|||||||
@ -2633,7 +2633,7 @@ class TestNewEndpoints:
|
|||||||
|
|
||||||
resp = self.client.post(
|
resp = self.client.post(
|
||||||
"/api/profiles",
|
"/api/profiles",
|
||||||
json={"name": "writer", "clone_from_default": False},
|
json={"name": "writer", "clone_from": None},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
@ -2641,7 +2641,7 @@ class TestNewEndpoints:
|
|||||||
assert wrapper_path.exists()
|
assert wrapper_path.exists()
|
||||||
assert wrapper_path.read_text() == '#!/bin/sh\nexec hermes -p writer "$@"\n'
|
assert wrapper_path.read_text() == '#!/bin/sh\nexec hermes -p writer "$@"\n'
|
||||||
|
|
||||||
def test_profiles_create_with_clone_from_default_copies_default_skills(self, monkeypatch):
|
def test_profiles_create_with_clone_from_copies_source_skills(self, monkeypatch):
|
||||||
from hermes_constants import get_hermes_home
|
from hermes_constants import get_hermes_home
|
||||||
import hermes_cli.profiles as profiles_mod
|
import hermes_cli.profiles as profiles_mod
|
||||||
|
|
||||||
@ -2652,7 +2652,7 @@ class TestNewEndpoints:
|
|||||||
|
|
||||||
resp = self.client.post(
|
resp = self.client.post(
|
||||||
"/api/profiles",
|
"/api/profiles",
|
||||||
json={"name": "cloned", "clone_from_default": True},
|
json={"name": "cloned", "clone_from": "default"},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
@ -2685,6 +2685,28 @@ class TestNewEndpoints:
|
|||||||
)
|
)
|
||||||
assert cloned_skill.exists()
|
assert cloned_skill.exists()
|
||||||
|
|
||||||
|
def test_profiles_create_clone_all_from_named_source(self, monkeypatch):
|
||||||
|
from hermes_constants import get_hermes_home
|
||||||
|
import hermes_cli.profiles as profiles_mod
|
||||||
|
|
||||||
|
monkeypatch.setattr(profiles_mod, "create_wrapper_script", lambda name: None)
|
||||||
|
|
||||||
|
assert self.client.post("/api/profiles", json={"name": "full-src"}).status_code == 200
|
||||||
|
source_dir = get_hermes_home() / "profiles" / "full-src"
|
||||||
|
(source_dir / "config.yaml").write_text("model:\n provider: source-only\n", encoding="utf-8")
|
||||||
|
(source_dir / "workspace" / "artifact.txt").parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
(source_dir / "workspace" / "artifact.txt").write_text("copied", encoding="utf-8")
|
||||||
|
|
||||||
|
resp = self.client.post(
|
||||||
|
"/api/profiles",
|
||||||
|
json={"name": "full-copy", "clone_from": "full-src", "clone_all": True},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert resp.status_code == 200
|
||||||
|
target_dir = get_hermes_home() / "profiles" / "full-copy"
|
||||||
|
assert (target_dir / "config.yaml").read_text(encoding="utf-8") == "model:\n provider: source-only\n"
|
||||||
|
assert (target_dir / "workspace" / "artifact.txt").read_text(encoding="utf-8") == "copied"
|
||||||
|
|
||||||
def test_profiles_create_without_clone_seeds_bundled_skills(self, monkeypatch):
|
def test_profiles_create_without_clone_seeds_bundled_skills(self, monkeypatch):
|
||||||
from hermes_constants import get_hermes_home
|
from hermes_constants import get_hermes_home
|
||||||
import hermes_cli.profiles as profiles_mod
|
import hermes_cli.profiles as profiles_mod
|
||||||
@ -2701,7 +2723,7 @@ class TestNewEndpoints:
|
|||||||
|
|
||||||
resp = self.client.post(
|
resp = self.client.post(
|
||||||
"/api/profiles",
|
"/api/profiles",
|
||||||
json={"name": "fresh", "clone_from_default": False},
|
json={"name": "fresh", "clone_from": None},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
|
|||||||
@ -286,8 +286,8 @@ export const af: Translations = {
|
|||||||
nameRequired: "Naam word vereis",
|
nameRequired: "Naam word vereis",
|
||||||
nameRule:
|
nameRule:
|
||||||
"Slegs kleinletters, syfers, _ en -; moet met 'n letter of syfer begin; tot 64 karakters.",
|
"Slegs kleinletters, syfers, _ en -; moet met 'n letter of syfer begin; tot 64 karakters.",
|
||||||
invalidName: "Ongeldige profielnaam",
|
invalidName: "Ongeldige profielnaam", cloneFrom: "Kloon konfigurasie vanaf profiel",
|
||||||
cloneFromDefault: "Kloon konfigurasie vanaf verstekprofiel",
|
cloneFromNone: "Geen (leeg)",
|
||||||
allProfiles: "Profiele",
|
allProfiles: "Profiele",
|
||||||
noProfiles: "Geen profiele gevind nie.",
|
noProfiles: "Geen profiele gevind nie.",
|
||||||
defaultBadge: "verstek",
|
defaultBadge: "verstek",
|
||||||
|
|||||||
@ -286,8 +286,8 @@ export const de: Translations = {
|
|||||||
nameRequired: "Name ist erforderlich",
|
nameRequired: "Name ist erforderlich",
|
||||||
nameRule:
|
nameRule:
|
||||||
"Nur Kleinbuchstaben, Ziffern, _ und -; muss mit einem Buchstaben oder einer Ziffer beginnen; maximal 64 Zeichen.",
|
"Nur Kleinbuchstaben, Ziffern, _ und -; muss mit einem Buchstaben oder einer Ziffer beginnen; maximal 64 Zeichen.",
|
||||||
invalidName: "Ungültiger Profilname",
|
invalidName: "Ungültiger Profilname", cloneFrom: "Konfiguration klonen von",
|
||||||
cloneFromDefault: "Konfiguration vom Standardprofil klonen",
|
cloneFromNone: "Keine (leer)",
|
||||||
allProfiles: "Profile",
|
allProfiles: "Profile",
|
||||||
noProfiles: "Keine Profile gefunden.",
|
noProfiles: "Keine Profile gefunden.",
|
||||||
defaultBadge: "Standard",
|
defaultBadge: "Standard",
|
||||||
|
|||||||
@ -297,7 +297,8 @@ export const en: Translations = {
|
|||||||
nameRule:
|
nameRule:
|
||||||
"Lowercase letters, digits, _ and - only; must start with a letter or digit; up to 64 characters.",
|
"Lowercase letters, digits, _ and - only; must start with a letter or digit; up to 64 characters.",
|
||||||
invalidName: "Invalid profile name",
|
invalidName: "Invalid profile name",
|
||||||
cloneFromDefault: "Clone config from default profile",
|
cloneFrom: "Clone config from",
|
||||||
|
cloneFromNone: "None (blank)",
|
||||||
allProfiles: "Profiles",
|
allProfiles: "Profiles",
|
||||||
noProfiles: "No profiles found.",
|
noProfiles: "No profiles found.",
|
||||||
defaultBadge: "default",
|
defaultBadge: "default",
|
||||||
|
|||||||
@ -287,7 +287,8 @@ export const es: Translations = {
|
|||||||
nameRule:
|
nameRule:
|
||||||
"Solo letras minúsculas, dígitos, _ y -; debe comenzar con una letra o dígito; hasta 64 caracteres.",
|
"Solo letras minúsculas, dígitos, _ y -; debe comenzar con una letra o dígito; hasta 64 caracteres.",
|
||||||
invalidName: "Nombre de perfil no válido",
|
invalidName: "Nombre de perfil no válido",
|
||||||
cloneFromDefault: "Clonar configuración del perfil predeterminado",
|
cloneFrom: "Clonar desde el perfil",
|
||||||
|
cloneFromNone: "Ninguno (vacío)",
|
||||||
allProfiles: "Perfiles",
|
allProfiles: "Perfiles",
|
||||||
noProfiles: "No se encontraron perfiles.",
|
noProfiles: "No se encontraron perfiles.",
|
||||||
defaultBadge: "predeterminado",
|
defaultBadge: "predeterminado",
|
||||||
|
|||||||
@ -287,7 +287,8 @@ export const fr: Translations = {
|
|||||||
nameRule:
|
nameRule:
|
||||||
"Lettres minuscules, chiffres, _ et - uniquement ; doit commencer par une lettre ou un chiffre ; jusqu'à 64 caractères.",
|
"Lettres minuscules, chiffres, _ et - uniquement ; doit commencer par une lettre ou un chiffre ; jusqu'à 64 caractères.",
|
||||||
invalidName: "Nom de profil invalide",
|
invalidName: "Nom de profil invalide",
|
||||||
cloneFromDefault: "Cloner la configuration du profil par défaut",
|
cloneFrom: "Cloner depuis le profil",
|
||||||
|
cloneFromNone: "Aucun (vide)",
|
||||||
allProfiles: "Profils",
|
allProfiles: "Profils",
|
||||||
noProfiles: "Aucun profil trouvé.",
|
noProfiles: "Aucun profil trouvé.",
|
||||||
defaultBadge: "défaut",
|
defaultBadge: "défaut",
|
||||||
|
|||||||
@ -294,8 +294,8 @@ export const ga: Translations = {
|
|||||||
nameRequired: "Tá ainm riachtanach",
|
nameRequired: "Tá ainm riachtanach",
|
||||||
nameRule:
|
nameRule:
|
||||||
"Litreacha cás íochtair, digití, _ agus - amháin; caithfidh tús a chur le litir nó digit; suas le 64 carachtar.",
|
"Litreacha cás íochtair, digití, _ agus - amháin; caithfidh tús a chur le litir nó digit; suas le 64 carachtar.",
|
||||||
invalidName: "Ainm próifíle neamhbhailí",
|
invalidName: "Ainm próifíle neamhbhailí", cloneFrom: "Clónáil cumraíocht ón bpróifíl",
|
||||||
cloneFromDefault: "Clónáil cumraíocht ón bpróifíl réamhshocraithe",
|
cloneFromNone: "Dada (folamh)",
|
||||||
allProfiles: "Próifílí",
|
allProfiles: "Próifílí",
|
||||||
noProfiles: "Níor aimsíodh próifílí.",
|
noProfiles: "Níor aimsíodh próifílí.",
|
||||||
defaultBadge: "réamhshocraithe",
|
defaultBadge: "réamhshocraithe",
|
||||||
|
|||||||
@ -286,8 +286,8 @@ export const hu: Translations = {
|
|||||||
nameRequired: "A név kötelező",
|
nameRequired: "A név kötelező",
|
||||||
nameRule:
|
nameRule:
|
||||||
"Csak kisbetűk, számjegyek, _ és - karakterek; betűvel vagy számjeggyel kell kezdődnie; legfeljebb 64 karakter.",
|
"Csak kisbetűk, számjegyek, _ és - karakterek; betűvel vagy számjeggyel kell kezdődnie; legfeljebb 64 karakter.",
|
||||||
invalidName: "Érvénytelen profilnév",
|
invalidName: "Érvénytelen profilnév", cloneFrom: "Konfiguráció klónozása ebből a profilból",
|
||||||
cloneFromDefault: "Konfiguráció klónozása az alapértelmezett profilból",
|
cloneFromNone: "Nincs (üres)",
|
||||||
allProfiles: "Profilok",
|
allProfiles: "Profilok",
|
||||||
noProfiles: "Nem található profil.",
|
noProfiles: "Nem található profil.",
|
||||||
defaultBadge: "alapértelmezett",
|
defaultBadge: "alapértelmezett",
|
||||||
|
|||||||
@ -286,8 +286,8 @@ export const it: Translations = {
|
|||||||
nameRequired: "Il nome è obbligatorio",
|
nameRequired: "Il nome è obbligatorio",
|
||||||
nameRule:
|
nameRule:
|
||||||
"Solo lettere minuscole, cifre, _ e -; deve iniziare con una lettera o cifra; fino a 64 caratteri.",
|
"Solo lettere minuscole, cifre, _ e -; deve iniziare con una lettera o cifra; fino a 64 caratteri.",
|
||||||
invalidName: "Nome del profilo non valido",
|
invalidName: "Nome del profilo non valido", cloneFrom: "Clona configurazione dal profilo",
|
||||||
cloneFromDefault: "Clona la configurazione dal profilo predefinito",
|
cloneFromNone: "Nessuno (vuoto)",
|
||||||
allProfiles: "Profili",
|
allProfiles: "Profili",
|
||||||
noProfiles: "Nessun profilo trovato.",
|
noProfiles: "Nessun profilo trovato.",
|
||||||
defaultBadge: "predefinito",
|
defaultBadge: "predefinito",
|
||||||
|
|||||||
@ -285,8 +285,8 @@ export const ja: Translations = {
|
|||||||
nameRequired: "名前は必須です",
|
nameRequired: "名前は必須です",
|
||||||
nameRule:
|
nameRule:
|
||||||
"小文字、数字、_ および - のみ使用可能。最初は文字または数字で始める必要があります。最大 64 文字。",
|
"小文字、数字、_ および - のみ使用可能。最初は文字または数字で始める必要があります。最大 64 文字。",
|
||||||
invalidName: "無効なプロファイル名",
|
invalidName: "無効なプロファイル名", cloneFrom: "プロファイルから複製",
|
||||||
cloneFromDefault: "デフォルトプロファイルから設定を複製",
|
cloneFromNone: "なし(空)",
|
||||||
allProfiles: "プロファイル",
|
allProfiles: "プロファイル",
|
||||||
noProfiles: "プロファイルが見つかりません。",
|
noProfiles: "プロファイルが見つかりません。",
|
||||||
defaultBadge: "デフォルト",
|
defaultBadge: "デフォルト",
|
||||||
|
|||||||
@ -285,8 +285,8 @@ export const ko: Translations = {
|
|||||||
nameRequired: "이름은 필수입니다",
|
nameRequired: "이름은 필수입니다",
|
||||||
nameRule:
|
nameRule:
|
||||||
"소문자, 숫자, _ 및 - 만 사용 가능합니다. 문자나 숫자로 시작해야 하며 최대 64자입니다.",
|
"소문자, 숫자, _ 및 - 만 사용 가능합니다. 문자나 숫자로 시작해야 하며 최대 64자입니다.",
|
||||||
invalidName: "잘못된 프로필 이름입니다",
|
invalidName: "잘못된 프로필 이름입니다", cloneFrom: "프로필에서 복제",
|
||||||
cloneFromDefault: "기본 프로필에서 설정 복제",
|
cloneFromNone: "없음 (빈 상태)",
|
||||||
allProfiles: "프로필",
|
allProfiles: "프로필",
|
||||||
noProfiles: "프로필을 찾을 수 없습니다.",
|
noProfiles: "프로필을 찾을 수 없습니다.",
|
||||||
defaultBadge: "기본",
|
defaultBadge: "기본",
|
||||||
|
|||||||
@ -287,7 +287,8 @@ export const pt: Translations = {
|
|||||||
nameRule:
|
nameRule:
|
||||||
"Apenas letras minúsculas, dígitos, _ e -; deve começar com letra ou dígito; até 64 caracteres.",
|
"Apenas letras minúsculas, dígitos, _ e -; deve começar com letra ou dígito; até 64 caracteres.",
|
||||||
invalidName: "Nome de perfil inválido",
|
invalidName: "Nome de perfil inválido",
|
||||||
cloneFromDefault: "Clonar configuração do perfil predefinido",
|
cloneFrom: "Clonar a partir do perfil",
|
||||||
|
cloneFromNone: "Nenhum (vazio)",
|
||||||
allProfiles: "Perfis",
|
allProfiles: "Perfis",
|
||||||
noProfiles: "Não foram encontrados perfis.",
|
noProfiles: "Não foram encontrados perfis.",
|
||||||
defaultBadge: "predefinido",
|
defaultBadge: "predefinido",
|
||||||
|
|||||||
@ -286,8 +286,8 @@ export const ru: Translations = {
|
|||||||
nameRequired: "Имя обязательно",
|
nameRequired: "Имя обязательно",
|
||||||
nameRule:
|
nameRule:
|
||||||
"Только строчные буквы, цифры, _ и -; должно начинаться с буквы или цифры; до 64 символов.",
|
"Только строчные буквы, цифры, _ и -; должно начинаться с буквы или цифры; до 64 символов.",
|
||||||
invalidName: "Недопустимое имя профиля",
|
invalidName: "Недопустимое имя профиля", cloneFrom: "Клонировать конфигурацию из профиля",
|
||||||
cloneFromDefault: "Клонировать конфигурацию из профиля по умолчанию",
|
cloneFromNone: "Нет (пусто)",
|
||||||
allProfiles: "Профили",
|
allProfiles: "Профили",
|
||||||
noProfiles: "Профили не найдены.",
|
noProfiles: "Профили не найдены.",
|
||||||
defaultBadge: "по умолчанию",
|
defaultBadge: "по умолчанию",
|
||||||
|
|||||||
@ -286,8 +286,8 @@ export const tr: Translations = {
|
|||||||
nameRequired: "Ad gereklidir",
|
nameRequired: "Ad gereklidir",
|
||||||
nameRule:
|
nameRule:
|
||||||
"Yalnızca küçük harfler, rakamlar, _ ve - kullanılabilir; harf veya rakamla başlamalı; en fazla 64 karakter.",
|
"Yalnızca küçük harfler, rakamlar, _ ve - kullanılabilir; harf veya rakamla başlamalı; en fazla 64 karakter.",
|
||||||
invalidName: "Geçersiz profil adı",
|
invalidName: "Geçersiz profil adı", cloneFrom: "Profilden yapılandırmayı klonla",
|
||||||
cloneFromDefault: "Varsayılan profilden yapılandırmayı klonla",
|
cloneFromNone: "Hiçbiri (boş)",
|
||||||
allProfiles: "Profiller",
|
allProfiles: "Profiller",
|
||||||
noProfiles: "Profil bulunamadı.",
|
noProfiles: "Profil bulunamadı.",
|
||||||
defaultBadge: "varsayılan",
|
defaultBadge: "varsayılan",
|
||||||
|
|||||||
@ -354,7 +354,8 @@ export interface Translations {
|
|||||||
nameRequired: string;
|
nameRequired: string;
|
||||||
nameRule: string;
|
nameRule: string;
|
||||||
invalidName: string;
|
invalidName: string;
|
||||||
cloneFromDefault: string;
|
cloneFrom: string;
|
||||||
|
cloneFromNone: string;
|
||||||
allProfiles: string;
|
allProfiles: string;
|
||||||
noProfiles: string;
|
noProfiles: string;
|
||||||
defaultBadge: string;
|
defaultBadge: string;
|
||||||
|
|||||||
@ -287,7 +287,8 @@ export const uk: Translations = {
|
|||||||
nameRule:
|
nameRule:
|
||||||
"Лише малі літери, цифри, _ та -; має починатися з літери або цифри; до 64 символів.",
|
"Лише малі літери, цифри, _ та -; має починатися з літери або цифри; до 64 символів.",
|
||||||
invalidName: "Недопустима назва профілю",
|
invalidName: "Недопустима назва профілю",
|
||||||
cloneFromDefault: "Клонувати конфігурацію з профілю за замовчуванням",
|
cloneFrom: "Клонувати з профілю",
|
||||||
|
cloneFromNone: "Жоден (порожній)",
|
||||||
allProfiles: "Профілі",
|
allProfiles: "Профілі",
|
||||||
noProfiles: "Профілів не знайдено.",
|
noProfiles: "Профілів не знайдено.",
|
||||||
defaultBadge: "за замовчуванням",
|
defaultBadge: "за замовчуванням",
|
||||||
|
|||||||
@ -285,8 +285,8 @@ export const zhHant: Translations = {
|
|||||||
nameRequired: "名稱為必填",
|
nameRequired: "名稱為必填",
|
||||||
nameRule:
|
nameRule:
|
||||||
"僅允許小寫字母、數字、底線及連字號;首字必須為字母或數字;最多 64 個字元。",
|
"僅允許小寫字母、數字、底線及連字號;首字必須為字母或數字;最多 64 個字元。",
|
||||||
invalidName: "設定檔名稱無效",
|
invalidName: "設定檔名稱無效", cloneFrom: "從設定檔複製",
|
||||||
cloneFromDefault: "從預設設定檔複製設定",
|
cloneFromNone: "無(空白)",
|
||||||
allProfiles: "設定檔",
|
allProfiles: "設定檔",
|
||||||
noProfiles: "找不到設定檔。",
|
noProfiles: "找不到設定檔。",
|
||||||
defaultBadge: "預設",
|
defaultBadge: "預設",
|
||||||
|
|||||||
@ -282,8 +282,8 @@ export const zh: Translations = {
|
|||||||
nameRequired: "名称必填",
|
nameRequired: "名称必填",
|
||||||
nameRule:
|
nameRule:
|
||||||
"仅允许小写字母、数字、下划线和短横线;首字符必须是字母或数字;最多 64 个字符。",
|
"仅允许小写字母、数字、下划线和短横线;首字符必须是字母或数字;最多 64 个字符。",
|
||||||
invalidName: "多Agent配置名称非法",
|
invalidName: "多Agent配置名称非法", cloneFrom: "从配置文件克隆",
|
||||||
cloneFromDefault: "从默认多Agent配置克隆配置",
|
cloneFromNone: "无(空白)",
|
||||||
allProfiles: "多Agent配置列表",
|
allProfiles: "多Agent配置列表",
|
||||||
noProfiles: "暂无多Agent配置。",
|
noProfiles: "暂无多Agent配置。",
|
||||||
defaultBadge: "默认",
|
defaultBadge: "默认",
|
||||||
|
|||||||
@ -552,7 +552,8 @@ export const api = {
|
|||||||
}),
|
}),
|
||||||
createProfile: (body: {
|
createProfile: (body: {
|
||||||
name: string;
|
name: string;
|
||||||
clone_from_default: boolean;
|
clone_from?: string | null;
|
||||||
|
clone_from_default?: boolean;
|
||||||
clone_all?: boolean;
|
clone_all?: boolean;
|
||||||
no_skills?: boolean;
|
no_skills?: boolean;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|||||||
@ -220,7 +220,7 @@ export default function ProfileBuilderPage() {
|
|||||||
try {
|
try {
|
||||||
const res = await api.createProfile({
|
const res = await api.createProfile({
|
||||||
name: n,
|
name: n,
|
||||||
clone_from_default: false,
|
clone_from: null,
|
||||||
description: description.trim() || undefined,
|
description: description.trim() || undefined,
|
||||||
provider: pickedModel?.provider,
|
provider: pickedModel?.provider,
|
||||||
model: pickedModel?.model,
|
model: pickedModel?.model,
|
||||||
|
|||||||
@ -35,11 +35,11 @@ import { Badge } from "@nous-research/ui/ui/components/badge";
|
|||||||
import { Button } from "@nous-research/ui/ui/components/button";
|
import { Button } from "@nous-research/ui/ui/components/button";
|
||||||
import { Input } from "@nous-research/ui/ui/components/input";
|
import { Input } from "@nous-research/ui/ui/components/input";
|
||||||
import { Label } from "@nous-research/ui/ui/components/label";
|
import { Label } from "@nous-research/ui/ui/components/label";
|
||||||
import { Checkbox } from "@nous-research/ui/ui/components/checkbox";
|
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectOption,
|
SelectOption,
|
||||||
} from "@nous-research/ui/ui/components/select";
|
} from "@nous-research/ui/ui/components/select";
|
||||||
|
import { Checkbox } from "@nous-research/ui/ui/components/checkbox";
|
||||||
import { useI18n } from "@/i18n";
|
import { useI18n } from "@/i18n";
|
||||||
import { usePageHeader } from "@/contexts/usePageHeader";
|
import { usePageHeader } from "@/contexts/usePageHeader";
|
||||||
import { cn, themedBody } from "@/lib/utils";
|
import { cn, themedBody } from "@/lib/utils";
|
||||||
@ -312,7 +312,7 @@ export default function ProfilesPage() {
|
|||||||
// Create modal
|
// Create modal
|
||||||
const [createModalOpen, setCreateModalOpen] = useState(false);
|
const [createModalOpen, setCreateModalOpen] = useState(false);
|
||||||
const [newName, setNewName] = useState("");
|
const [newName, setNewName] = useState("");
|
||||||
const [cloneFromDefault, setCloneFromDefault] = useState(true);
|
const [cloneFrom, setCloneFrom] = useState<string | null>("default");
|
||||||
const [cloneAll, setCloneAll] = useState(false);
|
const [cloneAll, setCloneAll] = useState(false);
|
||||||
const [noSkills, setNoSkills] = useState(false);
|
const [noSkills, setNoSkills] = useState(false);
|
||||||
const [newDescription, setNewDescription] = useState("");
|
const [newDescription, setNewDescription] = useState("");
|
||||||
@ -429,7 +429,7 @@ export default function ProfilesPage() {
|
|||||||
}
|
}
|
||||||
setCreating(true);
|
setCreating(true);
|
||||||
try {
|
try {
|
||||||
const cloning = cloneAll || cloneFromDefault;
|
const cloning = cloneFrom !== null;
|
||||||
const picked = modelChoice
|
const picked = modelChoice
|
||||||
? modelChoices?.find(
|
? modelChoices?.find(
|
||||||
(c) => `${c.provider}\u0000${c.model}` === modelChoice,
|
(c) => `${c.provider}\u0000${c.model}` === modelChoice,
|
||||||
@ -437,8 +437,8 @@ export default function ProfilesPage() {
|
|||||||
: undefined;
|
: undefined;
|
||||||
const res = await api.createProfile({
|
const res = await api.createProfile({
|
||||||
name,
|
name,
|
||||||
clone_from_default: cloneAll ? false : cloneFromDefault,
|
clone_from: cloneFrom,
|
||||||
clone_all: cloneAll,
|
clone_all: cloning && cloneAll,
|
||||||
no_skills: cloning ? false : noSkills,
|
no_skills: cloning ? false : noSkills,
|
||||||
description: newDescription.trim() || undefined,
|
description: newDescription.trim() || undefined,
|
||||||
provider: picked?.provider,
|
provider: picked?.provider,
|
||||||
@ -455,7 +455,7 @@ export default function ProfilesPage() {
|
|||||||
setNewDescription("");
|
setNewDescription("");
|
||||||
setNoSkills(false);
|
setNoSkills(false);
|
||||||
setCloneAll(false);
|
setCloneAll(false);
|
||||||
setCloneFromDefault(true);
|
setCloneFrom("default");
|
||||||
setModelChoice("");
|
setModelChoice("");
|
||||||
setCreateModalOpen(false);
|
setCreateModalOpen(false);
|
||||||
load();
|
load();
|
||||||
@ -772,7 +772,7 @@ export default function ProfilesPage() {
|
|||||||
};
|
};
|
||||||
}, [setEnd, t.common.create, loading, navigate]);
|
}, [setEnd, t.common.create, loading, navigate]);
|
||||||
|
|
||||||
const cloning = cloneAll || cloneFromDefault;
|
const cloning = cloneFrom !== null;
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
@ -862,6 +862,26 @@ export default function ProfilesPage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="clone-from">{t.profiles.cloneFrom}</Label>
|
||||||
|
<Select
|
||||||
|
id="clone-from"
|
||||||
|
value={cloneFrom ?? ""}
|
||||||
|
onValueChange={(v) => {
|
||||||
|
const next = v || null;
|
||||||
|
setCloneFrom(next);
|
||||||
|
if (next === null) setCloneAll(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectOption value="">{t.profiles.cloneFromNone}</SelectOption>
|
||||||
|
{profiles.map((profile) => (
|
||||||
|
<SelectOption key={profile.name} value={profile.name}>
|
||||||
|
{profile.name}
|
||||||
|
</SelectOption>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label htmlFor="profile-description">
|
<Label htmlFor="profile-description">
|
||||||
{L.descriptionOptional}
|
{L.descriptionOptional}
|
||||||
@ -909,33 +929,19 @@ export default function ProfilesPage() {
|
|||||||
{L.advancedOptions}
|
{L.advancedOptions}
|
||||||
</legend>
|
</legend>
|
||||||
|
|
||||||
<div className="flex items-center gap-2.5">
|
|
||||||
<Checkbox
|
|
||||||
checked={cloneFromDefault}
|
|
||||||
id="clone-from-default"
|
|
||||||
disabled={cloneAll}
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
setCloneFromDefault(checked === true)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Label
|
|
||||||
className="font-mondwest normal-case tracking-normal text-sm cursor-pointer"
|
|
||||||
htmlFor="clone-from-default"
|
|
||||||
>
|
|
||||||
{t.profiles.cloneFromDefault}
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2.5">
|
<div className="flex items-center gap-2.5">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={cloneAll}
|
checked={cloneAll}
|
||||||
|
disabled={!cloning}
|
||||||
id="clone-all"
|
id="clone-all"
|
||||||
onCheckedChange={(checked) => setCloneAll(checked === true)}
|
onCheckedChange={(checked) => setCloneAll(checked === true)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Label
|
<Label
|
||||||
className="font-mondwest normal-case tracking-normal text-sm cursor-pointer"
|
className={cn(
|
||||||
|
"font-mondwest normal-case tracking-normal text-sm cursor-pointer",
|
||||||
|
!cloning && "opacity-50",
|
||||||
|
)}
|
||||||
htmlFor="clone-all"
|
htmlFor="clone-all"
|
||||||
>
|
>
|
||||||
{L.cloneAll}
|
{L.cloneAll}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user