@nextclaw/ui 0.15.0 → 0.15.2
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.
- package/CHANGELOG.md +50 -0
- package/dist/assets/{api-DLtsiBKm.js → api-B7Qigvtm.js} +2 -2
- package/dist/assets/{appearance-settings-page-CtkeYXux.js → appearance-settings-page-CuE0Iiqj.js} +1 -1
- package/dist/assets/{book-open-BQwWBDKb.js → book-open-Ba2f7YiG.js} +1 -1
- package/dist/assets/channels-list-page-CTsM6TcP.js +8 -0
- package/dist/assets/chat-page-C_5dcuE3.js +1 -0
- package/dist/assets/config-split-page-Fbb4PtTf.js +1 -0
- package/dist/assets/{confirm-dialog-iIJFFp2T.js → confirm-dialog-D2EG3nqE.js} +1 -1
- package/dist/assets/desktop-update-config-BGR3HUi1.js +1 -0
- package/dist/assets/{dist-D2eZC3OR.js → dist-7JTjycxE.js} +1 -1
- package/dist/assets/{dist-CBxWOazz.js → dist-wXLhk4hl.js} +1 -1
- package/dist/assets/doc-browser-bHyXnxlj.js +1 -0
- package/dist/assets/doc-browser-context-CYAGm5d5.js +1 -0
- package/dist/assets/doc-browser-gTrQ3jCF.js +1 -0
- package/dist/assets/{es2015-DhQo_oS6.js → es2015-j1SRCIX5.js} +1 -1
- package/dist/assets/{external-link-CM5dnUI6.js → external-link-DxuPaDoK.js} +1 -1
- package/dist/assets/form-actions-BztL7P_T.js +1 -0
- package/dist/assets/{i18n-provider-di1waTGX.js → i18n-provider-B4oMxOFx.js} +1 -1
- package/dist/assets/index-CvrvblgX.css +1 -0
- package/dist/assets/index-Obd4JYU2.js +104 -0
- package/dist/assets/loader-circle-CdzgK4eH.js +1 -0
- package/dist/assets/{mcp-marketplace-page-C_MVQUbT.js → mcp-marketplace-page-Bt7CuOoq.js} +2 -2
- package/dist/assets/mcp-marketplace-page-Ck3t78_n.js +1 -0
- package/dist/assets/model-config-page-DeKk9gvE.js +1 -0
- package/dist/assets/plus-Bx7wQbhG.js +1 -0
- package/dist/assets/{provider-scoped-model-input-g1WkGiJ8.js → provider-scoped-model-input-DsH2LF1q.js} +1 -1
- package/dist/assets/providers-config-page-BFnGp68F.js +1 -0
- package/dist/assets/{react-DjpzoZXB.js → react-UirzUuST.js} +1 -1
- package/dist/assets/{refresh-cw-DaTv3zxE.js → refresh-cw-eT8BF-8B.js} +1 -1
- package/dist/assets/remote-CDkHZtA8.js +1 -0
- package/dist/assets/rotate-cw-1JqxHTuw.js +1 -0
- package/dist/assets/runtime-config-page-BelAqiNG.js +1 -0
- package/dist/assets/{save-BZof1uie.js → save-Cb2g0BOi.js} +1 -1
- package/dist/assets/{search-MxAarS_g.js → search-BcHjwdsb.js} +1 -1
- package/dist/assets/search-config-page-zPm6--Ig.js +1 -0
- package/dist/assets/secrets-config-page-Cxyp-KuQ.js +3 -0
- package/dist/assets/security-config-B_gCq9fn.js +1 -0
- package/dist/assets/{select-D7GcNP0z.js → select-CdSEOwVM.js} +1 -1
- package/dist/assets/{setting-row-CuozlnJd.js → setting-row-CTAtGexr.js} +1 -1
- package/dist/assets/settings-2-2DWauOjY.js +1 -0
- package/dist/assets/{side-dock-BcLau5S_.js → side-dock-DPvakJ0a.js} +2 -2
- package/dist/assets/{skeleton-shfmXuYp.js → skeleton-P5oM14E1.js} +1 -1
- package/dist/assets/{switch-C1BWMU5c.js → switch-BU_UEULb.js} +1 -1
- package/dist/assets/{tag-chip-DCUrQUCP.js → tag-chip-D-WsKoT1.js} +1 -1
- package/dist/assets/{tooltip-DfAHHA76.js → tooltip-CjkOaHW7.js} +1 -1
- package/dist/assets/{use-config-DAdbT5QJ.js → use-config--hx5oxQn.js} +1 -1
- package/dist/assets/{use-confirm-dialog-eWcJ6V-s.js → use-confirm-dialog-CD9a_SK1.js} +1 -1
- package/dist/assets/x-DT5x45JU.js +1 -0
- package/dist/index.html +38 -30
- package/index.html +12 -4
- package/package.json +9 -9
- package/src/app/components/layout/__tests__/sidebar.layout.test.tsx +29 -1
- package/src/app/components/layout/sidebar-items.tsx +43 -4
- package/src/app/components/layout/sidebar.tsx +83 -61
- package/src/app/configs/app-navigation.config.ts +49 -0
- package/src/app/styles/design-system.css +82 -0
- package/src/features/agents/components/__tests__/agents-page.test.tsx +135 -2
- package/src/features/agents/components/agent-advanced-config-fields.tsx +70 -0
- package/src/features/agents/components/agent-details-dialog.tsx +398 -0
- package/src/features/agents/components/agent-dialogs.tsx +20 -47
- package/src/features/agents/components/agents-page.tsx +76 -10
- package/src/features/agents/types/agent-form.types.ts +21 -0
- package/src/features/agents/utils/agent-form.utils.ts +39 -0
- package/src/features/channels/components/config/channel-form.tsx +14 -11
- package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +172 -1
- package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +11 -1
- package/src/features/chat/components/conversation/chat-conversation-header.tsx +7 -3
- package/src/features/chat/components/conversation/chat-conversation-panel.tsx +1 -1
- package/src/features/chat/components/layout/__tests__/chat-sidebar-toolbar.test.tsx +48 -0
- package/src/features/chat/components/layout/__tests__/chat-sidebar-utility-menu.test.tsx +55 -0
- package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +0 -3
- package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +3 -4
- package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +29 -30
- package/src/features/chat/components/layout/chat-sidebar.tsx +21 -7
- package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +18 -3
- package/src/features/chat/features/conversation/components/session-conversation-input.tsx +2 -1
- package/src/features/chat/features/message/components/__tests__/chat-inline-panel-app-card.test.tsx +22 -1
- package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +1 -1
- package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +10 -5
- package/src/features/chat/features/message/utils/chat-inline-panel-app-card.utils.ts +0 -11
- package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-agent-runtime.test.tsx +23 -1
- package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-session-list-view.test.tsx +74 -0
- package/src/features/chat/features/ncp/hooks/use-ncp-session-list-view.ts +3 -2
- package/src/features/chat/features/session/components/__tests__/chat-sidebar-project-groups.test.tsx +116 -0
- package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +108 -46
- package/src/features/chat/features/session/components/chat-sidebar-session-entry.tsx +6 -0
- package/src/features/chat/features/session/components/chat-sidebar-session-item.tsx +48 -24
- package/src/features/chat/features/session/components/chat-sidebar-session-list.tsx +0 -3
- package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +0 -4
- package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +294 -0
- package/src/features/chat/features/session/utils/__tests__/chat-session-display.utils.test.ts +15 -1
- package/src/features/chat/features/session/utils/__tests__/ncp-session-adapter.utils.test.ts +24 -4
- package/src/features/chat/features/session/utils/chat-session-display.utils.ts +3 -0
- package/src/features/chat/features/session/utils/chat-sidebar-session-groups.utils.ts +25 -3
- package/src/features/chat/features/session/utils/ncp-session-adapter.utils.ts +1 -0
- package/src/features/chat/features/welcome/components/chat-welcome-capability-grid.tsx +9 -4
- package/src/features/chat/features/welcome/components/chat-welcome.tsx +0 -5
- package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +193 -56
- package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +1 -0
- package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel.test.tsx +144 -0
- package/src/features/chat/features/workspace/components/chat-session-workspace-directory-browser.tsx +99 -0
- package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumbs.tsx +3 -1
- package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +67 -14
- package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +3 -0
- package/src/features/chat/features/workspace/components/chat-session-workspace-panel-nav.tsx +44 -1
- package/src/features/chat/features/workspace/components/chat-session-workspace-panel.tsx +45 -3
- package/src/features/chat/features/workspace/utils/__tests__/chat-workspace-panel-view-model.utils.test.ts +46 -0
- package/src/features/chat/managers/__tests__/chat-session-list.manager.test.ts +33 -2
- package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +47 -0
- package/src/features/chat/managers/chat-session-list.manager.ts +32 -0
- package/src/features/chat/managers/chat-thread.manager.ts +8 -2
- package/src/features/chat/stores/chat-session-list.store.ts +28 -5
- package/src/features/marketplace/components/mcp/mcp-marketplace-page.tsx +5 -12
- package/src/features/panel-apps/components/panel-app-list-item.tsx +6 -6
- package/src/features/panel-apps/index.ts +1 -0
- package/src/features/panel-apps/utils/panel-app-doc-browser.utils.tsx +2 -10
- package/src/features/panel-apps/utils/panel-app-iframe.utils.ts +6 -0
- package/src/features/pwa/managers/__tests__/pwa-shell-theme.manager.test.ts +11 -3
- package/src/features/pwa/managers/pwa-shell-theme.manager.ts +3 -1
- package/src/features/service-apps/components/service-apps-panel.tsx +145 -35
- package/src/features/settings/components/config/provider-auth-section.tsx +1 -1
- package/src/features/settings/components/config/provider-form.tsx +11 -8
- package/src/features/settings/components/config/provider-models-section.tsx +7 -7
- package/src/features/settings/components/config/provider-pill-selector.tsx +1 -1
- package/src/features/settings/components/config/secrets-config-form.tsx +5 -4
- package/src/features/settings/pages/model-config-page.tsx +26 -25
- package/src/features/settings/pages/providers-config-page.tsx +10 -13
- package/src/features/settings/pages/search-config-page.tsx +8 -5
- package/src/features/system-status/components/__tests__/desktop-update-config.test.tsx +97 -5
- package/src/features/system-status/components/config/runtime-config-editor.tsx +5 -4
- package/src/features/system-status/components/desktop-update-config.tsx +91 -2
- package/src/features/system-status/utils/update-release-notes.utils.ts +94 -0
- package/src/index.css +18 -0
- package/src/shared/components/config-split-page.tsx +6 -6
- package/src/shared/components/doc-browser/__tests__/doc-browser-url.utils.test.ts +73 -0
- package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +48 -0
- package/src/shared/components/doc-browser/doc-browser-context.tsx +4 -0
- package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +5 -1
- package/src/shared/components/doc-browser/doc-browser-renderer.types.ts +2 -1
- package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +10 -10
- package/src/shared/components/doc-browser/doc-browser.tsx +6 -4
- package/src/shared/components/doc-browser/utils/doc-browser-url.utils.ts +62 -4
- package/src/shared/components/resizable-right-panel/__tests__/resizable-right-panel.test.tsx +18 -0
- package/src/shared/components/resizable-right-panel/resizable-right-panel.tsx +28 -3
- package/src/shared/components/ui/actions/__tests__/icon-action-button.test.tsx +36 -0
- package/src/shared/components/ui/actions/form-actions.tsx +25 -0
- package/src/shared/components/ui/actions/icon-action-button.tsx +25 -2
- package/src/shared/components/ui/card.tsx +12 -4
- package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +2 -1
- package/src/shared/hooks/use-server-path-browse.ts +26 -4
- package/src/shared/hooks/use-server-path-read.ts +11 -1
- package/src/shared/lib/api/ncp-session.types.ts +1 -1
- package/src/shared/lib/api/types.ts +12 -3
- package/src/shared/lib/api/utils/server-path.utils.ts +1 -0
- package/src/shared/lib/i18n/locales/en-US/agents.json +41 -0
- package/src/shared/lib/i18n/locales/en-US/chat.json +19 -0
- package/src/shared/lib/i18n/locales/en-US/core.json +3 -0
- package/src/shared/lib/i18n/locales/en-US/desktop-update.json +8 -0
- package/src/shared/lib/i18n/locales/en-US/doc-browser.json +5 -0
- package/src/shared/lib/i18n/locales/zh-CN/agents.json +41 -0
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +19 -0
- package/src/shared/lib/i18n/locales/zh-CN/core.json +3 -0
- package/src/shared/lib/i18n/locales/zh-CN/desktop-update.json +8 -0
- package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +5 -0
- package/src/shared/lib/session-project/workspace-file-breadcrumb.utils.ts +9 -5
- package/src/shared/lib/theme/index.test.ts +21 -0
- package/src/shared/lib/theme/index.ts +6 -4
- package/src/vite-env.d.ts +2 -0
- package/dist/assets/channels-list-page-Cv8o9Hxg.js +0 -8
- package/dist/assets/chat-page-BtA7H1Ea.js +0 -1
- package/dist/assets/config-split-page-EidRBpP2.js +0 -1
- package/dist/assets/desktop-update-config-C6n_TB1r.js +0 -1
- package/dist/assets/doc-browser-B76XOXmW.js +0 -1
- package/dist/assets/doc-browser-DBnj9UMB.js +0 -1
- package/dist/assets/doc-browser-context-Sv-044Cl.js +0 -1
- package/dist/assets/ellipsis-2uzhypw5.js +0 -1
- package/dist/assets/index-CBRP-7-a.css +0 -1
- package/dist/assets/index-Z1fYpbNd.js +0 -104
- package/dist/assets/loader-circle-BjmN5DeJ.js +0 -1
- package/dist/assets/mcp-marketplace-page-CoZIiD6G.js +0 -1
- package/dist/assets/model-config-page-Cdr9P6TQ.js +0 -1
- package/dist/assets/plus-3ztRZD_l.js +0 -1
- package/dist/assets/providers-config-page-CWlO7J4A.js +0 -1
- package/dist/assets/remote-CKWZfVVB.js +0 -1
- package/dist/assets/rotate-cw-CHcaRUDz.js +0 -1
- package/dist/assets/runtime-config-page-CfTqh1nv.js +0 -1
- package/dist/assets/search-config-page-Dm8daYny.js +0 -1
- package/dist/assets/secrets-config-page-D8AbcR05.js +0 -3
- package/dist/assets/security-config-Ds0LsJmo.js +0 -1
- package/dist/assets/x-DqL1mYLC.js +0 -1
- /package/dist/assets/{config-hints-fGnUjDe9.js → config-hints-C_iP1zl_.js} +0 -0
|
@@ -99,7 +99,7 @@ export function ProviderModelsSection(props: ProviderModelsSectionProps) {
|
|
|
99
99
|
) : null}
|
|
100
100
|
|
|
101
101
|
{models.length === 0 ? (
|
|
102
|
-
<div className="rounded-xl border border-dashed border-border bg-muted/
|
|
102
|
+
<div className="rounded-xl border border-dashed border-border/65 bg-muted/30 px-4 py-6 text-center">
|
|
103
103
|
<p className="text-sm text-muted-foreground">{t('providerModelsEmptyShort')}</p>
|
|
104
104
|
{!showModelInput ? (
|
|
105
105
|
<button
|
|
@@ -123,14 +123,14 @@ export function ProviderModelsSection(props: ProviderModelsSectionProps) {
|
|
|
123
123
|
return (
|
|
124
124
|
<div
|
|
125
125
|
key={modelName}
|
|
126
|
-
className="group inline-flex max-w-full items-center gap-1 rounded-full border border-border bg-
|
|
126
|
+
className="group inline-flex max-w-full items-center gap-1 rounded-full border border-border/55 bg-muted/45 px-3 py-1.5"
|
|
127
127
|
>
|
|
128
128
|
<span className="max-w-[140px] truncate text-sm text-foreground sm:max-w-[220px]">{modelName}</span>
|
|
129
129
|
<Popover>
|
|
130
130
|
<PopoverTrigger asChild>
|
|
131
131
|
<button
|
|
132
132
|
type="button"
|
|
133
|
-
className="inline-flex h-5 w-5 items-center justify-center rounded-full text-muted-foreground/70 opacity-100 transition-opacity hover:bg-
|
|
133
|
+
className="inline-flex h-5 w-5 items-center justify-center rounded-full text-muted-foreground/70 opacity-100 transition-opacity hover:bg-muted hover:text-foreground md:opacity-0 md:group-hover:opacity-100 md:group-focus-within:opacity-100"
|
|
134
134
|
aria-label={t('providerModelThinkingTitle')}
|
|
135
135
|
title={t('providerModelThinkingTitle')}
|
|
136
136
|
>
|
|
@@ -138,7 +138,7 @@ export function ProviderModelsSection(props: ProviderModelsSectionProps) {
|
|
|
138
138
|
</button>
|
|
139
139
|
</PopoverTrigger>
|
|
140
140
|
<PopoverContent className="w-80 space-y-3">
|
|
141
|
-
<div className="flex items-center justify-between gap-3 rounded-lg
|
|
141
|
+
<div className="flex items-center justify-between gap-3 rounded-lg bg-muted/50 px-3 py-2">
|
|
142
142
|
<div className="min-w-0">
|
|
143
143
|
<p className="text-xs font-semibold text-foreground">{t('providerModelVisionTitle')}</p>
|
|
144
144
|
<p className="text-xs text-muted-foreground">{t('providerModelVisionHint')}</p>
|
|
@@ -159,8 +159,8 @@ export function ProviderModelsSection(props: ProviderModelsSectionProps) {
|
|
|
159
159
|
onClick={() => onToggleModelThinkingLevel(modelName, level)}
|
|
160
160
|
className={`rounded-full border px-2.5 py-1 text-xs font-medium transition-colors ${
|
|
161
161
|
selected
|
|
162
|
-
? 'border-
|
|
163
|
-
: 'border-border bg-
|
|
162
|
+
? 'border-foreground/15 bg-foreground text-background'
|
|
163
|
+
: 'border-border/55 bg-muted/50 text-muted-foreground hover:border-border hover:bg-muted hover:text-foreground'
|
|
164
164
|
}`}
|
|
165
165
|
>
|
|
166
166
|
{formatThinkingLevelLabel(level)}
|
|
@@ -198,7 +198,7 @@ export function ProviderModelsSection(props: ProviderModelsSectionProps) {
|
|
|
198
198
|
<button
|
|
199
199
|
type="button"
|
|
200
200
|
onClick={() => onRemoveModel(modelName)}
|
|
201
|
-
className="inline-flex h-5 w-5 items-center justify-center rounded-full text-muted-foreground/70 opacity-100 transition-opacity hover:bg-
|
|
201
|
+
className="inline-flex h-5 w-5 items-center justify-center rounded-full text-muted-foreground/70 opacity-100 transition-opacity hover:bg-muted hover:text-foreground md:opacity-0 md:group-hover:opacity-100 md:group-focus-within:opacity-100"
|
|
202
202
|
aria-label={t('remove')}
|
|
203
203
|
>
|
|
204
204
|
<X className="h-3 w-3" />
|
|
@@ -16,7 +16,7 @@ export function ProviderPillSelector(props: ProviderPillSelectorProps) {
|
|
|
16
16
|
type="button"
|
|
17
17
|
onClick={() => onChange(option.value)}
|
|
18
18
|
aria-pressed={selected}
|
|
19
|
-
className={`rounded-full border px-3 py-1.5 text-xs font-medium transition-colors ${selected ? 'border-
|
|
19
|
+
className={`rounded-full border px-3 py-1.5 text-xs font-medium transition-colors ${selected ? 'border-foreground/15 bg-foreground text-background' : 'border-border/55 bg-muted/50 text-muted-foreground hover:border-border hover:bg-muted hover:text-foreground'}`}
|
|
20
20
|
>
|
|
21
21
|
{option.label}
|
|
22
22
|
</button>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useMemo, useState } from 'react';
|
|
2
2
|
import { Plus, Save, Trash2 } from 'lucide-react';
|
|
3
3
|
import { Button } from '@/shared/components/ui/button';
|
|
4
|
+
import { FormActions } from '@/shared/components/ui/actions/form-actions';
|
|
4
5
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/shared/components/ui/card';
|
|
5
6
|
import { Input } from '@/shared/components/ui/input';
|
|
6
7
|
import { Label } from '@/shared/components/ui/label';
|
|
@@ -272,12 +273,12 @@ export function SecretsConfigForm(props: {
|
|
|
272
273
|
onRemove={(index) => setState((prev) => ({ ...prev, refs: prev.refs.filter((_, cursor) => cursor !== index) }))}
|
|
273
274
|
onAdd={() => setState((prev) => ({ ...prev, refs: [...prev.refs, createRefRow()] }))}
|
|
274
275
|
/>
|
|
275
|
-
<
|
|
276
|
-
<Button type="button" onClick={handleSave} disabled={isPending}>
|
|
277
|
-
<Save className="mr-
|
|
276
|
+
<FormActions>
|
|
277
|
+
<Button type="button" size="sm" onClick={handleSave} disabled={isPending}>
|
|
278
|
+
<Save className="mr-1.5 h-3.5 w-3.5" />
|
|
278
279
|
{isPending ? t('saving') : t('save')}
|
|
279
280
|
</Button>
|
|
280
|
-
</
|
|
281
|
+
</FormActions>
|
|
281
282
|
</PageLayout>
|
|
282
283
|
);
|
|
283
284
|
}
|
|
@@ -3,13 +3,14 @@ import { Card } from '@/shared/components/ui/card';
|
|
|
3
3
|
import { Input } from '@/shared/components/ui/input';
|
|
4
4
|
import { Label } from '@/shared/components/ui/label';
|
|
5
5
|
import { Skeleton } from '@/shared/components/ui/skeleton';
|
|
6
|
+
import { FormActions } from '@/shared/components/ui/actions/form-actions';
|
|
6
7
|
import { ProviderScopedModelInput } from '@/shared/components/common/provider-scoped-model-input';
|
|
7
8
|
import { useConfig, useConfigSchema, useProviderTemplates, useProviders, useUpdateModel } from '@/shared/hooks/use-config';
|
|
8
9
|
import { hintForPath } from '@/shared/lib/config-hints';
|
|
9
10
|
import { t } from '@/shared/lib/i18n';
|
|
10
11
|
import { buildProviderModelCatalog } from '@/shared/lib/provider-models';
|
|
11
12
|
import { PageLayout, PageHeader } from '@/app/components/layout/page-layout';
|
|
12
|
-
import {
|
|
13
|
+
import { getDocsUrl } from '@/shared/components/doc-browser/doc-browser-context';
|
|
13
14
|
import { BookOpen, Folder, Loader2, Sparkles } from 'lucide-react';
|
|
14
15
|
import { useMemo, useState } from 'react';
|
|
15
16
|
|
|
@@ -37,48 +38,48 @@ function ModelConfigForm(props: {
|
|
|
37
38
|
event.preventDefault();
|
|
38
39
|
onSubmit({ model, workspace });
|
|
39
40
|
}}
|
|
40
|
-
className="space-y-
|
|
41
|
+
className="space-y-5"
|
|
41
42
|
>
|
|
42
|
-
<div className="grid grid-cols-1 gap-
|
|
43
|
-
<div className="rounded-2xl border border-
|
|
44
|
-
<div className="mb-
|
|
45
|
-
<div className="flex h-
|
|
46
|
-
<Sparkles className="h-
|
|
43
|
+
<div className="grid grid-cols-1 gap-5 md:grid-cols-2">
|
|
44
|
+
<div className="rounded-2xl border border-border/75 bg-card p-6">
|
|
45
|
+
<div className="mb-6 flex items-center gap-3">
|
|
46
|
+
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-muted text-muted-foreground">
|
|
47
|
+
<Sparkles className="h-4 w-4" />
|
|
47
48
|
</div>
|
|
48
|
-
<h3 className="text-
|
|
49
|
+
<h3 className="text-base font-semibold text-foreground">{t('defaultModel')}</h3>
|
|
49
50
|
</div>
|
|
50
51
|
<div className="space-y-2">
|
|
51
|
-
<Label htmlFor="model" className="text-xs font-
|
|
52
|
+
<Label htmlFor="model" className="text-xs font-medium text-muted-foreground">
|
|
52
53
|
{modelLabel}
|
|
53
54
|
</Label>
|
|
54
55
|
<ProviderScopedModelInput id="model" value={model} onChange={setModel} providerCatalog={providerCatalog} modelPlaceholder={modelPlaceholder} />
|
|
55
|
-
<p className="text-xs text-
|
|
56
|
-
<a href={
|
|
56
|
+
<p className="text-xs text-muted-foreground/80">{modelHelpText}</p>
|
|
57
|
+
<a href={getDocsUrl('/guide/model-selection')} className="inline-flex items-center gap-1.5 text-xs text-primary transition-colors hover:text-primary-hover">
|
|
57
58
|
<BookOpen className="h-3.5 w-3.5" />
|
|
58
59
|
{t('channelsGuideTitle')}
|
|
59
60
|
</a>
|
|
60
61
|
</div>
|
|
61
62
|
</div>
|
|
62
|
-
<div className="rounded-2xl border border-
|
|
63
|
-
<div className="mb-
|
|
64
|
-
<div className="flex h-
|
|
65
|
-
<Folder className="h-
|
|
63
|
+
<div className="rounded-2xl border border-border/75 bg-card p-6">
|
|
64
|
+
<div className="mb-6 flex items-center gap-3">
|
|
65
|
+
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-muted text-muted-foreground">
|
|
66
|
+
<Folder className="h-4 w-4" />
|
|
66
67
|
</div>
|
|
67
|
-
<h3 className="text-
|
|
68
|
+
<h3 className="text-base font-semibold text-foreground">{t('workspace')}</h3>
|
|
68
69
|
</div>
|
|
69
70
|
<div className="space-y-2">
|
|
70
|
-
<Label htmlFor="workspace" className="text-xs font-
|
|
71
|
+
<Label htmlFor="workspace" className="text-xs font-medium text-muted-foreground">
|
|
71
72
|
{workspaceLabel}
|
|
72
73
|
</Label>
|
|
73
74
|
<Input id="workspace" value={workspace} onChange={(event) => setWorkspace(event.target.value)} placeholder={workspacePlaceholder} className="rounded-xl" />
|
|
74
75
|
</div>
|
|
75
76
|
</div>
|
|
76
77
|
</div>
|
|
77
|
-
<
|
|
78
|
-
<Button type="submit" disabled={isPending} size="
|
|
79
|
-
{isPending ? <Loader2 className="h-5 w-5 animate-spin" /> : t('saveChanges')}
|
|
78
|
+
<FormActions>
|
|
79
|
+
<Button type="submit" disabled={isPending} size="sm">
|
|
80
|
+
{isPending ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : t('saveChanges')}
|
|
80
81
|
</Button>
|
|
81
|
-
</
|
|
82
|
+
</FormActions>
|
|
82
83
|
</form>
|
|
83
84
|
);
|
|
84
85
|
}
|
|
@@ -108,9 +109,9 @@ export function ModelConfigPage() {
|
|
|
108
109
|
<Skeleton className="h-8 w-32" />
|
|
109
110
|
<Skeleton className="h-4 w-48" />
|
|
110
111
|
</div>
|
|
111
|
-
<Card className="rounded-2xl border-
|
|
112
|
-
<div className="mb-6 flex items-center gap-
|
|
113
|
-
<Skeleton className="h-
|
|
112
|
+
<Card hover={false} className="rounded-2xl border-border/75 p-6 shadow-none">
|
|
113
|
+
<div className="mb-6 flex items-center gap-3">
|
|
114
|
+
<Skeleton className="h-9 w-9 rounded-lg" />
|
|
114
115
|
<div className="space-y-2">
|
|
115
116
|
<Skeleton className="h-5 w-24" />
|
|
116
117
|
<Skeleton className="h-3 w-32" />
|
|
@@ -119,7 +120,7 @@ export function ModelConfigPage() {
|
|
|
119
120
|
<Skeleton className="mb-2 h-4 w-20" />
|
|
120
121
|
<Skeleton className="h-10 w-full rounded-xl" />
|
|
121
122
|
</Card>
|
|
122
|
-
<Card className="rounded-2xl border-
|
|
123
|
+
<Card hover={false} className="rounded-2xl border-border/75 p-6 shadow-none">
|
|
123
124
|
<Skeleton className="mb-2 h-5 w-24" />
|
|
124
125
|
<Skeleton className="h-10 w-full rounded-xl" />
|
|
125
126
|
</Card>
|
|
@@ -137,7 +137,7 @@ export function ProvidersConfigPage() {
|
|
|
137
137
|
<div className="grid max-h-[24rem] grid-cols-2 gap-2 overflow-y-auto sm:grid-cols-3">
|
|
138
138
|
<button
|
|
139
139
|
type="button"
|
|
140
|
-
className="flex min-h-20 w-full flex-col items-start gap-2 rounded-lg border border-dashed border-border bg-
|
|
140
|
+
className="flex min-h-20 w-full flex-col items-start gap-2 rounded-lg border border-dashed border-border/70 bg-muted/20 px-3 py-2 text-left transition-colors hover:border-border hover:bg-muted/45"
|
|
141
141
|
onClick={async () => {
|
|
142
142
|
try {
|
|
143
143
|
const result = await createProvider.mutateAsync({ data: { providerType: null } });
|
|
@@ -150,7 +150,7 @@ export function ProvidersConfigPage() {
|
|
|
150
150
|
}
|
|
151
151
|
}}
|
|
152
152
|
>
|
|
153
|
-
<LogoBadge name="custom" className="h-8 w-8 rounded-lg border border-border bg-
|
|
153
|
+
<LogoBadge name="custom" className="h-8 w-8 rounded-lg border border-border/60 bg-muted/50" />
|
|
154
154
|
<span className="line-clamp-2 min-w-0 text-xs font-semibold leading-4 text-foreground">
|
|
155
155
|
{t('providerAddCustom')}
|
|
156
156
|
</span>
|
|
@@ -159,7 +159,7 @@ export function ProvidersConfigPage() {
|
|
|
159
159
|
<button
|
|
160
160
|
key={template.providerType}
|
|
161
161
|
type="button"
|
|
162
|
-
className="flex min-h-20 w-full flex-col items-start gap-2 rounded-lg border border-border bg-
|
|
162
|
+
className="flex min-h-20 w-full flex-col items-start gap-2 rounded-lg border border-border/55 bg-muted/15 px-3 py-2 text-left transition-colors hover:border-border/80 hover:bg-muted/40"
|
|
163
163
|
onClick={async () => {
|
|
164
164
|
try {
|
|
165
165
|
const result = await createProvider.mutateAsync({ data: { providerType: template.providerType } });
|
|
@@ -175,7 +175,7 @@ export function ProvidersConfigPage() {
|
|
|
175
175
|
<LogoBadge
|
|
176
176
|
name={template.providerType}
|
|
177
177
|
src={template.logo ? `/logos/${template.logo}` : null}
|
|
178
|
-
className="h-8 w-8 rounded-lg border border-border bg-background"
|
|
178
|
+
className="h-8 w-8 rounded-lg border border-border/55 bg-background"
|
|
179
179
|
imgClassName="h-4 w-4 object-contain"
|
|
180
180
|
/>
|
|
181
181
|
<span className="line-clamp-2 min-w-0 text-xs font-semibold leading-4 text-foreground">
|
|
@@ -221,10 +221,10 @@ export function ProvidersConfigPage() {
|
|
|
221
221
|
role="button"
|
|
222
222
|
tabIndex={0}
|
|
223
223
|
className={cn(
|
|
224
|
-
'w-full rounded-xl border p-2.5 text-left transition-
|
|
224
|
+
'group w-full rounded-xl border p-2.5 text-left transition-colors',
|
|
225
225
|
resolvedSelectedProvider === provider.providerId
|
|
226
|
-
? 'border-
|
|
227
|
-
: 'border-
|
|
226
|
+
? 'border-border/70 bg-muted/65 text-foreground'
|
|
227
|
+
: 'border-transparent bg-transparent text-muted-foreground hover:border-border/50 hover:bg-muted/40 hover:text-foreground',
|
|
228
228
|
)}
|
|
229
229
|
>
|
|
230
230
|
<div className="relative min-h-10">
|
|
@@ -232,10 +232,7 @@ export function ProvidersConfigPage() {
|
|
|
232
232
|
<LogoBadge
|
|
233
233
|
name={provider.providerType ?? provider.providerId}
|
|
234
234
|
src={template?.logo ? `/logos/${template.logo}` : null}
|
|
235
|
-
className=
|
|
236
|
-
'h-10 w-10 rounded-lg border',
|
|
237
|
-
isReady ? 'border-primary/35 bg-background' : 'border-border bg-background',
|
|
238
|
-
)}
|
|
235
|
+
className="h-10 w-10 rounded-lg border border-border/55 bg-muted/40"
|
|
239
236
|
imgClassName="h-5 w-5 object-contain"
|
|
240
237
|
fallback={<span className="text-sm font-semibold uppercase text-muted-foreground">{provider.providerId[0]}</span>}
|
|
241
238
|
/>
|
|
@@ -247,7 +244,7 @@ export function ProvidersConfigPage() {
|
|
|
247
244
|
</p>
|
|
248
245
|
</div>
|
|
249
246
|
</div>
|
|
250
|
-
<div className="absolute right-0 top-0 flex items-center gap-1.5">
|
|
247
|
+
<div className="absolute right-0 top-0 flex items-center gap-1.5 opacity-70 transition-opacity group-hover:opacity-100">
|
|
251
248
|
<div onClick={(event) => event.stopPropagation()}>
|
|
252
249
|
<Switch
|
|
253
250
|
checked={isEnabled}
|
|
@@ -268,7 +265,7 @@ export function ProvidersConfigPage() {
|
|
|
268
265
|
<PopoverTrigger asChild>
|
|
269
266
|
<button
|
|
270
267
|
type="button"
|
|
271
|
-
className="inline-flex h-7 w-7 items-center justify-center rounded-lg text-muted-foreground/70 hover:bg-
|
|
268
|
+
className="inline-flex h-7 w-7 items-center justify-center rounded-lg text-muted-foreground/70 hover:bg-muted hover:text-foreground"
|
|
272
269
|
onClick={(event) => event.stopPropagation()}
|
|
273
270
|
title={t('more')}
|
|
274
271
|
>
|
|
@@ -2,6 +2,7 @@ import { useState } from "react";
|
|
|
2
2
|
import { ExternalLink, KeyRound, Search as SearchIcon } from "lucide-react";
|
|
3
3
|
import { PageHeader, PageLayout } from "@/app/components/layout/page-layout";
|
|
4
4
|
import { Button } from "@/shared/components/ui/button";
|
|
5
|
+
import { FormActions } from "@/shared/components/ui/actions/form-actions";
|
|
5
6
|
import { Input } from "@/shared/components/ui/input";
|
|
6
7
|
import { Label } from "@/shared/components/ui/label";
|
|
7
8
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/shared/components/ui/select";
|
|
@@ -456,11 +457,13 @@ function SearchConfigForm(props: {
|
|
|
456
457
|
/>
|
|
457
458
|
</ConfigSplitPaneBody>
|
|
458
459
|
|
|
459
|
-
<ConfigSplitPaneFooter
|
|
460
|
-
<
|
|
461
|
-
<
|
|
462
|
-
|
|
463
|
-
|
|
460
|
+
<ConfigSplitPaneFooter>
|
|
461
|
+
<FormActions>
|
|
462
|
+
<Button type="submit" size="sm" disabled={isPending}>
|
|
463
|
+
<KeyRound className="mr-1.5 h-3.5 w-3.5" />
|
|
464
|
+
{isPending ? t("saving") : t("saveChanges")}
|
|
465
|
+
</Button>
|
|
466
|
+
</FormActions>
|
|
464
467
|
</ConfigSplitPaneFooter>
|
|
465
468
|
</form>
|
|
466
469
|
</ConfigSplitDetailPane>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { render, screen } from '@testing-library/react';
|
|
2
2
|
import userEvent from '@testing-library/user-event';
|
|
3
|
-
import {
|
|
3
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
5
|
import { useRuntimeUpdateStore } from '@/features/system-status';
|
|
5
6
|
import { DesktopUpdateConfig } from '@/features/system-status/components/desktop-update-config';
|
|
6
7
|
import { setLanguage } from '@/shared/lib/i18n';
|
|
@@ -26,6 +27,22 @@ vi.mock('@/features/system-status', async () => {
|
|
|
26
27
|
};
|
|
27
28
|
});
|
|
28
29
|
|
|
30
|
+
function renderDesktopUpdateConfig() {
|
|
31
|
+
const queryClient = new QueryClient({
|
|
32
|
+
defaultOptions: {
|
|
33
|
+
queries: {
|
|
34
|
+
gcTime: 0,
|
|
35
|
+
retry: false
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
return render(
|
|
40
|
+
<QueryClientProvider client={queryClient}>
|
|
41
|
+
<DesktopUpdateConfig />
|
|
42
|
+
</QueryClientProvider>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
29
46
|
describe('DesktopUpdateConfig', () => {
|
|
30
47
|
beforeEach(() => {
|
|
31
48
|
setLanguage('zh');
|
|
@@ -60,7 +77,7 @@ describe('DesktopUpdateConfig', () => {
|
|
|
60
77
|
availableVersion: '0.18.2-beta.1',
|
|
61
78
|
downloadedVersion: null,
|
|
62
79
|
minimumHostVersion: null,
|
|
63
|
-
releaseNotesUrl:
|
|
80
|
+
releaseNotesUrl: null,
|
|
64
81
|
lastCheckedAt: '2026-04-13T12:00:00.000Z',
|
|
65
82
|
progress: null,
|
|
66
83
|
canAutoDownload: false,
|
|
@@ -77,8 +94,12 @@ describe('DesktopUpdateConfig', () => {
|
|
|
77
94
|
});
|
|
78
95
|
});
|
|
79
96
|
|
|
97
|
+
afterEach(() => {
|
|
98
|
+
vi.unstubAllGlobals();
|
|
99
|
+
});
|
|
100
|
+
|
|
80
101
|
it('renders current channel information and beta guidance', () => {
|
|
81
|
-
|
|
102
|
+
renderDesktopUpdateConfig();
|
|
82
103
|
|
|
83
104
|
expect(mocks.start).not.toHaveBeenCalled();
|
|
84
105
|
expect(mocks.stop).not.toHaveBeenCalled();
|
|
@@ -93,7 +114,7 @@ describe('DesktopUpdateConfig', () => {
|
|
|
93
114
|
it('sends the newly selected release channel to the desktop update manager', async () => {
|
|
94
115
|
const user = userEvent.setup();
|
|
95
116
|
|
|
96
|
-
|
|
117
|
+
renderDesktopUpdateConfig();
|
|
97
118
|
|
|
98
119
|
await user.click(screen.getByRole('combobox'));
|
|
99
120
|
await user.click(screen.getByRole('option', { name: 'Stable' }));
|
|
@@ -118,9 +139,80 @@ describe('DesktopUpdateConfig', () => {
|
|
|
118
139
|
: null
|
|
119
140
|
}));
|
|
120
141
|
|
|
121
|
-
|
|
142
|
+
renderDesktopUpdateConfig();
|
|
122
143
|
|
|
123
144
|
expect(screen.getByText('正在下载 50%')).toBeTruthy();
|
|
124
145
|
expect(screen.getByText('50 B / 100 B')).toBeTruthy();
|
|
125
146
|
});
|
|
147
|
+
|
|
148
|
+
it('loads structured release notes from the docs JSON endpoint', async () => {
|
|
149
|
+
const fetchMock = vi.fn(async () => new Response(JSON.stringify({
|
|
150
|
+
schemaVersion: 1,
|
|
151
|
+
product: 'NextClaw',
|
|
152
|
+
version: '0.22.0',
|
|
153
|
+
sections: [
|
|
154
|
+
{
|
|
155
|
+
kind: 'feature',
|
|
156
|
+
title: {
|
|
157
|
+
'zh-CN': '功能',
|
|
158
|
+
'en-US': 'Features'
|
|
159
|
+
},
|
|
160
|
+
items: [
|
|
161
|
+
{
|
|
162
|
+
title: {
|
|
163
|
+
'zh-CN': '更新前查看内容',
|
|
164
|
+
'en-US': 'Review before updating'
|
|
165
|
+
},
|
|
166
|
+
body: {
|
|
167
|
+
'zh-CN': '更新提示可以直接展示本版本的结构化说明。',
|
|
168
|
+
'en-US': 'The update prompt can show structured notes for this version.'
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
]
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
kind: 'fix',
|
|
175
|
+
title: {
|
|
176
|
+
'zh-CN': '修复',
|
|
177
|
+
'en-US': 'Fixes'
|
|
178
|
+
},
|
|
179
|
+
items: [
|
|
180
|
+
{
|
|
181
|
+
title: {
|
|
182
|
+
'zh-CN': '附件发送修复',
|
|
183
|
+
'en-US': 'Attachment delivery fix'
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
]
|
|
187
|
+
}
|
|
188
|
+
]
|
|
189
|
+
}), {
|
|
190
|
+
headers: { 'content-type': 'application/json' },
|
|
191
|
+
status: 200
|
|
192
|
+
}));
|
|
193
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
194
|
+
useRuntimeUpdateStore.setState((state) => ({
|
|
195
|
+
...state,
|
|
196
|
+
snapshot: state.snapshot
|
|
197
|
+
? {
|
|
198
|
+
...state.snapshot,
|
|
199
|
+
status: 'update-available',
|
|
200
|
+
availableVersion: '0.22.0',
|
|
201
|
+
releaseNotesUrl: 'https://docs.nextclaw.io/zh/notes/2026-07-05-nextclaw-v0-22-0'
|
|
202
|
+
}
|
|
203
|
+
: null
|
|
204
|
+
}));
|
|
205
|
+
|
|
206
|
+
renderDesktopUpdateConfig();
|
|
207
|
+
|
|
208
|
+
expect(await screen.findByText('更新前查看内容')).toBeTruthy();
|
|
209
|
+
expect(screen.getByText('修复')).toBeTruthy();
|
|
210
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
211
|
+
'https://docs.nextclaw.io/release-notes/nextclaw-v0.22.0.json',
|
|
212
|
+
expect.objectContaining({
|
|
213
|
+
cache: 'no-store',
|
|
214
|
+
headers: { accept: 'application/json' }
|
|
215
|
+
})
|
|
216
|
+
);
|
|
217
|
+
});
|
|
126
218
|
});
|
|
@@ -2,6 +2,7 @@ import { useMemo, useState } from 'react';
|
|
|
2
2
|
import { Save } from 'lucide-react';
|
|
3
3
|
import type { ConfigView, RuntimeConfigUpdate } from '@/shared/lib/api';
|
|
4
4
|
import { Button } from '@/shared/components/ui/button';
|
|
5
|
+
import { FormActions } from '@/shared/components/ui/actions/form-actions';
|
|
5
6
|
import { PageLayout } from '@/app/components/layout/page-layout';
|
|
6
7
|
import { t } from '@/shared/lib/i18n';
|
|
7
8
|
import { RuntimeConfigOverview } from '@/features/system-status/components/config/runtime-config-overview';
|
|
@@ -111,12 +112,12 @@ export function RuntimeConfigEditor({
|
|
|
111
112
|
onRemoveBinding={(index) => setBindings((previous) => previous.filter((_, cursor) => cursor !== index))}
|
|
112
113
|
onAddBinding={() => setBindings((previous) => [...previous, createEmptyRuntimeBinding()])}
|
|
113
114
|
/>
|
|
114
|
-
<
|
|
115
|
-
<Button type="button" onClick={handleSave} disabled={updateRuntime.isPending}>
|
|
116
|
-
<Save className="h-
|
|
115
|
+
<FormActions>
|
|
116
|
+
<Button type="button" size="sm" onClick={handleSave} disabled={updateRuntime.isPending}>
|
|
117
|
+
<Save className="mr-1.5 h-3.5 w-3.5" />
|
|
117
118
|
{updateRuntime.isPending ? t('saving') : t('saveRuntimeSettings')}
|
|
118
119
|
</Button>
|
|
119
|
-
</
|
|
120
|
+
</FormActions>
|
|
120
121
|
</PageLayout>
|
|
121
122
|
);
|
|
122
123
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { UpdateSnapshot } from '@nextclaw/shared';
|
|
2
|
+
import { useQuery } from '@tanstack/react-query';
|
|
2
3
|
import { runtimeUpdateManager, useRuntimeUpdateStore } from '@/features/system-status';
|
|
3
4
|
import { Button } from '@/shared/components/ui/button';
|
|
4
5
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/shared/components/ui/card';
|
|
@@ -6,9 +7,16 @@ import { Label } from '@/shared/components/ui/label';
|
|
|
6
7
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/components/ui/select';
|
|
7
8
|
import { Switch } from '@/shared/components/ui/switch';
|
|
8
9
|
import { PageHeader, PageLayout } from '@/app/components/layout/page-layout';
|
|
9
|
-
import { formatDateTime, t } from '@/shared/lib/i18n';
|
|
10
|
+
import { formatDateTime, getLanguage, t } from '@/shared/lib/i18n';
|
|
10
11
|
import { cn } from '@/shared/lib/utils';
|
|
11
12
|
import { hostCapabilityManager } from '@/shared/lib/host-capabilities';
|
|
13
|
+
import {
|
|
14
|
+
fetchReleaseNotesData,
|
|
15
|
+
readReleaseNotesText,
|
|
16
|
+
resolveReleaseNotesDataUrl,
|
|
17
|
+
type ReleaseNotesLocale,
|
|
18
|
+
type ReleaseNotesSection
|
|
19
|
+
} from '@/features/system-status/utils/update-release-notes.utils';
|
|
12
20
|
import { Download, ExternalLink, RefreshCw, RotateCw } from 'lucide-react';
|
|
13
21
|
|
|
14
22
|
const STATUS_LABEL_KEYS: Record<string, string> = {
|
|
@@ -21,6 +29,13 @@ const STATUS_LABEL_KEYS: Record<string, string> = {
|
|
|
21
29
|
failed: 'desktopUpdatesStatusFailed',
|
|
22
30
|
};
|
|
23
31
|
|
|
32
|
+
const RELEASE_NOTES_KIND_LABEL_KEYS: Record<string, string> = {
|
|
33
|
+
feature: 'desktopUpdatesReleaseNotesFeature',
|
|
34
|
+
enhancement: 'desktopUpdatesReleaseNotesEnhancement',
|
|
35
|
+
fix: 'desktopUpdatesReleaseNotesFix',
|
|
36
|
+
compatibility: 'desktopUpdatesReleaseNotesCompatibility'
|
|
37
|
+
};
|
|
38
|
+
|
|
24
39
|
function StatusBadge({ status }: { status: string }) {
|
|
25
40
|
return <span className={cn('inline-flex rounded-full px-3 py-1 text-xs font-medium ring-1', getStatusTone(status))}>{getStatusLabel(status)}</span>;
|
|
26
41
|
}
|
|
@@ -117,6 +132,80 @@ function getStatusTone(status: string): string {
|
|
|
117
132
|
return 'bg-gray-100 text-gray-700 ring-gray-200';
|
|
118
133
|
}
|
|
119
134
|
|
|
135
|
+
function getReleaseNotesLocale(): ReleaseNotesLocale {
|
|
136
|
+
return getLanguage() === 'zh' ? 'zh-CN' : 'en-US';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function getReleaseNotesSectionTitle(section: ReleaseNotesSection, locale: ReleaseNotesLocale): string {
|
|
140
|
+
const title = readReleaseNotesText(section.title, locale);
|
|
141
|
+
return title ?? t(RELEASE_NOTES_KIND_LABEL_KEYS[section.kind] ?? 'desktopUpdatesReleaseNotesEnhancement');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function ReleaseNotesPreview({ snapshot }: { snapshot: UpdateSnapshot }) {
|
|
145
|
+
const dataUrl = resolveReleaseNotesDataUrl(snapshot);
|
|
146
|
+
const locale = getReleaseNotesLocale();
|
|
147
|
+
const releaseNotesQuery = useQuery({
|
|
148
|
+
queryKey: ['runtime-release-notes', dataUrl],
|
|
149
|
+
queryFn: async () => await fetchReleaseNotesData(dataUrl ?? ''),
|
|
150
|
+
enabled: Boolean(dataUrl),
|
|
151
|
+
retry: false,
|
|
152
|
+
staleTime: 5 * 60 * 1000
|
|
153
|
+
});
|
|
154
|
+
const payload = releaseNotesQuery.data;
|
|
155
|
+
const title = readReleaseNotesText(payload?.title, locale) ?? t('desktopUpdatesReleaseNotesPreviewTitle');
|
|
156
|
+
const summary = readReleaseNotesText(payload?.summary, locale) ?? t('desktopUpdatesReleaseNotesPreviewDescription');
|
|
157
|
+
|
|
158
|
+
if (!snapshot.releaseNotesUrl) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<Card>
|
|
164
|
+
<CardHeader>
|
|
165
|
+
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
166
|
+
<div className="space-y-1">
|
|
167
|
+
<CardTitle>{title}</CardTitle>
|
|
168
|
+
<CardDescription>{summary}</CardDescription>
|
|
169
|
+
</div>
|
|
170
|
+
<Button variant="ghost" onClick={() => void hostCapabilityManager.openExternalUrl(snapshot.releaseNotesUrl ?? '')}>
|
|
171
|
+
<ExternalLink className="mr-2 h-4 w-4" />
|
|
172
|
+
{t('desktopUpdatesReleaseNotes')}
|
|
173
|
+
</Button>
|
|
174
|
+
</div>
|
|
175
|
+
</CardHeader>
|
|
176
|
+
<CardContent className="space-y-4">
|
|
177
|
+
{releaseNotesQuery.isLoading ? (
|
|
178
|
+
<p className="text-sm text-gray-500">{t('desktopUpdatesReleaseNotesPreviewLoading')}</p>
|
|
179
|
+
) : null}
|
|
180
|
+
{!dataUrl || releaseNotesQuery.isError ? (
|
|
181
|
+
<p className="text-sm text-gray-500">{t('desktopUpdatesReleaseNotesPreviewUnavailable')}</p>
|
|
182
|
+
) : null}
|
|
183
|
+
{payload ? (
|
|
184
|
+
<div className="grid gap-4 md:grid-cols-2">
|
|
185
|
+
{payload.sections.map((section) => (
|
|
186
|
+
<div key={section.kind} className="rounded-xl border border-gray-200 bg-gray-50/60 p-4">
|
|
187
|
+
<p className="text-sm font-semibold text-gray-900">{getReleaseNotesSectionTitle(section, locale)}</p>
|
|
188
|
+
<ul className="mt-3 space-y-3">
|
|
189
|
+
{section.items.map((item, index) => {
|
|
190
|
+
const itemTitle = readReleaseNotesText(item.title, locale);
|
|
191
|
+
const itemBody = readReleaseNotesText(item.body, locale);
|
|
192
|
+
return (
|
|
193
|
+
<li key={`${itemTitle ?? section.kind}-${index}`} className="space-y-1">
|
|
194
|
+
{itemTitle ? <p className="text-sm font-medium text-gray-800">{itemTitle}</p> : null}
|
|
195
|
+
{itemBody ? <p className="text-sm leading-5 text-gray-600">{itemBody}</p> : null}
|
|
196
|
+
</li>
|
|
197
|
+
);
|
|
198
|
+
})}
|
|
199
|
+
</ul>
|
|
200
|
+
</div>
|
|
201
|
+
))}
|
|
202
|
+
</div>
|
|
203
|
+
) : null}
|
|
204
|
+
</CardContent>
|
|
205
|
+
</Card>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
120
209
|
function RuntimeUpdateUnavailableState() {
|
|
121
210
|
return (
|
|
122
211
|
<PageLayout className="space-y-6">
|
|
@@ -192,6 +281,7 @@ export function DesktopUpdateConfig() {
|
|
|
192
281
|
{snapshot.errorMessage && snapshot.status !== 'blocked' ? <div className="rounded-2xl border border-red-200 bg-red-50/70 p-4 text-sm text-red-700">{snapshot.errorMessage}</div> : null}
|
|
193
282
|
</CardContent>
|
|
194
283
|
</Card>
|
|
284
|
+
<ReleaseNotesPreview snapshot={snapshot} />
|
|
195
285
|
<Card>
|
|
196
286
|
<CardHeader>
|
|
197
287
|
<CardTitle>{t('desktopUpdatesPreferencesTitle')}</CardTitle>
|
|
@@ -250,7 +340,6 @@ export function DesktopUpdateConfig() {
|
|
|
250
340
|
<RotateCw className={cn('mr-2 h-4 w-4', isApplying && 'animate-spin')} />
|
|
251
341
|
{t('runtimeUpdatesApplyNow')}
|
|
252
342
|
</Button>
|
|
253
|
-
{snapshot.releaseNotesUrl ? <Button variant="ghost" onClick={() => void hostCapabilityManager.openExternalUrl(snapshot.releaseNotesUrl ?? '')}><ExternalLink className="mr-2 h-4 w-4" />{t('desktopUpdatesReleaseNotes')}</Button> : null}
|
|
254
343
|
</CardContent>
|
|
255
344
|
</Card>
|
|
256
345
|
</PageLayout>
|