@modesto_ma/dsh-channels 0.5.0-beta.4 → 0.5.0-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Tabs made only of Host fields (「会话」「模型」「附件与工具」): one titled
3
+ * section per field group the tab owns (`TAB_FIELD_GROUPS`).
4
+ *
5
+ * @module dsh-channels/client/panel/fields-tab
6
+ */
7
+
8
+ import type { ReactNode } from 'react'
9
+ import { PANEL_COPY, TAB_FIELD_GROUPS } from '../../settings/contract.ts'
10
+ import type { FieldGroup, PanelTabId } from '../../settings/contract.ts'
11
+ import { FieldSections } from './shared.tsx'
12
+ import type { FormView } from './shared.tsx'
13
+
14
+ /** Footnotes under one group's section, per group. */
15
+ const FOOTNOTES: Partial<Record<FieldGroup, ReactNode>> = {
16
+ model: PANEL_COPY.modelFootnote,
17
+ }
18
+
19
+ export function FieldsTab(props: { form: FormView; tab: PanelTabId; note?: string }): JSX.Element {
20
+ const { form, tab, note } = props
21
+ return (
22
+ <div className="dsh-channels-cfg__tabBody">
23
+ {note === undefined ? null : <p className="dsh-channels-cfg__secFoot">{note}</p>}
24
+ <FieldSections form={form} groups={TAB_FIELD_GROUPS[tab]} footnotes={FOOTNOTES} />
25
+ </div>
26
+ )
27
+ }
@@ -1,6 +1,6 @@
1
1
  /**
2
- * 设置 →「飞书渠道」页面:数据加载、5 秒状态轮询、全局 draft 与顶部状态头 /
3
- * tab 导航的外壳。
2
+ * 设置 →「飞书渠道」页面:数据加载、5 秒状态轮询、全局 draft、顶部状态头 /
3
+ * tab 导航与底部保存条的外壳。
4
4
  *
5
5
  * 所有 Host 交互(panel / status / catalog / config / credentials / access /
6
6
  * pick-directory)都集中在这里;tab 组件只接收渲染所需的数据与
@@ -48,14 +48,15 @@ import { applyCatalogFields, catalogFromFields } from '../catalog.ts'
48
48
  import { AccessTab } from './access-tab.tsx'
49
49
  import { AdvancedTab } from './advanced-tab.tsx'
50
50
  import { ConnectionTab } from './connection-tab.tsx'
51
- import { ModelTab } from './model-tab.tsx'
52
- import { SessionsTab } from './sessions-tab.tsx'
51
+ import { FieldsTab } from './fields-tab.tsx'
53
52
  import {
54
53
  fieldLabel,
54
+ formatUptime,
55
55
  messageOf,
56
56
  NoticeBar,
57
57
  phaseBadge,
58
- phaseSummary,
58
+ Pill,
59
+ SaveBar,
59
60
  validateDraft,
60
61
  } from './shared.tsx'
61
62
  import type {
@@ -67,7 +68,6 @@ import type {
67
68
  Notice,
68
69
  } from './shared.tsx'
69
70
  import { WikiTab } from './wiki-tab.tsx'
70
- import { WorkspaceTab } from './workspace-tab.tsx'
71
71
 
72
72
  /** Owner props handed to a `settings.section` contribution (unused beyond parity). */
73
73
  export interface FeishuChannelsSectionProps {
@@ -316,24 +316,14 @@ export function FeishuChannelsSection(_props: FeishuChannelsSectionProps): JSX.E
316
316
  }
317
317
  }, [draft, panel])
318
318
 
