feat: refactor HomePage to integrate agents store and update related components

feat: add runtime event handling for providers in ProvidersSection

feat: update routing to include Channels and Agents pages

feat: extend route types and navigation items for Channels and Agents

feat: implement agents store for managing agent data and interactions

fix: update chat store to utilize agents store for agent-related functionality

chore: export agents store from index

fix: enhance runtime types for better event handling

fix: update Vite config to handle dev server URL correctly
This commit is contained in:
duanshuwen
2026-04-18 14:56:32 +08:00
parent dfa4388087
commit ee72cf7261
52 changed files with 6626 additions and 189 deletions

View File

@@ -0,0 +1,357 @@
import { useEffect, useMemo, useState } from 'react';
import type { AgentSummary } from '@runtime/lib/agents';
import type { ProviderAccount } from '@runtime/lib/providers';
import AgentsDialogSurface from './AgentsDialogSurface';
const FIELD_CLASS_NAME = [
'w-full rounded-[20px] border border-black/10 bg-[#F8F4EC] px-5 py-4 text-[16px] text-[#171717]',
'outline-none transition-colors placeholder:text-[#99A0AE] focus:border-black/20',
'disabled:cursor-not-allowed disabled:opacity-65 dark:border-white/10 dark:bg-[#222225] dark:text-[#f3f4f6] dark:placeholder:text-gray-500 dark:focus:border-white/20',
].join(' ');
type AgentSettingsDialogCopy = {
title: string;
subtitle: string;
identityTitle: string;
nameLabel: string;
agentIdLabel: string;
workspaceTitle: string;
workspaceDescription: string;
workspaceLabel: string;
inheritedWorkspaceLabel: string;
inheritedWorkspaceYes: string;
inheritedWorkspaceNo: string;
modelTitle: string;
providerAccountLabel: string;
useDefaultProvider: string;
modelRefLabel: string;
modelRefPlaceholder: string;
effectiveProviderLabel: string;
effectiveModelLabel: string;
modelHelp: string;
managedFromModels: string;
openModelsLabel: string;
channelsTitle: string;
channelSummaryLabel: string;
accountBindingsLabel: string;
noChannels: string;
openChannelsLabel: string;
providerLoadErrorPrefix: string;
cancelLabel: string;
saveLabel: string;
savingLabel: string;
deleteLabel: string;
};
type AgentSettingsDialogProps = {
open: boolean;
agent: AgentSummary | null;
providerAccounts: ProviderAccount[];
providerLoading: boolean;
providerError: string | null;
defaultProviderAccountId: string | null;
defaultModelRef: string | null;
mainWorkspacePath: string | null;
channelSummary: string;
accountBindingCount: number;
saving: boolean;
deleting: boolean;
copy: AgentSettingsDialogCopy;
onClose: () => void;
onSave: (input: { name: string; providerAccountId: string | null; modelRef: string | null }) => Promise<void> | void;
onDelete: (agent: AgentSummary) => Promise<void> | void;
onOpenChannels: () => void;
onOpenModels: () => void;
};
export default function AgentSettingsDialog({
open,
agent,
providerAccounts,
providerLoading,
providerError,
defaultProviderAccountId,
defaultModelRef,
mainWorkspacePath,
channelSummary,
accountBindingCount,
saving,
deleting,
copy,
onClose,
onSave,
onDelete,
onOpenChannels,
onOpenModels,
}: AgentSettingsDialogProps) {
const [name, setName] = useState('');
const [providerAccountId, setProviderAccountId] = useState('');
const [modelRef, setModelRef] = useState('');
useEffect(() => {
if (!agent || !open) return;
setName(agent.name);
setProviderAccountId(agent.providerAccountId ?? '');
setModelRef(agent.overrideModelRef ?? '');
}, [agent, open]);
const selectedProvider = useMemo(
() => providerAccounts.find((account) => account.id === providerAccountId) ?? null,
[providerAccountId, providerAccounts],
);
const defaultProvider = useMemo(
() => providerAccounts.find((account) => account.id === defaultProviderAccountId) ?? null,
[defaultProviderAccountId, providerAccounts],
);
if (!agent) return null;
const isDefault = agent.isDefault;
const inheritedWorkspace = Boolean(
!isDefault
&& mainWorkspacePath
&& agent.workspace
&& agent.workspace === mainWorkspacePath,
);
const effectiveProviderLabel = selectedProvider?.label || defaultProvider?.label || copy.useDefaultProvider;
const effectiveModelLabel = modelRef.trim()
|| selectedProvider?.model
|| defaultModelRef
|| copy.modelRefPlaceholder;
const hasChanges = !isDefault && (
name.trim() !== agent.name
|| providerAccountId !== (agent.providerAccountId ?? '')
|| modelRef !== (agent.overrideModelRef ?? '')
);
return (
<AgentsDialogSurface
open={open}
onClose={onClose}
title={copy.title}
subtitle={copy.subtitle}
widthClassName="max-w-[980px]"
>
<div className="space-y-6">
<div className="grid gap-5 lg:grid-cols-[1.02fr_0.98fr]">
<section className="rounded-[28px] bg-[#F7F2E9] p-6 dark:bg-[#17171a]">
<div className="text-[20px] font-semibold text-[#2E3445] dark:text-[#f3f4f6]">
{copy.identityTitle}
</div>
<div className="mt-5 space-y-4">
<label className="block">
<span className="mb-3 block text-[16px] font-semibold text-[#2E3445] dark:text-[#f3f4f6]">
{copy.nameLabel}
</span>
<input
value={name}
onChange={(event) => setName(event.target.value)}
disabled={isDefault || saving || deleting}
className={FIELD_CLASS_NAME}
/>
</label>
<div className="rounded-[22px] border border-dashed border-black/10 bg-white/70 px-5 py-4 text-[14px] text-[#525866] dark:border-white/10 dark:bg-[#222225] dark:text-gray-300">
<div className="font-semibold text-[#2E3445] dark:text-[#f3f4f6]">
{copy.agentIdLabel}
</div>
<div className="mt-2 break-all font-mono text-[13px]">
{agent.id}
</div>
</div>
<div className="rounded-[22px] border border-dashed border-black/10 bg-white/70 px-5 py-4 dark:border-white/10 dark:bg-[#222225]">
<div className="font-semibold text-[#2E3445] dark:text-[#f3f4f6]">
{copy.workspaceTitle}
</div>
<div className="mt-2 text-[14px] leading-[1.6] text-[#5B6475] dark:text-gray-400">
{copy.workspaceDescription}
</div>
<div className="mt-4 grid gap-4 sm:grid-cols-[1fr_auto]">
<div className="min-w-0">
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-[#99A0AE] dark:text-gray-500">
{copy.workspaceLabel}
</div>
<div className="mt-2 break-all font-mono text-[13px] text-[#5B6475] dark:text-gray-300">
{agent.workspace || '--'}
</div>
</div>
<div className="min-w-[148px]">
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-[#99A0AE] dark:text-gray-500">
{copy.inheritedWorkspaceLabel}
</div>
<div className="mt-2 text-[14px] text-[#5B6475] dark:text-gray-300">
{inheritedWorkspace ? copy.inheritedWorkspaceYes : copy.inheritedWorkspaceNo}
</div>
</div>
</div>
</div>
</div>
</section>
<section className="rounded-[28px] bg-[#F7F2E9] p-6 dark:bg-[#17171a]">
<div className="flex items-start justify-between gap-4">
<div>
<div className="text-[20px] font-semibold text-[#2E3445] dark:text-[#f3f4f6]">
{copy.channelsTitle}
</div>
<div className="mt-2 text-[14px] leading-[1.6] text-[#646C7A] dark:text-gray-400">
{copy.channelSummaryLabel}
</div>
</div>
<button
type="button"
className="rounded-full border border-black/12 bg-white/80 px-5 py-2.5 text-[14px] font-medium text-[#2E3445] transition-colors hover:bg-white dark:border-white/10 dark:bg-[#222225] dark:text-gray-200 dark:hover:bg-[#2a2a2d]"
onClick={onOpenChannels}
>
{copy.openChannelsLabel}
</button>
</div>
<div className="mt-5 space-y-4 text-[14px] leading-[1.6] text-[#5B6475] dark:text-gray-300">
<div className="rounded-[22px] border border-dashed border-black/10 bg-white/70 px-5 py-4 dark:border-white/10 dark:bg-[#222225]">
<div className="font-semibold text-[#2E3445] dark:text-[#f3f4f6]">
{copy.channelSummaryLabel}
</div>
<div className="mt-2">
{channelSummary || copy.noChannels}
</div>
</div>
<div className="rounded-[22px] border border-dashed border-black/10 bg-white/70 px-5 py-4 dark:border-white/10 dark:bg-[#222225]">
<div className="font-semibold text-[#2E3445] dark:text-[#f3f4f6]">
{copy.accountBindingsLabel}
</div>
<div className="mt-2">
{accountBindingCount}
</div>
</div>
</div>
</section>
</div>
<section className="rounded-[28px] bg-[#F7F2E9] p-6 dark:bg-[#17171a]">
<div className="flex items-start justify-between gap-4">
<div>
<div className="text-[20px] font-semibold text-[#2E3445] dark:text-[#f3f4f6]">
{copy.modelTitle}
</div>
<div className="mt-2 text-[14px] leading-[1.6] text-[#646C7A] dark:text-gray-400">
{isDefault ? copy.managedFromModels : copy.modelHelp}
</div>
</div>
{isDefault ? (
<button
type="button"
className="rounded-full border border-black/12 bg-white/80 px-5 py-2.5 text-[14px] font-medium text-[#2E3445] transition-colors hover:bg-white dark:border-white/10 dark:bg-[#222225] dark:text-gray-200 dark:hover:bg-[#2a2a2d]"
onClick={onOpenModels}
>
{copy.openModelsLabel}
</button>
) : null}
</div>
{isDefault ? null : (
<div className="mt-5 grid gap-4 lg:grid-cols-[1fr_1fr_0.95fr]">
<label className="block">
<span className="mb-3 block text-[16px] font-semibold text-[#2E3445] dark:text-[#f3f4f6]">
{copy.providerAccountLabel}
</span>
<select
value={providerAccountId}
onChange={(event) => setProviderAccountId(event.target.value)}
disabled={providerLoading || saving || deleting}
className={FIELD_CLASS_NAME}
>
<option value="">{copy.useDefaultProvider}</option>
{providerAccounts.map((account) => (
<option key={account.id} value={account.id}>
{`${account.label}${account.isDefault ? ' · Default' : ''}${account.model ? ` · ${account.model}` : ''}`}
</option>
))}
</select>
</label>
<label className="block">
<span className="mb-3 block text-[16px] font-semibold text-[#2E3445] dark:text-[#f3f4f6]">
{copy.modelRefLabel}
</span>
<input
value={modelRef}
onChange={(event) => setModelRef(event.target.value)}
disabled={saving || deleting}
placeholder={copy.modelRefPlaceholder}
className={FIELD_CLASS_NAME}
/>
</label>
<div className="rounded-[22px] border border-dashed border-black/10 bg-white/70 px-5 py-4 text-[14px] text-[#5B6475] dark:border-white/10 dark:bg-[#222225] dark:text-gray-300">
<div>
<span className="font-semibold text-[#2E3445] dark:text-[#f3f4f6]">
{copy.effectiveProviderLabel}
</span>
<span className="ml-2">{effectiveProviderLabel}</span>
</div>
<div className="mt-3">
<span className="font-semibold text-[#2E3445] dark:text-[#f3f4f6]">
{copy.effectiveModelLabel}
</span>
<span className="ml-2 break-all font-mono text-[13px]">
{effectiveModelLabel}
</span>
</div>
</div>
</div>
)}
{providerError && !isDefault ? (
<div className="mt-4 rounded-[20px] border border-amber-500/25 bg-amber-500/10 px-4 py-3 text-[13px] text-amber-700 dark:text-amber-300">
{`${copy.providerLoadErrorPrefix}${providerError}`}
</div>
) : null}
</section>
<div className="flex flex-wrap items-center justify-end gap-4 pt-2">
{!isDefault ? (
<button
type="button"
className="rounded-full border border-red-500/20 bg-red-500/10 px-8 py-3 text-[16px] font-medium text-red-600 transition-colors hover:bg-red-500/15 disabled:cursor-not-allowed disabled:opacity-60 dark:text-red-300"
disabled={saving || deleting}
onClick={() => {
void onDelete(agent);
}}
>
{deleting ? copy.savingLabel : copy.deleteLabel}
</button>
) : null}
<button
type="button"
className="rounded-full border border-black/12 bg-white/80 px-8 py-3 text-[16px] font-medium text-[#2E3445] transition-colors hover:bg-white dark:border-white/10 dark:bg-[#222225] dark:text-gray-200 dark:hover:bg-[#2a2a2d]"
onClick={onClose}
disabled={saving || deleting}
>
{copy.cancelLabel}
</button>
{!isDefault ? (
<button
type="button"
className="rounded-full bg-[#7E9DF5] px-8 py-3 text-[16px] font-medium text-white transition-colors hover:bg-[#6E90F3] disabled:cursor-not-allowed disabled:opacity-60"
disabled={!hasChanges || saving || deleting || name.trim().length === 0}
onClick={() => {
void onSave({
name: name.trim(),
providerAccountId: providerAccountId || null,
modelRef: modelRef.trim() || null,
});
}}
>
{saving ? copy.savingLabel : copy.saveLabel}
</button>
) : null}
</div>
</div>
</AgentsDialogSurface>
);
}