chore: commit pending workspace changes (supervisor settings tabs, conversation keep-alive budget, PR article updates)
This commit is contained in:
+16
-12
@@ -325,8 +325,8 @@ const conversationPersistenceIntervalMs = 500;
|
||||
const liveMessageFlushIntervalMs = 250;
|
||||
const keepAliveExpirationMs = 60 * 60 * 1_000;
|
||||
const keepAliveSweepIntervalMs = 5 * 60 * 1_000;
|
||||
const maximumCachedConversations = 12;
|
||||
const recentCachedConversations = 5;
|
||||
const maximumCachedConversations = 6;
|
||||
const recentCachedConversations = 2;
|
||||
// Chat is the primary route and stays resident outside the capacity below,
|
||||
// together with any route that reports unsaved edits.
|
||||
const pinnedWorkspaceViews = ["chat"] as const;
|
||||
@@ -1842,6 +1842,13 @@ function App(): React.JSX.Element {
|
||||
},
|
||||
[],
|
||||
);
|
||||
// Busy conversations (sidebar activity indicators and queued follow-ups) keep their cached view.
|
||||
const busyConversationIdsRef = useRef<ReadonlySet<string>>(new Set());
|
||||
const activeConversationViewIds = useCallback((): Set<string> => new Set([
|
||||
...[...activeRuns.current.values()].map((run) => run.conversationId),
|
||||
...preparingConversations.current,
|
||||
...busyConversationIdsRef.current,
|
||||
]), []);
|
||||
const setActiveId = useCallback((update: SetStateAction<string>): void => {
|
||||
const next =
|
||||
typeof update === "function"
|
||||
@@ -1850,23 +1857,17 @@ function App(): React.JSX.Element {
|
||||
activeConversationIdRef.current = next;
|
||||
if (next) {
|
||||
const now = Date.now();
|
||||
const runningConversationIds = new Set(
|
||||
[...activeRuns.current.values()].map((run) => run.conversationId),
|
||||
);
|
||||
preparingConversations.current.forEach((conversationId) =>
|
||||
runningConversationIds.add(conversationId),
|
||||
);
|
||||
setCachedConversationViews((current) =>
|
||||
touchAndPruneKeepAliveEntries(current, next, now, {
|
||||
expiresAfterMs: keepAliveExpirationMs,
|
||||
maximumEntries: maximumCachedConversations,
|
||||
protectedKeys: runningConversationIds,
|
||||
protectedKeys: activeConversationViewIds(),
|
||||
recentEntries: recentCachedConversations,
|
||||
}),
|
||||
);
|
||||
}
|
||||
setActiveIdState(next);
|
||||
}, []);
|
||||
}, [activeConversationViewIds]);
|
||||
const [remoteProjectsEnabled, setRemoteProjectsEnabled] = useState(false);
|
||||
const [
|
||||
conversationHtmlRenderingEnabled,
|
||||
@@ -2387,7 +2388,7 @@ function App(): React.JSX.Element {
|
||||
expiresAfterMs: keepAliveExpirationMs,
|
||||
maximumEntries: maximumCachedConversations,
|
||||
now,
|
||||
protectedKeys: runningConversationIds,
|
||||
protectedKeys: activeConversationViewIds(),
|
||||
recentEntries: recentCachedConversations,
|
||||
},
|
||||
),
|
||||
@@ -2407,7 +2408,7 @@ function App(): React.JSX.Element {
|
||||
};
|
||||
const interval = window.setInterval(sweep, keepAliveSweepIntervalMs);
|
||||
return () => window.clearInterval(interval);
|
||||
}, [activeId, assistantTasks, getPinnedWorkspaceViews, knowledgeOperationCount, view, conversationStore]);
|
||||
}, [activeConversationViewIds, activeId, assistantTasks, getPinnedWorkspaceViews, knowledgeOperationCount, view, conversationStore]);
|
||||
|
||||
useEffect(() => {
|
||||
livePrimarySidebarWidthRef.current = primarySidebarWidth;
|
||||
@@ -3335,6 +3336,9 @@ function App(): React.JSX.Element {
|
||||
),
|
||||
[projectActivity],
|
||||
);
|
||||
useLayoutEffect(() => {
|
||||
busyConversationIdsRef.current = new Set([...activityByConversationId.keys(), ...queuedConversationIds]);
|
||||
}, [activityByConversationId, queuedConversationIds]);
|
||||
const pendingSidebarApprovals = usePendingSidebarApprovals(conversationStore);
|
||||
const sidebarArtifacts = useMemo<SidebarArtifact[]>(
|
||||
() =>
|
||||
|
||||
@@ -219,7 +219,12 @@ describe('HeartbeatCenter', () => {
|
||||
expect(screen.getAllByRole('heading', { level: 1 })).toHaveLength(1)
|
||||
expect(within(panel).queryByRole('heading', { name: t(key) })).not.toBeInTheDocument()
|
||||
expect(within(panel).queryByRole('tablist', { name: t('supervisor.navigation') })).not.toBeInTheDocument()
|
||||
if (key !== 'supervisor.graph') {
|
||||
if (key === 'supervisor.settings') {
|
||||
const settingsNavigation = within(panel).getByRole('tablist', { name: t('settingsTabs.label') })
|
||||
expect(screen.getAllByRole('tablist')).toEqual([navigation, settingsNavigation])
|
||||
expect(within(settingsNavigation).getAllByRole('tab').map((tab) => tab.textContent))
|
||||
.toEqual(['model', 'review', 'stories', 'suggestions'].map((id) => t(`settingsTabs.${id}`)))
|
||||
} else if (key !== 'supervisor.graph') {
|
||||
expect(screen.getAllByRole('tablist')).toEqual([navigation])
|
||||
expect(within(panel).queryByRole('tablist')).not.toBeInTheDocument()
|
||||
}
|
||||
@@ -539,7 +544,14 @@ describe('HeartbeatCenter', () => {
|
||||
expect(screen.queryByRole('heading', { name })).not.toBeInTheDocument()
|
||||
}
|
||||
fireEvent.click(screen.getByRole('tab', { name: '设置' }))
|
||||
expect(screen.getByRole('heading', { level: 2, name: '模型超时与并发' })).toBeInTheDocument()
|
||||
fireEvent.click(screen.getByRole('tab', { name: '回顾整理' }))
|
||||
expect(screen.getByRole('heading', { level: 2, name: '回顾算法' })).toBeInTheDocument()
|
||||
expect(screen.queryByRole('heading', { name: '模型超时与并发' })).not.toBeInTheDocument()
|
||||
fireEvent.click(screen.getByRole('tab', { name: '故事与经验' }))
|
||||
expect(screen.getByRole('switch', { name: '跨项目关联' })).toBeVisible()
|
||||
fireEvent.click(screen.getByRole('tab', { name: '建议' }))
|
||||
expect(screen.getByLabelText(/故事多少天没有新进展/)).toBeVisible()
|
||||
expect(screen.queryByRole('button', { name: '新建计划' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
@@ -644,7 +656,17 @@ describe('HeartbeatCenter', () => {
|
||||
expect(screen.queryByRole('region', { name: '心跳计划' })).not.toBeInTheDocument()
|
||||
fireEvent.click(screen.getByRole('tab', { name: '设置' }))
|
||||
expect(screen.getAllByRole('heading', { level: 1 })).toHaveLength(1)
|
||||
expect(screen.getByRole('heading', { name: '回顾算法' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('heading', { name: '模型超时与并发' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('keeps an unsaved draft when switching settings tabs', () => {
|
||||
renderComponent(<HeartbeatCenter {...createProps()} />)
|
||||
fireEvent.click(screen.getByRole('tab', { name: '设置' }))
|
||||
fireEvent.click(screen.getByRole('tab', { name: '回顾整理' }))
|
||||
fireEvent.change(screen.getByLabelText(/每批消息数/), { target: { value: '33' } })
|
||||
fireEvent.click(screen.getByRole('tab', { name: '建议' }))
|
||||
fireEvent.click(screen.getByRole('tab', { name: '回顾整理' }))
|
||||
expect(screen.getByLabelText(/每批消息数/)).toHaveValue(33)
|
||||
})
|
||||
|
||||
it('passes manual project and time selections without requiring an automatic plan', async () => {
|
||||
|
||||
@@ -19,7 +19,7 @@ import type {
|
||||
} from '../../shared/assistant-contracts'
|
||||
import { HeartbeatSettings } from './HeartbeatSettings'
|
||||
import { defaultSupervisionTimeoutSeconds, supervisionTimeoutSecondsSchema, defaultSupervisorModelConcurrency, supervisorModelConcurrencySchema, type ApplicationSettings, type ApplicationSettingsUpdate } from '../../shared/application-settings-contracts'
|
||||
import { SupervisionReviewSettings } from './SupervisionReviewSettings'
|
||||
import { SupervisionReviewSettings, type SupervisionSettingsSection } from './SupervisionReviewSettings'
|
||||
import { SupervisorWorkspace, type SupervisionGraphNavigation } from './SupervisorWorkspace'
|
||||
import { SupervisorActivity } from './SupervisorActivity'
|
||||
import './supervisor-workspace.css'
|
||||
@@ -165,17 +165,37 @@ function UnifiedSupervisorCenter(props: HeartbeatCenterProps): React.JSX.Element
|
||||
setPageTab('activity')
|
||||
window.requestAnimationFrame(() => centerRef.current?.querySelector<HTMLElement>('#supervisor-tab-activity')?.focus())
|
||||
}} />
|
||||
{pageTab === 'settings' && <>
|
||||
<SupervisionModelSettings {...props} />
|
||||
<SupervisionReviewSettings settings={props.applicationSettings}
|
||||
disabled={props.applicationSettingsPending || props.applicationSettingsLocked}
|
||||
onSave={props.onUpdateApplicationSettings} />
|
||||
</>}
|
||||
{pageTab === 'settings' && <SupervisorSettingsTabs {...props} />}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
type SettingsTab = 'model' | SupervisionSettingsSection
|
||||
|
||||
/** Supervisor settings grouped by purpose. Every section stays mounted, so switching tabs keeps unsaved drafts. */
|
||||
function SupervisorSettingsTabs(props: HeartbeatCenterProps): React.JSX.Element {
|
||||
const { t } = useTranslation('heartbeat')
|
||||
const [tab, setTab] = useState<SettingsTab>('model')
|
||||
const reviewProps = { settings: props.applicationSettings,
|
||||
disabled: props.applicationSettingsPending || props.applicationSettingsLocked,
|
||||
onSave: props.onUpdateApplicationSettings }
|
||||
const tabs: { id: SettingsTab; label: string }[] = [
|
||||
{ id: 'model', label: t('settingsTabs.model') },
|
||||
{ id: 'review', label: t('settingsTabs.review') },
|
||||
{ id: 'stories', label: t('settingsTabs.stories') },
|
||||
{ id: 'suggestions', label: t('settingsTabs.suggestions') }
|
||||
]
|
||||
return <div className="supervisor-settings">
|
||||
<PageTabs ariaLabel={t('settingsTabs.label')} idPrefix="supervisor-settings" value={tab} onChange={setTab}
|
||||
variant="segmented" tabs={tabs} />
|
||||
{tabs.map(({ id }) => <div key={id} role="tabpanel" id={`supervisor-settings-panel-${id}`}
|
||||
aria-labelledby={`supervisor-settings-tab-${id}`} hidden={tab !== id}>
|
||||
{id === 'model' ? <SupervisionModelSettings {...props} /> : <SupervisionReviewSettings section={id} {...reviewProps} />}
|
||||
</div>)}
|
||||
</div>
|
||||
}
|
||||
|
||||
function SupervisionModelSettings(props: HeartbeatCenterProps): React.JSX.Element {
|
||||
const { t } = useTranslation('heartbeat')
|
||||
const supervisor = props.applicationSettings?.supervisorOrganizeTimeoutSeconds ?? defaultSupervisionTimeoutSeconds
|
||||
|
||||
@@ -28,8 +28,7 @@ it('saves validated algorithm controls and retains the draft until saved setting
|
||||
fireEvent.change(screen.getByLabelText(/每次读取来源条数/), { target: { value: '17' } })
|
||||
fireEvent.change(screen.getByLabelText(/每批消息数/), { target: { value: '30' } })
|
||||
fireEvent.change(screen.getByLabelText(/单次响应容量/), { target: { value: '2048' } })
|
||||
const cross = screen.getByRole('switch', { name: '跨项目关联' })
|
||||
expect(cross).not.toBeChecked()
|
||||
expect(screen.queryByRole('switch', { name: '跨项目关联' })).not.toBeInTheDocument()
|
||||
fireEvent.click(screen.getByRole('button', { name: '保存回顾算法' }))
|
||||
expect(save).toHaveBeenCalledWith({ supervisionReview: { pageSize: 17, batchCharacters: 8000, batchMessages: 30, executionSeconds: 300, responseKiB: 2048, crossProject: false, storyThreadEvents: 20, experienceMinEvents: 5, stalledDays: 14 } })
|
||||
expect(screen.getByLabelText(/每次读取来源条数/)).toHaveValue(17)
|
||||
@@ -38,9 +37,23 @@ it('saves validated algorithm controls and retains the draft until saved setting
|
||||
it('enables cross-project linking only when the switch is turned on', () => {
|
||||
const save = vi.fn()
|
||||
const settings = applicationSettingsSchema.parse({ checkUpdatesOnStartup: true, updateSource: 'github', modelDownloadSource: 'modelscope', localToolEnvironment: defaultLocalToolEnvironmentSettings, conversationHtmlRenderingEnabled: true, remoteProjectsEnabled: false })
|
||||
render(<SupervisionReviewSettings settings={settings} onSave={save} />)
|
||||
expect(screen.getByRole('button', { name: '保存回顾算法' })).toBeDisabled()
|
||||
render(<SupervisionReviewSettings section="stories" settings={settings} onSave={save} />)
|
||||
expect(screen.getByRole('button', { name: '保存故事与经验设置' })).toBeDisabled()
|
||||
fireEvent.click(screen.getByRole('switch', { name: '跨项目关联' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: '保存回顾算法' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: '保存故事与经验设置' }))
|
||||
expect(save).toHaveBeenCalledWith({ supervisionReview: expect.objectContaining({ crossProject: true }) })
|
||||
})
|
||||
|
||||
it('saves one section over stored values without carrying another section\'s draft', () => {
|
||||
const save = vi.fn()
|
||||
const settings = applicationSettingsSchema.parse({ checkUpdatesOnStartup: true, updateSource: 'github', modelDownloadSource: 'modelscope', localToolEnvironment: defaultLocalToolEnvironmentSettings, conversationHtmlRenderingEnabled: true, remoteProjectsEnabled: false })
|
||||
render(<>
|
||||
<SupervisionReviewSettings section="review" settings={settings} onSave={save} />
|
||||
<SupervisionReviewSettings section="suggestions" settings={settings} onSave={save} />
|
||||
</>)
|
||||
expect(screen.queryByLabelText(/故事多少天没有新进展/)).toBeInTheDocument()
|
||||
fireEvent.change(screen.getByLabelText(/每批消息数/), { target: { value: '30' } })
|
||||
fireEvent.change(screen.getByLabelText(/故事多少天没有新进展/), { target: { value: '7' } })
|
||||
fireEvent.click(screen.getByRole('button', { name: '保存建议设置' }))
|
||||
expect(save).toHaveBeenCalledWith({ supervisionReview: expect.objectContaining({ stalledDays: 7, batchMessages: 20 }) })
|
||||
})
|
||||
|
||||
@@ -2,47 +2,71 @@ import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { ApplicationSettings, ApplicationSettingsUpdate } from '../../shared/application-settings-contracts'
|
||||
import { defaultExperienceMinEvents, defaultStalledDays, defaultStoryThreadEvents, defaultSupervisionReviewSettings, supervisionReviewSettingsSchema } from '../../shared/supervision-review-contracts'
|
||||
import { useWorkspaceUnsavedChanges } from './workspace-unsaved-changes'
|
||||
|
||||
const fields = [
|
||||
['pageSize', 1, 200], ['batchCharacters', 1000, 16000], ['batchMessages', 1, 50], ['responseKiB', 100, 16384], ['storyThreadEvents', 4, 500], ['experienceMinEvents', 2, 200], ['stalledDays', 1, 365]
|
||||
] as const
|
||||
export type SupervisionSettingsSection = 'review' | 'stories' | 'suggestions'
|
||||
|
||||
export function SupervisionReviewSettings({ settings, disabled, onSave }: {
|
||||
type NumericKey = 'pageSize' | 'batchCharacters' | 'batchMessages' | 'responseKiB' | 'storyThreadEvents' | 'experienceMinEvents' | 'stalledDays'
|
||||
|
||||
const sections: Record<SupervisionSettingsSection, { fields: readonly (readonly [NumericKey, number, number])[]; crossProject?: boolean }> = {
|
||||
review: { fields: [['pageSize', 1, 200], ['batchCharacters', 1000, 16000], ['batchMessages', 1, 50], ['responseKiB', 100, 16384]] },
|
||||
stories: { fields: [['storyThreadEvents', 4, 500], ['experienceMinEvents', 2, 200]], crossProject: true },
|
||||
suggestions: { fields: [['stalledDays', 1, 365]] }
|
||||
}
|
||||
|
||||
const titles = { review: 'reviewSettings.title', stories: 'reviewSettings.storiesTitle', suggestions: 'reviewSettings.suggestionsTitle' } as const
|
||||
const saves = { review: 'reviewSettings.save', stories: 'reviewSettings.saveStories', suggestions: 'reviewSettings.saveSuggestions' } as const
|
||||
|
||||
/**
|
||||
* One settings section of the supervisor's review configuration. Each section edits only its own
|
||||
* fields and saves them over the current stored values, so another section's unsaved draft is not
|
||||
* overwritten or reset when this one saves.
|
||||
*/
|
||||
export function SupervisionReviewSettings({ settings, disabled, onSave, section = 'review' }: {
|
||||
settings?: ApplicationSettings
|
||||
disabled?: boolean
|
||||
onSave?: (value: ApplicationSettingsUpdate) => void | Promise<unknown>
|
||||
section?: SupervisionSettingsSection
|
||||
}) {
|
||||
const { t } = useTranslation('heartbeat')
|
||||
const { fields, crossProject } = sections[section]
|
||||
const current = { ...defaultSupervisionReviewSettings, ...settings?.supervisionReview,
|
||||
responseKiB: settings?.supervisionReview?.responseKiB ?? 1024, crossProject: settings?.supervisionReview?.crossProject ?? false,
|
||||
storyThreadEvents: settings?.supervisionReview?.storyThreadEvents ?? defaultStoryThreadEvents,
|
||||
experienceMinEvents: settings?.supervisionReview?.experienceMinEvents ?? defaultExperienceMinEvents,
|
||||
stalledDays: settings?.supervisionReview?.stalledDays ?? defaultStalledDays }
|
||||
const same = (left: typeof current, right: typeof current) =>
|
||||
fields.every(([key]) => left[key] === right[key]) && (!crossProject || left.crossProject === right.crossProject)
|
||||
const [saved, setSaved] = useState(current)
|
||||
const [draft, setDraft] = useState(current)
|
||||
if (fields.some(([key]) => saved[key] !== current[key]) || saved.crossProject !== current.crossProject) { setSaved(current); setDraft(current) }
|
||||
const unchanged = fields.every(([key]) => draft[key] === current[key]) && draft.crossProject === current.crossProject
|
||||
const valid = supervisionReviewSettingsSchema.safeParse(draft).success
|
||||
if (!same(saved, current)) { setSaved(current); setDraft(current) }
|
||||
const unchanged = same(draft, current)
|
||||
useWorkspaceUnsavedChanges(!unchanged)
|
||||
const next = { ...current, ...Object.fromEntries(fields.map(([key]) => [key, draft[key]])), ...(crossProject ? { crossProject: draft.crossProject } : {}) }
|
||||
const valid = supervisionReviewSettingsSchema.safeParse(next).success
|
||||
const locked = disabled || !settings || !onSave
|
||||
return <form className="heartbeat-settings heartbeat-settings__editor" onSubmit={event => {
|
||||
event.preventDefault()
|
||||
if (valid && !locked) void onSave?.({ supervisionReview: draft })
|
||||
if (valid && !locked) void onSave?.({ supervisionReview: next })
|
||||
}}>
|
||||
<h2>{t('reviewSettings.title')}</h2>
|
||||
<p>{t('reviewSettings.help')}</p>
|
||||
<h2>{t(titles[section])}</h2>
|
||||
{section === 'review' && <p>{t('reviewSettings.help')}</p>}
|
||||
{section === 'suggestions' && <p>{t('reviewSettings.suggestionsHelp')}</p>}
|
||||
{fields.map(([key, min, max]) => <label key={key} className="heartbeat-settings__field">
|
||||
{t(`reviewSettings.${key}`)} ({min}..{max})
|
||||
<input type="number" min={min} max={max} step={1} value={draft[key]} disabled={locked}
|
||||
onChange={event => setDraft({ ...draft, [key]: Number(event.target.value) })} />
|
||||
</label>)}
|
||||
<label className="toggle-row">
|
||||
<span>{t('reviewSettings.crossProject')}</span>
|
||||
<input type="checkbox" role="switch" checked={draft.crossProject} disabled={locked}
|
||||
onChange={event => setDraft({ ...draft, crossProject: event.target.checked })} />
|
||||
</label>
|
||||
<p>{t('reviewSettings.crossProjectHelp')}</p>
|
||||
<p>{t('reviewSettings.pauseHelp')}</p>
|
||||
{crossProject && <>
|
||||
<label className="toggle-row">
|
||||
<span>{t('reviewSettings.crossProject')}</span>
|
||||
<input type="checkbox" role="switch" checked={draft.crossProject} disabled={locked}
|
||||
onChange={event => setDraft({ ...draft, crossProject: event.target.checked })} />
|
||||
</label>
|
||||
<p>{t('reviewSettings.crossProjectHelp')}</p>
|
||||
</>}
|
||||
{section === 'review' && <p>{t('reviewSettings.pauseHelp')}</p>}
|
||||
{!valid && <p role="alert">{t('reviewSettings.invalid')}</p>}
|
||||
<button type="submit" className="primary-button" disabled={locked || !valid || unchanged}>{t('reviewSettings.save')}</button>
|
||||
<button type="submit" className="primary-button" disabled={locked || !valid || unchanged}>{t(saves[section])}</button>
|
||||
</form>
|
||||
}
|
||||
|
||||
@@ -498,6 +498,9 @@ export function SupervisorWorkspace({
|
||||
// The graph column does not scroll: the graph scales to the height left in the window. Below 660px the flat
|
||||
// graph's labels would drop under 11px, so very short windows keep that height and scroll the page instead.
|
||||
useFillHeight(graphLayoutRef, tab === 'graph' && graph.events.length > 0, 24, GRAPH_MIN_HEIGHT)
|
||||
const graphEmptyRef = useRef<HTMLDivElement>(null)
|
||||
// Without a graph, the empty state takes the visible height so its message sits in the middle of the page.
|
||||
useFillHeight(graphEmptyRef, tab === 'graph' && !loading && !loadError && graph.events.length === 0, 24, 320)
|
||||
// The work review reads the selected result's own scope, so it matches the period shown.
|
||||
const recapStories = useSupervisionStories(latest?.scope, tab === 'overview', latest?.id)
|
||||
const selectedStory = selection?.kind === 'story' ? storyState.view.stories.find((story) => story.id === selection.id) : undefined
|
||||
@@ -700,31 +703,32 @@ export function SupervisorWorkspace({
|
||||
)}
|
||||
{tab === 'graph' && (
|
||||
<>
|
||||
{/* With a graph, scope and refresh live in the canvas heading; without one, keep refresh reachable here. */}
|
||||
{!graph.events.length && <div className="supervisor-workspace__action-bar">
|
||||
<span />
|
||||
<button
|
||||
className="secondary-button"
|
||||
disabled={busy}
|
||||
onClick={() => void refresh()}
|
||||
>
|
||||
{t('center.actions.refresh')}
|
||||
</button>
|
||||
</div>}
|
||||
{/* With a graph, scope and refresh live in the canvas heading; without one, they sit in the centred empty state. */}
|
||||
{!loading && !loadError && !graph.events.length && (
|
||||
<EmptyState
|
||||
icon={<Network size={28} />}
|
||||
title={t('supervisor.graphEmpty')}
|
||||
description={t('supervisor.emptyHint')}
|
||||
action={
|
||||
<button
|
||||
className="secondary-button"
|
||||
onClick={() => onTabChange?.('overview')}
|
||||
>
|
||||
{t('supervisor.recap')}
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
<div className="supervisor-workspace__graph-empty" ref={graphEmptyRef}>
|
||||
<EmptyState
|
||||
icon={<Network size={28} />}
|
||||
title={t('supervisor.graphEmpty')}
|
||||
description={t('supervisor.emptyHint')}
|
||||
action={
|
||||
<div className="supervisor-workspace__empty-actions">
|
||||
<button
|
||||
className="primary-button"
|
||||
onClick={() => onTabChange?.('overview')}
|
||||
>
|
||||
{t('supervisor.recap')}
|
||||
</button>
|
||||
<button
|
||||
className="secondary-button"
|
||||
disabled={busy}
|
||||
onClick={() => void refresh()}
|
||||
>
|
||||
{t('center.actions.refresh')}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{graph.events.length > 0 && (
|
||||
<div className="supervisor-workspace__graph-layout" ref={graphLayoutRef}>
|
||||
|
||||
@@ -9,6 +9,9 @@ export const heartbeat = {
|
||||
experienceMinEvents: 'Minimum events before a story is considered for experiences (default 5)',
|
||||
stalledDays: 'Days without new events before a story is flagged (default 14)',
|
||||
save: 'Save review algorithm',
|
||||
storiesTitle: 'Stories and experiences', saveStories: 'Save story and experience settings',
|
||||
suggestionsTitle: 'Supervisor suggestions', saveSuggestions: 'Save suggestion settings',
|
||||
suggestionsHelp: 'Used when a heartbeat plan is set to suggest. Suggestions only remind you; they never change a story\'s status.',
|
||||
help: 'Page size controls database reads, not total coverage. Batch limits split work without dropping remaining text. These settings and the organize timeout are frozen for each new review; continuing uses its saved configuration. Response capacity uses the current setting for each request; increase it before continuing a failed review.',
|
||||
pageSize: 'Sources read at a time (default 50)', batchCharacters: 'Text size per batch (default 8000)',
|
||||
batchMessages: 'Messages per batch (default 20)', executionSeconds: 'Time before pausing, seconds (default 300)',
|
||||
@@ -31,6 +34,7 @@ export const heartbeat = {
|
||||
runError: 'This run failed. Review the error details before continuing; saved batches remain available.',
|
||||
diagnostics: 'Error details'
|
||||
},
|
||||
settingsTabs: { label: 'Supervisor settings sections', model: 'Model', review: 'Review processing', stories: 'Stories and experiences', suggestions: 'Suggestions' },
|
||||
suggestions: {
|
||||
title: 'Supervisor suggestions',
|
||||
kind: { open_item: 'Open item', conflict: 'Disagreement', convention: 'Possible convention', revision: 'Important revision', stalled: 'No recent progress', experience: 'Relevant experience' },
|
||||
|
||||
@@ -6,6 +6,9 @@ export const heartbeat = {
|
||||
experienceMinEvents: '故事参与经验整理的最少事件数(默认 5)',
|
||||
stalledDays: '故事多少天没有新进展时提醒(默认 14)',
|
||||
save: '保存回顾算法',
|
||||
storiesTitle: '故事与经验', saveStories: '保存故事与经验设置',
|
||||
suggestionsTitle: '监督建议', saveSuggestions: '保存建议设置',
|
||||
suggestionsHelp: '心跳计划选择“生成建议”时使用。只提醒,不会改变故事的状态。',
|
||||
help: '每页条数只控制数据库读取量,不限制回顾总量。分批上限用于拆分工作,剩余正文会继续处理。这些设置及整理超时在新回顾开始时固定;继续回顾沿用已保存的配置。响应容量按每次请求的当前设置生效,调高后可继续失败的回顾。',
|
||||
pageSize: '每次读取来源条数(默认 50)', batchCharacters: '每批文本量(默认 8000)',
|
||||
batchMessages: '每批消息数(默认 20)', executionSeconds: '单次整理时长,秒(默认 300)',
|
||||
@@ -28,6 +31,7 @@ export const heartbeat = {
|
||||
runError: '本次运行失败,请查看错误详情后继续。已保存的批次仍保留。',
|
||||
diagnostics: '错误详情'
|
||||
},
|
||||
settingsTabs: { label: '监督者设置分类', model: '模型', review: '回顾整理', stories: '故事与经验', suggestions: '建议' },
|
||||
suggestions: {
|
||||
title: '监督建议',
|
||||
kind: { open_item: '未决事项', conflict: '分歧', convention: '候选约定', revision: '重要修订', stalled: '暂无进展', experience: '可用经验' },
|
||||
|
||||
@@ -14,6 +14,20 @@
|
||||
box-sizing: border-box;
|
||||
align-self: stretch;
|
||||
}
|
||||
.supervisor-workspace__graph-empty {
|
||||
display: grid;
|
||||
min-height: var(--fill-height, 320px);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.supervisor-workspace__graph-empty > .empty-state {
|
||||
min-height: 0;
|
||||
}
|
||||
.supervisor-workspace__empty-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
.supervisor-workspace__run-status {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||
|
||||
Reference in New Issue
Block a user