319
- /** Drop the draft keys owned by one tab (nothing is written). */
320
- const discardTab = useCallback((tab: PanelTabId): void => {
319
+ /** Drop the whole draft (nothing is written). */
320
+ const discardDraft = useCallback((): void => {
321
321
  if (panel === null) return
322
- const groups = new Set<string>(TAB_FIELD_GROUPS[tab])
323
- const keys = new Set(
324
- panel.config.fields.filter(field => groups.has(field.group)).map(field => field.key),
325
- )
326
- setDraft((previous) => {
327
- const next = { ...previous }
328
- for (const key of keys) delete next[key]
329
- return next
330
- })
322
+ setDraft({})
331
323
  setNotice(null)
332
- if (tab === 'model') {
333
- catalogTicket.current += 1
334
- setCatalog(catalogFromFields(panel.config.fields))
335
- setCatalogBusy(false)
336
- }
324
+ catalogTicket.current += 1
325
+ setCatalog(catalogFromFields(panel.config.fields))
326
+ setCatalogBusy(false)
337
327
  }, [panel])
338
328
 
339
329
  /**
@@ -359,6 +349,10 @@ export function FeishuChannelsSection(_props: FeishuChannelsSectionProps): JSX.E
359
349
 
360
350
  /** Drop every Web override and return the refreshed panel. */
361
351
  const onClear = useCallback(async (): Promise<void> => {
352
+ const confirmed = typeof window.confirm === 'function'
353
+ ? window.confirm(PANEL_COPY.clearConfirm)
354
+ : true
355
+ if (!confirmed) return
362
356
  setBusy('clear')
363
357
  setNotice(null)
364
358
  try {
@@ -624,6 +618,10 @@ export function FeishuChannelsSection(_props: FeishuChannelsSectionProps): JSX.E
624
618
  [notice],
625
619
  )
626
620
  const dirtyCount = Object.keys(draft).length
621
+ const restartCount = useMemo(
622
+ () => (panel?.config.fields ?? []).filter(field => field.restartRequired && field.key in draft).length,
623
+ [draft, panel],
624
+ )
627
625
 
628
626
  /** Which tabs currently own a dirty key (the dot is per tab, not global). */
629
627
  const dirtyTabs = useMemo<ReadonlySet<PanelTabId>>(() => {
@@ -649,13 +647,10 @@ export function FeishuChannelsSection(_props: FeishuChannelsSectionProps): JSX.E
649
647
  issues,
650
648
  busy,
651
649
  locked: hostBusy,
652
- dirtyCount,
653
650
  onChange: updateDraft,
654
- onSave: () => { void onSave() },
655
- onDiscardTab: discardTab,
656
651
  onPickDirectory,
657
652
  }
658
- }, [busy, discardTab, dirtyCount, draft, hostBusy, issues, onPickDirectory, onSave, panel, renderedFields, updateDraft])
653
+ }, [busy, draft, hostBusy, issues, onPickDirectory, panel, renderedFields, updateDraft])
659
654
 
660
655
  const credentials = useMemo<CredentialsView>(() => ({
661
656
  appId: appIdInput,
@@ -713,8 +708,14 @@ export function FeishuChannelsSection(_props: FeishuChannelsSectionProps): JSX.E
713
708
  locked={hostBusy}
714
709
  onRefresh={() => { void onRefresh() }}
715
710
  />
716
- <TabNav activeTab={activeTab} dirtyTabs={dirtyTabs} onChange={setActiveTab} />
717
- {notice === null ? null : <NoticeBar notice={notice} />}
711
+ <TabNav
712
+ activeTab={activeTab}
713
+ dirtyTabs={dirtyTabs}
714
+ counts={{ access: load.panel.access.loaded ? load.panel.access.requests.length : 0 }}
715
+ onChange={setActiveTab}
716
+ />
717
+ {load.panel.access.mode === 'open' ? <div className="dsh-channels-cfg__banner">{PANEL_COPY.debugModeWarning}</div> : null}
718
+ {notice === null ? null : <NoticeBar kind={notice.kind} text={notice.text} />}
718
719
  <div
719
720
  role="tabpanel"
720
721
  id={`dsh-channels-tabpanel-${activeTab}`}
@@ -731,13 +732,11 @@ export function FeishuChannelsSection(_props: FeishuChannelsSectionProps): JSX.E
731
732
  />
732
733
  )
733
734
  : null}
734
- {activeTab === 'sessions' ? <SessionsTab form={form} analyzeToken={analyzeToken} /> : null}
735
- {activeTab === 'model' ? <ModelTab form={form} catalogBusy={catalogBusy} /> : null}
736
- {activeTab === 'workspace'
737
- ? (
738
- <WorkspaceTab form={form} />
739
- )
735
+ {activeTab === 'sessions' ? <FieldsTab form={form} tab="sessions" /> : null}
736
+ {activeTab === 'model'
737
+ ? <FieldsTab form={form} tab="model" note={catalogBusy ? PANEL_COPY.catalogLoading : undefined} />
740
738
  : null}
739
+ {activeTab === 'attachments' ? <FieldsTab form={form} tab="attachments" /> : null}
741
740
  {activeTab === 'access'
742
741
  ? (
743
742
  <AccessTab
@@ -764,14 +763,22 @@ export function FeishuChannelsSection(_props: FeishuChannelsSectionProps): JSX.E
764
763
  )
765
764
  : null}
766
765
  {activeTab === 'advanced'
767
- ? <AdvancedTab form={form} onClear={() => { void onClear() }} />
766
+ ? <AdvancedTab form={form} analyzeToken={analyzeToken} onClear={() => { void onClear() }} />
768
767
  : null}
769
768
  </div>
769
+ <SaveBar
770
+ dirtyCount={dirtyCount}
771
+ restartCount={restartCount}
772
+ busy={busy}
773
+ locked={hostBusy}
774
+ onSave={() => { void onSave() }}
775
+ onDiscard={discardDraft}
776
+ />
770
777
  </div>
771
778
  )
772
779
  }
773
780
 
774
- /** Page header: title + phase badge + refresh, visible on every tab. */
781
+ /** Page header: title, phase pill, version and uptime, refresh; visible on every tab. */
775
782
  function PanelHeader(props: {
776
783
  status: ChannelStatusWire
777
784
  pollError: string | null
@@ -782,24 +789,24 @@ function PanelHeader(props: {
782
789
  const { status, pollError, busy, locked, onRefresh } = props
783
790
  const badge = phaseBadge(status.phase)
784
791
  return (
785
- <header className="dsh-channels-cfg__panelHead" title={PANEL_COPY.description}>
792
+ <header className="dsh-channels-cfg__panelHead">
786
793
  <div className="dsh-channels-cfg__headRow">
787
794
  <h2 className="dsh-channels-cfg__title">{PANEL_COPY.title}</h2>
788
- <span className={`dsh-channels-cfg__badge dsh-channels-cfg__badge--${badge.tone}`}>
789
- <span className="dsh-channels-cfg__dot" />
790
- {badge.text}
791
- </span>
795
+ <Pill tone={badge.tone} dot>{badge.text}</Pill>
796
+ <span className="dsh-channels-cfg__meta dsh-channels-cfg__mono">v{status.version}</span>
797
+ {status.uptimeMs === undefined
798
+ ? null
799
+ : <span className="dsh-channels-cfg__meta">· {PANEL_COPY.uptimePrefix} {formatUptime(status.uptimeMs)}</span>}
792
800
  <span className="dsh-channels-cfg__spacer" />
793
801
  <button
794
802
  type="button"
795
- className="dsh-channels-cfg__btn"
803
+ className="dsh-channels-cfg__btn dsh-channels-cfg__btn--ghost"
796
804
  onClick={onRefresh}
797
805
  disabled={busy !== null || locked}
798
806
  >
799
807
  {PANEL_COPY.refresh}
800
808
  </button>
801
809
  </div>
802
- <p className="dsh-channels-cfg__summary">{phaseSummary(status.phase)}</p>
803
810
  {pollError === null
804
811
  ? null
805
812
  : (
@@ -815,9 +822,11 @@ function PanelHeader(props: {
815
822
  function TabNav(props: {
816
823
  activeTab: PanelTabId
817
824
  dirtyTabs: ReadonlySet<PanelTabId>
825
+ /** Items waiting for the operator per tab (pending access requests). */
826
+ counts: Partial<Record<PanelTabId, number>>
818
827
  onChange: (tab: PanelTabId) => void
819
828
  }): JSX.Element {
820
- const { activeTab, dirtyTabs, onChange } = props
829
+ const { activeTab, dirtyTabs, counts, onChange } = props
821
830
  const refs = useRef<Array<HTMLButtonElement | null>>([])
822
831
 
823
832
  /** Roving-focus keyboard navigation between tabs (WAI-ARIA tabs pattern). */
@@ -856,12 +865,12 @@ function TabNav(props: {
856
865
  onKeyDown={event => onKeyDown(index, event)}
857
866
  >
858
867
  {tab.label}
868
+ {(counts[tab.id] ?? 0) > 0 ? <span className="dsh-channels-cfg__tabCount">{counts[tab.id]}</span> : null}
859
869
  {dirty
860
870
  ? (
861
871
  <span
862
872
  className="dsh-channels-cfg__tabDot"
863
- title={PANEL_COPY.dirtyCount}
864
- aria-label={PANEL_COPY.dirtyCount}
873
+ aria-hidden="true"
865
874
  />
866
875
  )
867
876
  : null}