Consume the UI capability platform: shared icons/avatars, settings shell
Why: avatars, cards, and settings are base capabilities now (user decision 2026-09-30); the host must eat its own dogfood so plugins get the identical surfaces. Evidence: - Local icons.tsx removed; Icon/Avatar/BrandMark/primitives/Settings Shell imported from @ui; message stream gains avatar columns (user initial from /api/me name, assistant brand mark); SettingsModal rebuilt on SettingsShell (side-nav sections with badges, EmptyState for plugins, iconified actions); AdminUsersModal rows show shared avatars. - tsconfig paths pin react/react-dom types to the frontend copy — the source-composed ui package otherwise resolves a second @types/react through its own node_modules during make frontend (npm ci), which breaks type identity (surfaced by SVGProps crossing the boundary); vite dedupe already guarantees one runtime copy. - Headless on the real binary: avatar columns render, settings shell switches sections with badges, admin rows show avatars, zero console errors; build + vitest green. Impact: visual/structural only; no API or state changes. Refs: Agent-System-Documents proposal 2026-09-30-ui-redesign-and-auth.md
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { api } from '../api'
|
||||
import { store, useStore } from '../store'
|
||||
import { Icon } from '../icons'
|
||||
import { Icon, Avatar } from '@ui'
|
||||
import { randomPassword } from '../passwords'
|
||||
|
||||
interface AdminUserRow { owner: string; name: string; isAdmin: boolean; mustChangePassword: boolean; hasPassword: boolean; passwordSetAt?: string; createdAt: string }
|
||||
@@ -100,7 +100,7 @@ export function AdminUsersModal({ onClose }: { onClose: () => void }) {
|
||||
<tbody>
|
||||
{(users ?? []).map(u => (
|
||||
<tr key={u.owner}>
|
||||
<td className="ad-mono">{u.owner}</td>
|
||||
<td className="ad-user-cell"><Avatar name={u.name || u.owner} size="sm" /><span className="ad-mono">{u.owner}</span></td>
|
||||
<td>{u.name || '—'}</td>
|
||||
<td>{u.isAdmin ? <span className="ad-chip ad-chip-admin">ADMIN</span> : <span className="ad-chip">USER</span>}</td>
|
||||
<td>{u.mustChangePassword ? <span className="ad-chip ad-chip-warn">待改密</span> : u.hasPassword ? '正常' : '未设密码'}</td>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// 中栏:消息流(Part 渲染器注册表)+ 流式渲染 + 过程折叠 + HITL 卡片 + 输入区(4.4)。
|
||||
import { Icon } from '../icons'
|
||||
import { Icon, Avatar, BrandMark } from '@ui'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { marked } from 'marked'
|
||||
import { api } from '../api'
|
||||
@@ -97,6 +97,7 @@ function AssistantMessage({ msg, live }: { msg: Message; live?: LiveState | null
|
||||
return (
|
||||
<div className="msg assistant">
|
||||
<div className="msg-role">
|
||||
<span className="role-mark role-assistant"><BrandMark size={16} /></span>
|
||||
<span>助手</span>
|
||||
{live && <span className="live-bar"><span className="live-dot" />{live.phase}</span>}
|
||||
</div>
|
||||
@@ -216,6 +217,7 @@ export function Conversation() {
|
||||
const currentConvId = useStore(s => s.currentConvId)
|
||||
const conversation = useStore(s => s.tasks.find(c => c.id === s.currentConvId))
|
||||
const messages = useStore(s => s.messages)
|
||||
const myName = useStore(s => s.me?.name || s.me?.owner || '你')
|
||||
const activeHitl = useStore(s => s.activeHitl)
|
||||
const live = useStore(s => s.live)
|
||||
const runs = useStore(s => s.runs)
|
||||
@@ -290,7 +292,9 @@ export function Conversation() {
|
||||
const anchored = cards.filter(c => c.anchorMessageId === m.id && c.kind === 'confirm')
|
||||
return (
|
||||
<div className="msg user" key={m.id}>
|
||||
<div className="msg-role">你</div>
|
||||
<div className="msg-role">
|
||||
<span className="role-mark"><Avatar name={myName} size="sm" /></span>你
|
||||
</div>
|
||||
<div className="bubble">{m.parts.filter(p => p.type === 'text').map(p => p.text).join('\n')}</div>
|
||||
{anchored.map(c => (
|
||||
<InlineConfirmCard key={c.id} card={c} callbacks={{
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { useState } from 'react'
|
||||
import { auth } from '../auth'
|
||||
import { api } from '../api'
|
||||
import { Icon, BrandMark } from '../icons'
|
||||
import { Icon, BrandMark } from '@ui'
|
||||
|
||||
function Err({ msg }: { msg: string }) {
|
||||
if (!msg) return null
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useEffect, useMemo, useState } from 'react'
|
||||
import { api } from '../api'
|
||||
import { store, useStore } from '../store'
|
||||
import type { AgentProfile, SettingDesc } from '../contracts'
|
||||
import { SettingsShell, Btn, EmptyState } from '@ui'
|
||||
|
||||
type Section = 'settings' | 'profiles' | 'plugins'
|
||||
|
||||
@@ -45,22 +46,17 @@ export function SettingsModal() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="overlay" onClick={e => { if (e.target === e.currentTarget) store.openSettings(false) }}>
|
||||
<div className="modal">
|
||||
<div className="modal-header">
|
||||
⚙ 设置
|
||||
<div className="tabs" style={{ margin: '0 16px', borderBottom: 'none' }}>
|
||||
{(['settings', 'profiles', 'plugins'] as Section[]).map(s => (
|
||||
<button key={s} className={`tab ${section === s ? 'active' : ''}`} onClick={() => setSection(s)}>
|
||||
{s === 'settings' ? '设置' : s === 'profiles' ? '智能体画像' : '插件'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<span className="spacer" />
|
||||
{saved && <span style={{ color: 'var(--ok)', marginRight: 10, fontSize: 12.5 }}>已保存</span>}
|
||||
<button className="btn ghost" onClick={() => store.openSettings(false)}>关闭</button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<SettingsShell
|
||||
title="设置" active={section} onSelect={k => setSection(k as Section)}
|
||||
onClose={() => store.openSettings(false)}
|
||||
sections={[
|
||||
{ key: 'settings', label: '常规', icon: 'settings' },
|
||||
{ key: 'profiles', label: '智能体画像', icon: 'bot', badge: profiles.length || undefined },
|
||||
{ key: 'plugins', label: '插件', icon: 'puzzle' },
|
||||
]}
|
||||
footer={saved ? <span style={{ color: 'var(--ok)' }}>✓ 已保存</span> : 'Agent-System · 设置中心'}
|
||||
>
|
||||
<div>
|
||||
{section === 'settings' && (
|
||||
<>
|
||||
{Object.entries(groups).map(([g, descs]) => (
|
||||
@@ -77,14 +73,13 @@ export function SettingsModal() {
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
<button className="btn" onClick={save}>保存设置</button>
|
||||
<Btn variant="primary" icon="check" onClick={save}>保存设置</Btn>
|
||||
</>
|
||||
)}
|
||||
{section === 'profiles' && <ProfilesSection profiles={profiles} />}
|
||||
{section === 'plugins' && <PluginsSection />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsShell>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -135,7 +130,7 @@ function ProfilesSection({ profiles }: { profiles: AgentProfile[] }) {
|
||||
return (
|
||||
<div>
|
||||
<div className="doc-toolbar">
|
||||
<button className="btn" onClick={() => { setEditing(newProfile()); setText('') }}>+ 新建画像</button>
|
||||
<Btn variant="primary" icon="plus" onClick={() => { setEditing(newProfile()); setText('') }}>新建画像</Btn>
|
||||
<label className="btn ghost" style={{ cursor: 'pointer' }}>
|
||||
导入 JSON<input type="file" accept=".json" style={{ display: 'none' }} onChange={async e => {
|
||||
const f = e.target.files?.[0]
|
||||
@@ -207,15 +202,15 @@ function PluginsSection() {
|
||||
return (
|
||||
<div>
|
||||
<div className="doc-toolbar">
|
||||
<button className="btn" onClick={async () => {
|
||||
<Btn variant="ghost" icon="undo" onClick={async () => {
|
||||
await api.reloadPlugins()
|
||||
await refresh()
|
||||
store.refreshPlugins()
|
||||
store.toast('插件已热重载')
|
||||
}}>⟳ 热重载</button>
|
||||
}}>热重载</Btn>
|
||||
<span style={{ color: 'var(--text-faint)', fontSize: 12 }}>声明资产(工作流/意图/提示词/面板)热加载;装卸不影响核心</span>
|
||||
</div>
|
||||
{plugins.length === 0 && <div style={{ color: 'var(--text-faint)' }}>暂无已装载插件(目录:设置 → 插件目录;示例插件见 Dev 仓 plugins/example)</div>}
|
||||
{plugins.length === 0 && <EmptyState icon="puzzle">暂无已装载插件<br/>(目录:常规 → 插件目录;示例见 Dev 仓 plugins/example)</EmptyState>}
|
||||
{plugins.map(p => (
|
||||
<div className="setting-row" key={p.manifest.id}>
|
||||
<label>{p.manifest.name} <span style={{ fontSize: 11, color: 'var(--text-faint)' }}>v{p.manifest.version}</span></label>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useEffect, useRef, useState } from 'react'
|
||||
import { store, useStore } from '../store'
|
||||
import { auth } from '../auth'
|
||||
import { api } from '../api'
|
||||
import { Icon, BrandMark, workspaceIcon } from '../icons'
|
||||
import { Icon, BrandMark, workspaceIcon } from '@ui'
|
||||
import type { Conversation } from '../contracts'
|
||||
import { workspaceEntries } from './Workspaces'
|
||||
import { groupByProject, groupByTimeline, relativeTime, filterTasks, type TaskOrg } from '../tasklist'
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { store, useStore } from '../store'
|
||||
import type { Run } from '../contracts'
|
||||
import { InboxList, pendingCount, type ActivityCard, type DocBridge } from '@ui'
|
||||
import { Icon } from '../icons'
|
||||
import { Icon } from '@ui'
|
||||
|
||||
// 工件解析/打开注入:渲染器纯展示,doc-ref 词汇经此桥接本仓状态(D29)。
|
||||
export const docBridge: DocBridge = {
|
||||
|
||||
Reference in New Issue
Block a user