diff --git a/locales/en-US/setting.json b/locales/en-US/setting.json index 395cb90ee2..6108f08ce8 100644 --- a/locales/en-US/setting.json +++ b/locales/en-US/setting.json @@ -280,7 +280,33 @@ "defaultAgent.title": "New Agent", "devices.actions.edit": "Edit", "devices.actions.remove": "Remove", + "devices.capabilities.commands.desc": "Safely execute terminal commands in your environment.", + "devices.capabilities.commands.title": "Run commands", + "devices.capabilities.files.desc": "Let agents directly access and organize the files on your computer.", + "devices.capabilities.files.title": "Read & write local files", + "devices.capabilities.title": "What you can do once connected", + "devices.capabilities.tools.desc": "Connect local tools to extend what agents can do.", + "devices.capabilities.tools.title": "Call system tools", "devices.channel.connected": "Connected {{time}}", + "devices.connectWizard.button": "Connect Device", + "devices.connectWizard.cli.connectDesc": "Start the background daemon to keep the device online and listening for remote operations.", + "devices.connectWizard.cli.connectTitle": "Start the daemon", + "devices.connectWizard.cli.installDesc": "Install the LobeHub CLI globally with your preferred package manager to enable device connectivity and management.", + "devices.connectWizard.cli.installTitle": "Install the CLI", + "devices.connectWizard.cli.loginDesc": "Complete OAuth authorization in your browser to link the CLI with your account.", + "devices.connectWizard.cli.loginTitle": "Sign in", + "devices.connectWizard.desktop.downloadLink": "Download LobeHub Desktop", + "devices.connectWizard.desktop.step1": "Download the desktop app", + "devices.connectWizard.desktop.step1Desc": "Visit the LobeHub downloads page and get the app for your operating system.", + "devices.connectWizard.desktop.step2": "Sign in and open the device gateway", + "devices.connectWizard.desktop.step2Desc": "After signing in, click the device gateway icon in the top-right corner and confirm it's turned on.", + "devices.connectWizard.desktop.step3": "Your device appears automatically", + "devices.connectWizard.desktop.step3Desc": "The desktop app registers itself as a device on launch — you'll see it in the list once connected.", + "devices.connectWizard.footer": "Only device metadata is registered — your data is never accessed.", + "devices.connectWizard.method.cli": "Via CLI", + "devices.connectWizard.method.desktop": "Via Desktop", + "devices.connectWizard.subtitle": "Choose how to connect your computer to LobeHub.", + "devices.connectWizard.title": "Connect Device", "devices.currentBadge": "This device", "devices.detail.addDir": "Add directory", "devices.detail.connections": "Connections", @@ -294,7 +320,13 @@ "devices.edit.friendlyNamePlaceholder": "A name to recognize this device", "devices.edit.save": "Save", "devices.edit.title": "Edit device", - "devices.empty": "No devices yet. Connect one with `lh connect` or by signing in to the desktop app.", + "devices.empty.desc": "Once connected, LobeHub agents can read/write files, run commands, and call system tools directly on your computer.", + "devices.empty.methodCli.desc": "Install the CLI in your terminal — great for servers or headless machines.", + "devices.empty.methodCli.title": "Connect via CLI", + "devices.empty.methodDesktop.badge": "Recommended", + "devices.empty.methodDesktop.desc": "Download the desktop app, sign in, and your device connects automatically.", + "devices.empty.methodDesktop.title": "Connect via Desktop", + "devices.empty.title": "Connect your first device", "devices.fallbackBadge": "Unstable identity", "devices.fallbackTooltip": "This device couldn't be identified by its machine ID, so reinstalling the app may create a duplicate entry.", "devices.lastSeen": "Last active {{time}}", diff --git a/locales/zh-CN/setting.json b/locales/zh-CN/setting.json index 1356749714..bc981f0f7c 100644 --- a/locales/zh-CN/setting.json +++ b/locales/zh-CN/setting.json @@ -280,7 +280,33 @@ "defaultAgent.title": "新建助理", "devices.actions.edit": "编辑", "devices.actions.remove": "移除", + "devices.capabilities.commands.desc": "在你的环境中安全地执行终端命令。", + "devices.capabilities.commands.title": "运行命令", + "devices.capabilities.files.desc": "让智能体直接访问并整理你电脑上的文件。", + "devices.capabilities.files.title": "读写本地文件", + "devices.capabilities.title": "连接后你能做什么", + "devices.capabilities.tools.desc": "连接本地工具,扩展智能体的能力边界。", + "devices.capabilities.tools.title": "调用系统工具", "devices.channel.connected": "已连接 {{time}}", + "devices.connectWizard.button": "连接设备", + "devices.connectWizard.cli.connectDesc": "启动后台守护进程,保持设备在线并等待远程操作。", + "devices.connectWizard.cli.connectTitle": "启动守护进程", + "devices.connectWizard.cli.installDesc": "使用你喜欢的包管理器全局安装 LobeHub CLI,提供设备连接与管理能力。", + "devices.connectWizard.cli.installTitle": "安装 CLI", + "devices.connectWizard.cli.loginDesc": "在浏览器中完成 OAuth 授权,将 CLI 与你的账号关联。", + "devices.connectWizard.cli.loginTitle": "登录账号", + "devices.connectWizard.desktop.downloadLink": "下载 LobeHub 桌面端", + "devices.connectWizard.desktop.step1": "下载桌面端", + "devices.connectWizard.desktop.step1Desc": "前往 LobeHub 下载页获取适用于你操作系统的桌面版应用。", + "devices.connectWizard.desktop.step2": "登录并打开设备网关", + "devices.connectWizard.desktop.step2Desc": "登录后,点击右上角的设备网关图标,确认开关已打开。", + "devices.connectWizard.desktop.step3": "设备自动出现", + "devices.connectWizard.desktop.step3Desc": "桌面端启动后会自动注册为设备,连接成功后即可在列表中看到它。", + "devices.connectWizard.footer": "仅注册设备信息,不会访问你的数据。", + "devices.connectWizard.method.cli": "通过 CLI", + "devices.connectWizard.method.desktop": "通过桌面端", + "devices.connectWizard.subtitle": "选择一种方式将你的电脑连接到 LobeHub。", + "devices.connectWizard.title": "连接设备", "devices.currentBadge": "当前设备", "devices.detail.addDir": "添加目录", "devices.detail.connections": "连接通道", @@ -294,7 +320,13 @@ "devices.edit.friendlyNamePlaceholder": "便于识别此设备的名称", "devices.edit.save": "保存", "devices.edit.title": "编辑设备", - "devices.empty": "还没有设备。用 `lh connect` 连接,或登录桌面端应用。", + "devices.empty.desc": "连接后,LobeHub 上的智能体可以直接在你的电脑上读写文件、运行命令和调用系统工具。", + "devices.empty.methodCli.desc": "通过终端安装 CLI,适用于服务器或无桌面环境。", + "devices.empty.methodCli.title": "使用 CLI 连接", + "devices.empty.methodDesktop.badge": "推荐", + "devices.empty.methodDesktop.desc": "下载桌面端,登录后设备自动连接。", + "devices.empty.methodDesktop.title": "使用桌面端连接", + "devices.empty.title": "连接你的第一台设备", "devices.fallbackBadge": "身份不稳定", "devices.fallbackTooltip": "未能通过机器 ID 识别此设备,重装应用可能会产生重复条目。", "devices.lastSeen": "最近活跃 {{time}}", diff --git a/packages/locales/src/default/setting.ts b/packages/locales/src/default/setting.ts index 98e2c337fd..13bcb8f70a 100644 --- a/packages/locales/src/default/setting.ts +++ b/packages/locales/src/default/setting.ts @@ -353,8 +353,50 @@ export default { 'devices.edit.friendlyNamePlaceholder': 'A name to recognize this device', 'devices.edit.save': 'Save', 'devices.edit.title': 'Edit device', - 'devices.empty': - 'No devices yet. Connect one with `lh connect` or by signing in to the desktop app.', + 'devices.capabilities.commands.desc': 'Safely execute terminal commands in your environment.', + 'devices.capabilities.commands.title': 'Run commands', + 'devices.capabilities.files.desc': + 'Let agents directly access and organize the files on your computer.', + 'devices.capabilities.files.title': 'Read & write local files', + 'devices.capabilities.title': 'What you can do once connected', + 'devices.capabilities.tools.desc': 'Connect local tools to extend what agents can do.', + 'devices.capabilities.tools.title': 'Call system tools', + 'devices.connectWizard.button': 'Connect Device', + 'devices.connectWizard.cli.connectDesc': + 'Start the background daemon to keep the device online and listening for remote operations.', + 'devices.connectWizard.cli.connectTitle': 'Start the daemon', + 'devices.connectWizard.cli.installDesc': + 'Install the LobeHub CLI globally with your preferred package manager to enable device connectivity and management.', + 'devices.connectWizard.cli.installTitle': 'Install the CLI', + 'devices.connectWizard.cli.loginDesc': + 'Complete OAuth authorization in your browser to link the CLI with your account.', + 'devices.connectWizard.cli.loginTitle': 'Sign in', + 'devices.connectWizard.desktop.downloadLink': 'Download LobeHub Desktop', + 'devices.connectWizard.desktop.step1': 'Download the desktop app', + 'devices.connectWizard.desktop.step1Desc': + 'Visit the LobeHub downloads page and get the app for your operating system.', + 'devices.connectWizard.desktop.step2': 'Sign in and open the device gateway', + 'devices.connectWizard.desktop.step2Desc': + "After signing in, click the device gateway icon in the top-right corner and confirm it's turned on.", + 'devices.connectWizard.desktop.step3': 'Your device appears automatically', + 'devices.connectWizard.desktop.step3Desc': + "The desktop app registers itself as a device on launch — you'll see it in the list once connected.", + 'devices.connectWizard.footer': + 'Only device metadata is registered — your data is never accessed.', + 'devices.connectWizard.method.cli': 'Via CLI', + 'devices.connectWizard.method.desktop': 'Via Desktop', + 'devices.connectWizard.subtitle': 'Choose how to connect your computer to LobeHub.', + 'devices.connectWizard.title': 'Connect Device', + 'devices.empty.desc': + 'Once connected, LobeHub agents can read/write files, run commands, and call system tools directly on your computer.', + 'devices.empty.methodCli.desc': + 'Install the CLI in your terminal — great for servers or headless machines.', + 'devices.empty.methodCli.title': 'Connect via CLI', + 'devices.empty.methodDesktop.badge': 'Recommended', + 'devices.empty.methodDesktop.desc': + 'Download the desktop app, sign in, and your device connects automatically.', + 'devices.empty.methodDesktop.title': 'Connect via Desktop', + 'devices.empty.title': 'Connect your first device', 'devices.fallbackBadge': 'Unstable identity', 'devices.fallbackTooltip': "This device couldn't be identified by its machine ID, so reinstalling the app may create a duplicate entry.", diff --git a/src/routes/(main)/settings/devices/features/ConnectDeviceModal.tsx b/src/routes/(main)/settings/devices/features/ConnectDeviceModal.tsx new file mode 100644 index 0000000000..67b0ecbace --- /dev/null +++ b/src/routes/(main)/settings/devices/features/ConnectDeviceModal.tsx @@ -0,0 +1,215 @@ +'use client'; + +import { DOWNLOAD_URL } from '@lobechat/const'; +import { Button, CopyButton, Flexbox, Icon, Modal, Segmented, Text } from '@lobehub/ui'; +import { createStaticStyles, cssVar } from 'antd-style'; +import { DownloadIcon, MonitorDownIcon, ShieldCheckIcon, TerminalIcon } from 'lucide-react'; +import { memo, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +const styles = createStaticStyles(({ css }) => ({ + codeBlock: css` + display: flex; + gap: 12px; + align-items: center; + + padding-block: 10px; + padding-inline: 14px; + border: 1px solid ${cssVar.colorBorderSecondary}; + border-radius: ${cssVar.borderRadius}; + + background: ${cssVar.colorFillQuaternary}; + `, + command: css` + overflow: hidden; + flex: 1; + + font-family: ${cssVar.fontFamilyCode}; + font-size: 13px; + color: ${cssVar.colorText}; + text-overflow: ellipsis; + white-space: nowrap; + `, + footer: css` + margin-block-start: 4px; + padding-block-start: 16px; + border-block-start: 1px solid ${cssVar.colorBorderSecondary}; + + font-size: 12px; + color: ${cssVar.colorTextTertiary}; + `, + index: css` + display: flex; + flex-shrink: 0; + align-items: center; + justify-content: center; + + width: 24px; + height: 24px; + border-radius: 50%; + + font-size: 12px; + font-weight: 600; + color: ${cssVar.colorPrimary}; + + background: ${cssVar.colorPrimaryBg}; + `, + line: css` + flex: 1; + width: 1px; + margin-block-start: 4px; + background: ${cssVar.colorBorderSecondary}; + `, + stepDesc: css` + font-size: 13px; + line-height: 1.6; + color: ${cssVar.colorTextTertiary}; + `, + subtitle: css` + font-size: 13px; + color: ${cssVar.colorTextTertiary}; + `, +})); + +interface ConnectDeviceModalProps { + initialTab?: 'cli' | 'desktop'; + onClose: () => void; + open: boolean; +} + +interface StepProps { + children?: React.ReactNode; + desc: string; + index: number; + last?: boolean; + title: string; +} + +const Step = memo(({ index, title, desc, children, last }) => ( + + + {index} + {!last && } + + + {title} + {desc} + {children &&
{children}
} +
+
+)); + +const CommandLine = memo<{ command: string }>(({ command }) => ( +
+ {command} + +
+)); + +const cliCommands = { + connect: 'lh connect --daemon', + install: 'npm install -g @lobehub/cli', + login: 'lh login', +}; + +const ConnectDeviceModal = memo(({ onClose, open, initialTab }) => { + const { t } = useTranslation('setting'); + const [active, setActive] = useState<'cli' | 'desktop'>(initialTab ?? 'desktop'); + + useEffect(() => { + if (open) setActive(initialTab ?? 'desktop'); + }, [open, initialTab]); + + return ( + + + {t('devices.connectWizard.subtitle')} + + , + label: t('devices.connectWizard.method.desktop'), + value: 'desktop', + }, + { + icon: , + label: t('devices.connectWizard.method.cli'), + value: 'cli', + }, + ]} + onChange={(value) => setActive(value as 'cli' | 'desktop')} + /> + + {active === 'desktop' ? ( + + + + + + + + + + ) : ( + + + + + + + + + + + + )} + + + + {t('devices.connectWizard.footer')} + + + + ); +}); + +ConnectDeviceModal.displayName = 'ConnectDeviceModal'; + +export default ConnectDeviceModal; diff --git a/src/routes/(main)/settings/devices/features/DeviceList.tsx b/src/routes/(main)/settings/devices/features/DeviceList.tsx index 8997303d06..56c9b2d7b3 100644 --- a/src/routes/(main)/settings/devices/features/DeviceList.tsx +++ b/src/routes/(main)/settings/devices/features/DeviceList.tsx @@ -1,36 +1,166 @@ 'use client'; import { isDesktop } from '@lobechat/const'; -import { Flexbox, Skeleton, Text } from '@lobehub/ui'; +import { Flexbox, Icon, Skeleton, Text } from '@lobehub/ui'; import { createStaticStyles, cssVar } from 'antd-style'; +import { + ChevronRightIcon, + FolderCogIcon, + type LucideIcon, + MonitorDownIcon, + ShieldCheckIcon, + TerminalIcon, + ZapIcon, +} from 'lucide-react'; import { memo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { lambdaQuery } from '@/libs/trpc/client'; import { useElectronStore } from '@/store/electron'; +import ConnectDeviceModal from './ConnectDeviceModal'; import DeviceDetailPanel from './DeviceDetailPanel'; import DeviceItem from './DeviceItem'; const styles = createStaticStyles(({ css }) => ({ + badge: css` + padding-block: 1px; + padding-inline: 8px; + border-radius: 999px; + + font-size: 11px; + font-weight: 500; + color: ${cssVar.colorPrimary}; + + background: ${cssVar.colorPrimaryBg}; + `, + capabilityCard: css` + padding: 16px; + border: 1px solid ${cssVar.colorBorderSecondary}; + border-radius: ${cssVar.borderRadiusLG}; + + background: ${cssVar.colorBgContainer}; + + transition: border-color 0.2s; + + &:hover { + border-color: ${cssVar.colorPrimaryBorder}; + } + `, + capabilityIcon: css` + display: flex; + align-items: center; + justify-content: center; + + width: 36px; + height: 36px; + border-radius: ${cssVar.borderRadius}; + + color: ${cssVar.colorText}; + + background: ${cssVar.colorFillSecondary}; + `, detailCol: css` align-self: stretch; min-width: 0; border: 1px solid ${cssVar.colorBorderSecondary}; border-radius: ${cssVar.borderRadiusLG}; `, - empty: css` - padding-block: 48px; - color: ${cssVar.colorTextTertiary}; + emptyCard: css` + overflow: hidden; + border: 1px solid ${cssVar.colorBorderSecondary}; + border-radius: ${cssVar.borderRadiusLG}; + background: ${cssVar.colorBgContainer}; + `, + emptyHero: css` + padding-block: 40px; + padding-inline: 32px; + border-block-end: 1px solid ${cssVar.colorBorderSecondary}; + text-align: center; + + background: ${cssVar.colorFillQuaternary}; + `, + heroIcon: css` + display: flex; + align-items: center; + justify-content: center; + + width: 56px; + height: 56px; + border-radius: ${cssVar.borderRadiusLG}; + + color: ${cssVar.colorPrimary}; + + background: ${cssVar.colorPrimaryBg}; `, listCol: css` min-width: 0; border: 1px solid ${cssVar.colorBorderSecondary}; border-radius: ${cssVar.borderRadiusLG}; `, + option: css` + cursor: pointer; + padding: 20px; + background: ${cssVar.colorBgContainer}; + transition: background 0.2s; + + &:hover { + background: ${cssVar.colorFillQuaternary}; + } + `, + optionGrid: css` + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1px; + background: ${cssVar.colorBorderSecondary}; + `, + optionIcon: css` + display: flex; + flex-shrink: 0; + align-items: center; + justify-content: center; + + width: 40px; + height: 40px; + border-radius: ${cssVar.borderRadius}; + + color: ${cssVar.colorTextSecondary}; + + background: ${cssVar.colorFillSecondary}; + `, + subtitle: css` + font-size: 13px; + color: ${cssVar.colorTextTertiary}; + `, })); +interface ConnectOptionProps { + badge?: string; + desc: string; + icon: LucideIcon; + onClick: () => void; + title: string; +} + +const ConnectOption = memo(({ icon, title, desc, badge, onClick }) => ( + + + + + + + {title} + {badge && {badge}} + + + {desc} + + + + +)); + const DeviceList = memo(() => { const { t } = useTranslation('setting'); const { data: devices, isLoading } = lambdaQuery.device.listDevices.useQuery(undefined, { @@ -48,14 +178,93 @@ const DeviceList = memo(() => { // No device is selected by default — the detail panel only appears once the // user clicks a row. const [selectedId, setSelectedId] = useState(); + const [connectTab, setConnectTab] = useState<'cli' | 'desktop'>(); + + const openConnect = (tab: 'cli' | 'desktop') => setConnectTab(tab); if (isLoading) return ; if (!devices || devices.length === 0) return ( - - {t('devices.empty')} - + <> + + {/* Onboarding card: hero + the two real connection methods */} + + + + + + {t('devices.empty.title')} + + {t('devices.empty.desc')} + + + +
+ openConnect('desktop')} + /> + openConnect('cli')} + /> +
+
+ + {/* Capabilities unlocked once a device is connected */} + + + + + {t('devices.capabilities.title')} + + + + {[ + { + desc: t('devices.capabilities.files.desc'), + icon: FolderCogIcon, + title: t('devices.capabilities.files.title'), + }, + { + desc: t('devices.capabilities.commands.desc'), + icon: TerminalIcon, + title: t('devices.capabilities.commands.title'), + }, + { + desc: t('devices.capabilities.tools.desc'), + icon: ZapIcon, + title: t('devices.capabilities.tools.title'), + }, + ].map((cap) => ( + + + + + + {cap.title} + + {cap.desc} + + + + ))} + + +
+ + setConnectTab(undefined)} + /> + ); const selected = selectedId ? devices.find((d) => d.deviceId === selectedId) : undefined; diff --git a/src/routes/(main)/settings/devices/index.tsx b/src/routes/(main)/settings/devices/index.tsx index f728bdd5fa..5e372fcdde 100644 --- a/src/routes/(main)/settings/devices/index.tsx +++ b/src/routes/(main)/settings/devices/index.tsx @@ -1,19 +1,37 @@ 'use client'; -import { memo } from 'react'; +import { Button, Icon } from '@lobehub/ui'; +import { MonitorUpIcon } from 'lucide-react'; +import { memo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import SettingHeader from '@/routes/(main)/settings/features/SettingHeader'; +import ConnectDeviceModal from './features/ConnectDeviceModal'; import DeviceList from './features/DeviceList'; const Page = memo(() => { const { t } = useTranslation('setting'); + const [connectModalOpen, setConnectModalOpen] = useState(false); return ( <> - + } + size={'small'} + onClick={() => setConnectModalOpen(true)} + > + {t('devices.connectWizard.button')} + + } + /> + + + setConnectModalOpen(false)} /> ); });