diff --git a/packages/webui/src/components/debug/file-source.tsx b/packages/webui/src/components/debug/file-source.tsx index 4cbc3134..de91d0e8 100644 --- a/packages/webui/src/components/debug/file-source.tsx +++ b/packages/webui/src/components/debug/file-source.tsx @@ -4,7 +4,7 @@ // server, or a browser upload that we stream to a server temp path (and then // feed that path to the action). Upload progress flows through the app-level // task registry so it survives tab switches. -import { useRef, useState } from 'react'; +import { useId, useState } from 'react'; import { FileUp, Link2, Loader2, ServerCog, X } from 'lucide-react'; import { Input } from '@/components/ui/input'; import { useApi } from '@/lib/api'; @@ -15,9 +15,9 @@ import type { FieldRole } from '@/types'; type Mode = 'url' | 'server' | 'upload'; const ACCEPT: Partial> = { - image: 'image/*', - record: 'audio/*', - video: 'video/*', + image: 'image/*,.png,.jpg,.jpeg,.gif,.webp,.bmp,.heic,.heif', + record: 'audio/*,.silk,.amr,.mp3,.wav,.m4a,.ogg', + video: 'video/*,.mp4,.mov,.mkv,.webm', }; function Seg({ active, onClick, icon, label }: { active: boolean; onClick: () => void; icon: React.ReactNode; label: string }) { @@ -45,7 +45,7 @@ export function FileSource({ value, onChange, role, placeholder }: { const [uploading, setUploading] = useState(false); const [uploadName, setUploadName] = useState(null); const [uploadErr, setUploadErr] = useState(null); - const inputRef = useRef(null); + const inputId = useId(); const accept = role ? ACCEPT[role] : undefined; const doUpload = async (file: File) => { @@ -93,20 +93,25 @@ export function FileSource({ value, onChange, role, placeholder }: { {mode === 'upload' && (
{ const f = e.target.files?.[0]; if (f) void doUpload(f); e.target.value = ''; }} /> - + {uploading ? <> 上传中… : <> 选择或拖入文件上传到服务器} + {value && uploadName && !uploading && (
已上传:{uploadName} diff --git a/packages/webui/src/components/debug/message-builder.tsx b/packages/webui/src/components/debug/message-builder.tsx index 43cd37f5..48f7b37a 100644 --- a/packages/webui/src/components/debug/message-builder.tsx +++ b/packages/webui/src/components/debug/message-builder.tsx @@ -205,7 +205,12 @@ export function MessageBuilder({ segments, onChange, uin, groupId, depth = 0, mo // stopPropagation keeps a nested (forward-node) builder's drag // from reaching the parent row; onDragEnd lives here because the // grip — not the row — is now the drag source. - onDragStart={(e) => { e.stopPropagation(); setDragFrom(i); }} + onDragStart={(e) => { + e.stopPropagation(); + e.dataTransfer.setData('text/plain', String(i)); + e.dataTransfer.effectAllowed = 'move'; + setDragFrom(i); + }} onDragEnd={() => setDragFrom(null)} title="拖动此处排序" aria-label="拖动排序" className="flex shrink-0 cursor-grab rounded-md p-0.5 text-muted-foreground/50 transition-colors hover:bg-muted hover:text-foreground active:cursor-grabbing" diff --git a/packages/webui/src/components/layout/sidebar.tsx b/packages/webui/src/components/layout/sidebar.tsx index ade76fff..8b520356 100644 --- a/packages/webui/src/components/layout/sidebar.tsx +++ b/packages/webui/src/components/layout/sidebar.tsx @@ -1,5 +1,5 @@ import { Bug, Check, Eye, EyeOff, GripVertical, LayoutDashboard, Lock, Pin, PinOff, PlugZap, Settings, Sparkles, SlidersHorizontal, Terminal } from 'lucide-react'; -import { motion, Reorder } from 'motion/react'; +import { motion, Reorder, useDragControls } from 'motion/react'; import { Link, useRouterState } from '@tanstack/react-router'; import { ScrollArea } from '@/components/ui/scroll-area'; import { cn } from '@/lib/utils'; @@ -9,6 +9,7 @@ import { useTheme } from '@/contexts/ThemeContext'; import { reconcileLayoutItems, useLayout } from '@/contexts/LayoutContext'; import { useMediaQuery } from '@/hooks/use-media-query'; import type { AppPath } from '@/router'; +import type { UiLayoutItem } from '@/types'; export interface NavItem { to: AppPath; @@ -32,6 +33,58 @@ export const NAV_ITEMS: NavItem[] = [ // '/settings'— account + appearance. export const PINNED_NAV: AppPath[] = ['/', '/settings']; +function NavReorderRow({ + item, + meta, + onToggle, +}: { + item: UiLayoutItem; + meta: NavItem; + onToggle: (id: string) => void; +}) { + const controls = useDragControls(); + const Icon = meta.icon; + const itemPinned = (PINNED_NAV as string[]).includes(item.id); + return ( + + + + {meta.label} + {itemPinned ? ( + + + + ) : ( + + )} + + ); +} + interface SidebarProps { collapsed?: boolean; onItemClick?: () => void; @@ -98,36 +151,13 @@ export function Sidebar({ collapsed = false, onItemClick }: SidebarProps) { {reconciled.map((item) => { const meta = NAV_ITEMS.find((n) => n.to === item.id); if (!meta) return null; - const Icon = meta.icon; - const itemPinned = (PINNED_NAV as string[]).includes(item.id); return ( - - - - {meta.label} - {itemPinned ? ( - - - - ) : ( - - )} - + item={item} + meta={meta} + onToggle={toggleNav} + /> ); })} diff --git a/packages/webui/src/components/pages/dashboard-grid.tsx b/packages/webui/src/components/pages/dashboard-grid.tsx index 96e3e479..eb857127 100644 --- a/packages/webui/src/components/pages/dashboard-grid.tsx +++ b/packages/webui/src/components/pages/dashboard-grid.tsx @@ -78,6 +78,7 @@ export function DashboardGrid({ float: true, // free placement — items stay where dropped (no auto-compact) disableDrag: !editing, disableResize: !editing, + handle: '.grid-drag-handle', // Drag a card onto the gallery to remove it (edit mode only). The whole // card is the drag target; the corner icons are just hints. removable: editing && removableSelector ? removableSelector : false, @@ -150,30 +151,28 @@ export function DashboardGrid({
{editing && ( <> - {/* Subtle drag hint — a small square, not a bar. The whole card - is draggable; this just signals it (pointer-events-none so - the drag passes through to gridstack). */} - - + {configurableIds.has(b.id) && ( )} )} -
+
{renderWidget(b)}
, diff --git a/packages/webui/src/components/pages/overview-page.tsx b/packages/webui/src/components/pages/overview-page.tsx index 99453579..c0639b3b 100644 --- a/packages/webui/src/components/pages/overview-page.tsx +++ b/packages/webui/src/components/pages/overview-page.tsx @@ -1,5 +1,5 @@ import { useEffect, useId, useMemo, useState, type ReactNode } from 'react'; -import { motion, Reorder } from 'motion/react'; +import { motion, Reorder, useDragControls } from 'motion/react'; import { Link } from '@tanstack/react-router'; import { Activity, ArrowRight, Bell, Cable, Check, Cpu, ExternalLink, Eye, EyeOff, GripVertical, @@ -147,7 +147,7 @@ export function OverviewPage() { {editing && (

{isWide - ? '拖动卡片移动位置、拖右下角缩放,右上齿轮改设置;把卡片拖回右侧「部件库」即可移除,从部件库拖到画布即可添加。左侧导航也可在编辑态拖动排序。' + ? '拖动卡片移动位置、拖右下角缩放,右上齿轮改设置;把卡片拖回右侧「部件库」即可移除,从部件库点加号或拖到画布即可添加。左侧导航也可在编辑态拖动手柄排序。' : '拖动手柄调整卡片顺序,眼睛图标显隐,齿轮改设置。手机为单列布局,与桌面网格各自独立。'}

)} @@ -198,7 +198,7 @@ export function OverviewPage() { /> )}
- {editing &&
) : ( void }) { ); } +function MobileLayoutRow({ + item, + onToggle, + onConfigOpen, +}: { + item: UiLayoutItem; + onToggle: (id: string) => void; + onConfigOpen: (id: string) => void; +}) { + const controls = useDragControls(); + return ( + + + {widgetLabel(item.id)} + {CONFIGURABLE_WIDGETS.has(item.id) && item.visible && ( + + )} + + + ); +} + // ─────────────── mobile single-column overview ─────────────── // Phones get a one-dimensional layout (`overviewMobile`), fully disjoint from // the desktop 2D grid. View mode reuses the grid renderer (drag disabled) so @@ -262,37 +317,12 @@ function MobileOverview({ return ( i.id)} onReorder={onReorder} className="flex flex-col gap-2"> {items.map((item) => ( - - - {widgetLabel(item.id)} - {CONFIGURABLE_WIDGETS.has(item.id) && item.visible && ( - - )} - - + item={item} + onToggle={onToggle} + onConfigOpen={onConfigOpen} + /> ))} ); @@ -332,7 +362,7 @@ function MobileOverview({ // tile onto the canvas adds it (HTML5 DnD); dragging a canvas card back onto // this panel removes it (gridstack `removable`, matched by the .widget-gallery // class). Edit-mode only, desktop only. -function WidgetGallery({ hidden }: { hidden: UiLayoutItem[] }) { +function WidgetGallery({ hidden, onAdd }: { hidden: UiLayoutItem[]; onAdd: (id: string, x: number, y: number) => void }) { return (
@@ -340,7 +370,7 @@ function WidgetGallery({ hidden }: { hidden: UiLayoutItem[] }) { 部件库

- 拖到左侧画布添加;把卡片拖回此处即可移除。 + 点加号或拖到左侧画布添加;把卡片拖回此处即可移除。

{hidden.length === 0 ? (
@@ -360,7 +390,15 @@ function WidgetGallery({ hidden }: { hidden: UiLayoutItem[] }) { > {widgetLabel(b.id)} - +
))}
@@ -383,7 +421,7 @@ function DropPlaceholder({ onAdd }: { onAdd: (id: string, x: number, y: number) className="flex min-h-60 flex-col items-center justify-center gap-3 rounded-xl border-2 border-dashed py-16 text-center text-muted-foreground" > -

从右侧「部件库」拖动部件到这里

+

从右侧「部件库」点加号或拖动部件到这里

); }