@devxcrew/react-ui 0.0.0-stage → 0.1.7
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/README.md +85 -2
- package/package.json +105 -4
- package/src/blocks/agent-chat-workspace/agent-chat-workspace.tsx +162 -0
- package/src/blocks/agent-chat-workspace/index.ts +2 -0
- package/src/blocks/agent-provider-settings/agent-provider-settings.tsx +183 -0
- package/src/blocks/agent-provider-settings/index.ts +2 -0
- package/src/blocks/agent-task-workspace/agent-task-workspace.tsx +48 -0
- package/src/blocks/agent-task-workspace/index.ts +2 -0
- package/src/blocks/analysis-context/analysis-context.tsx +6 -0
- package/src/blocks/analysis-context/index.ts +1 -0
- package/src/blocks/app-header/app-header.tsx +159 -0
- package/src/blocks/app-header/index.ts +2 -0
- package/src/blocks/archived-chat-workspace/archived-chat-workspace.tsx +29 -0
- package/src/blocks/archived-chat-workspace/index.ts +2 -0
- package/src/blocks/auth/README.md +32 -0
- package/src/blocks/auth/admin-login-page.tsx +15 -0
- package/src/blocks/auth/admin-password-forgot-page.tsx +5 -0
- package/src/blocks/auth/auth-page-layout.tsx +90 -0
- package/src/blocks/auth/client-login-page.tsx +20 -0
- package/src/blocks/auth/client-password-forgot-page.tsx +5 -0
- package/src/blocks/auth/identity-management-desk.tsx +530 -0
- package/src/blocks/auth/index.ts +12 -0
- package/src/blocks/auth/login-page.tsx +206 -0
- package/src/blocks/auth/password-forgot-page.tsx +60 -0
- package/src/blocks/auth/portal-pages.tsx +32 -0
- package/src/blocks/auth/privileged-desk.tsx +55 -0
- package/src/blocks/auth/register-page.tsx +117 -0
- package/src/blocks/auth/session-boundary.tsx +296 -0
- package/src/blocks/auth/super-admin-login-page.tsx +15 -0
- package/src/blocks/auth/super-admin-password-forgot-page.tsx +5 -0
- package/src/blocks/blog/blog-card.tsx +156 -0
- package/src/blocks/blog/blog-reader.tsx +175 -0
- package/src/blocks/blog/blog-types.ts +47 -0
- package/src/blocks/blog/index.ts +3 -0
- package/src/blocks/chat-composer/chat-composer.tsx +140 -0
- package/src/blocks/chat-composer/index.ts +1 -0
- package/src/blocks/chat-history/chat-history.tsx +59 -0
- package/src/blocks/chat-history/index.ts +1 -0
- package/src/blocks/chat-response-progress/chat-response-progress.tsx +34 -0
- package/src/blocks/chat-response-progress/index.ts +2 -0
- package/src/blocks/chat-runtime-controls/chat-runtime-controls.tsx +56 -0
- package/src/blocks/chat-runtime-controls/index.ts +2 -0
- package/src/blocks/chat-runtime-trace/chat-runtime-trace.tsx +71 -0
- package/src/blocks/chat-runtime-trace/index.ts +2 -0
- package/src/blocks/codex-connection-settings/codex-connection-settings.tsx +42 -0
- package/src/blocks/codex-connection-settings/index.ts +1 -0
- package/src/blocks/dropzone/dropzone-file-item.tsx +113 -0
- package/src/blocks/dropzone/dropzone-types.ts +32 -0
- package/src/blocks/dropzone/dropzone.tsx +216 -0
- package/src/blocks/dropzone/index.ts +3 -0
- package/src/blocks/ecommerce/cart/cart-drawer.tsx +228 -0
- package/src/blocks/ecommerce/cart/cart-types.ts +27 -0
- package/src/blocks/ecommerce/cart/index.ts +2 -0
- package/src/blocks/ecommerce/categories/categories-types.ts +23 -0
- package/src/blocks/ecommerce/categories/category-showcase.tsx +125 -0
- package/src/blocks/ecommerce/categories/index.ts +2 -0
- package/src/blocks/ecommerce/checkout/checkout-types.ts +48 -0
- package/src/blocks/ecommerce/checkout/checkout-wizard.tsx +468 -0
- package/src/blocks/ecommerce/checkout/index.ts +2 -0
- package/src/blocks/ecommerce/comparison/comparison-types.ts +28 -0
- package/src/blocks/ecommerce/comparison/index.ts +2 -0
- package/src/blocks/ecommerce/comparison/product-comparison.tsx +174 -0
- package/src/blocks/ecommerce/coupon-wallet/coupon-wallet-types.ts +21 -0
- package/src/blocks/ecommerce/coupon-wallet/coupon-wallet.tsx +154 -0
- package/src/blocks/ecommerce/coupon-wallet/index.ts +2 -0
- package/src/blocks/ecommerce/delivery-tracker/delivery-tracker-types.ts +20 -0
- package/src/blocks/ecommerce/delivery-tracker/delivery-tracker.tsx +169 -0
- package/src/blocks/ecommerce/delivery-tracker/index.ts +2 -0
- package/src/blocks/ecommerce/index.ts +12 -0
- package/src/blocks/ecommerce/payment-methods/index.ts +2 -0
- package/src/blocks/ecommerce/payment-methods/payment-method-selector.tsx +224 -0
- package/src/blocks/ecommerce/payment-methods/payment-methods-types.ts +21 -0
- package/src/blocks/ecommerce/price-history/index.ts +2 -0
- package/src/blocks/ecommerce/price-history/price-history-chart.tsx +217 -0
- package/src/blocks/ecommerce/price-history/price-history-types.ts +16 -0
- package/src/blocks/ecommerce/reviews/index.ts +2 -0
- package/src/blocks/ecommerce/reviews/reviews-section.tsx +309 -0
- package/src/blocks/ecommerce/reviews/reviews-types.ts +26 -0
- package/src/blocks/ecommerce/wishlist/index.ts +2 -0
- package/src/blocks/ecommerce/wishlist/wishlist-grid.tsx +149 -0
- package/src/blocks/ecommerce/wishlist/wishlist-types.ts +19 -0
- package/src/blocks/execution-status/README.md +25 -0
- package/src/blocks/execution-status/execution-status.tsx +135 -0
- package/src/blocks/execution-status/index.ts +2 -0
- package/src/blocks/file-tree/file-icons.tsx +84 -0
- package/src/blocks/file-tree/file-tree-node.tsx +158 -0
- package/src/blocks/file-tree/file-tree-types.ts +33 -0
- package/src/blocks/file-tree/file-tree.tsx +163 -0
- package/src/blocks/file-tree/index.ts +5 -0
- package/src/blocks/filter-builder/filter-builder.tsx +169 -0
- package/src/blocks/filter-builder/filter-row.tsx +164 -0
- package/src/blocks/filter-builder/filter-types.ts +61 -0
- package/src/blocks/filter-builder/index.ts +12 -0
- package/src/blocks/footer/footer-types.ts +35 -0
- package/src/blocks/footer/index.ts +2 -0
- package/src/blocks/footer/site-footer.tsx +212 -0
- package/src/blocks/form/form-block.tsx +113 -0
- package/src/blocks/form/form-lookup-field.tsx +60 -0
- package/src/blocks/form/index.ts +4 -0
- package/src/blocks/handover-stack/handover-stack.tsx +41 -0
- package/src/blocks/handover-stack/index.ts +1 -0
- package/src/blocks/idea-handover/idea-handover-readiness.ts +41 -0
- package/src/blocks/idea-handover/idea-handover-workspace.tsx +150 -0
- package/src/blocks/idea-handover/index.ts +4 -0
- package/src/blocks/kanban/index.ts +11 -0
- package/src/blocks/kanban/kanban-board.tsx +146 -0
- package/src/blocks/kanban/kanban-card.tsx +149 -0
- package/src/blocks/kanban/kanban-column.tsx +102 -0
- package/src/blocks/kanban/kanban-types.ts +38 -0
- package/src/blocks/loader/global-loader.tsx +90 -0
- package/src/blocks/loader/index.ts +1 -0
- package/src/blocks/mascot/README.md +21 -0
- package/src/blocks/mascot/index.ts +2 -0
- package/src/blocks/mascot/mascot.tsx +202 -0
- package/src/blocks/master-list/README.md +21 -0
- package/src/blocks/master-list/index.ts +12 -0
- package/src/blocks/master-list/master-form.tsx +157 -0
- package/src/blocks/master-list/master-list-desk.tsx +370 -0
- package/src/blocks/master-list/master-list.tsx +189 -0
- package/src/blocks/master-list/types.ts +18 -0
- package/src/blocks/mermaid-preview/index.ts +1 -0
- package/src/blocks/mermaid-preview/mermaid-preview.tsx +164 -0
- package/src/blocks/notifications/README.md +7 -0
- package/src/blocks/notifications/index.ts +1 -0
- package/src/blocks/notifications/notification-center-page.tsx +73 -0
- package/src/blocks/pricing/index.ts +7 -0
- package/src/blocks/pricing/pricing-table.tsx +173 -0
- package/src/blocks/pricing/pricing-types.ts +34 -0
- package/src/blocks/product-card/index.ts +2 -0
- package/src/blocks/product-card/product-card-types.ts +35 -0
- package/src/blocks/product-card/product-card.tsx +183 -0
- package/src/blocks/table/data-table-block.tsx +313 -0
- package/src/blocks/table/data-table-filter-menu.tsx +48 -0
- package/src/blocks/table/data-table-icon-menu-trigger.tsx +44 -0
- package/src/blocks/table/data-table-menu-content.tsx +32 -0
- package/src/blocks/table/data-table-pagination.tsx +149 -0
- package/src/blocks/table/data-table-row-actions.tsx +53 -0
- package/src/blocks/table/data-table-status.tsx +42 -0
- package/src/blocks/table/data-table-toolbar.tsx +124 -0
- package/src/blocks/table/data-table-totals.tsx +31 -0
- package/src/blocks/table/index.ts +13 -0
- package/src/blocks/workspace/index.ts +19 -0
- package/src/blocks/workspace/workspace-action-card.tsx +73 -0
- package/src/blocks/workspace/workspace-application-header.tsx +67 -0
- package/src/blocks/workspace/workspace-entity-card.tsx +48 -0
- package/src/blocks/workspace/workspace-health-summary.tsx +15 -0
- package/src/blocks/workspace/workspace-metric-card.tsx +97 -0
- package/src/blocks/workspace/workspace-page-header.tsx +43 -0
- package/src/blocks/workspace/workspace-publish-status.tsx +15 -0
- package/src/blocks/workspace/workspace-route-checklist.tsx +28 -0
- package/src/blocks/workspace/workspace-runtime-status.tsx +48 -0
- package/src/blocks/workspace/workspace-section-card.tsx +42 -0
- package/src/components/accordion.tsx +65 -0
- package/src/components/alert-dialog.tsx +160 -0
- package/src/components/alert.tsx +68 -0
- package/src/components/aspect-ratio.tsx +22 -0
- package/src/components/attachment.tsx +195 -0
- package/src/components/avatar.tsx +90 -0
- package/src/components/badge.tsx +55 -0
- package/src/components/breadcrumb.tsx +100 -0
- package/src/components/bubble.tsx +123 -0
- package/src/components/button-group.tsx +79 -0
- package/src/components/button.tsx +71 -0
- package/src/components/calendar.tsx +185 -0
- package/src/components/card.tsx +100 -0
- package/src/components/carousel.tsx +229 -0
- package/src/components/chart.tsx +338 -0
- package/src/components/checkbox.tsx +27 -0
- package/src/components/collapsible.tsx +15 -0
- package/src/components/combobox.tsx +269 -0
- package/src/components/command.tsx +181 -0
- package/src/components/compact-model-switcher.tsx +128 -0
- package/src/components/context-menu.tsx +244 -0
- package/src/components/dialog.tsx +138 -0
- package/src/components/direction.tsx +1 -0
- package/src/components/drawer.tsx +208 -0
- package/src/components/dropdown-menu.tsx +257 -0
- package/src/components/empty.tsx +93 -0
- package/src/components/field.tsx +232 -0
- package/src/components/hover-card.tsx +45 -0
- package/src/components/input-group.tsx +146 -0
- package/src/components/input-otp.tsx +83 -0
- package/src/components/input.tsx +19 -0
- package/src/components/item.tsx +188 -0
- package/src/components/kbd.tsx +26 -0
- package/src/components/label.tsx +17 -0
- package/src/components/markdown-content.tsx +113 -0
- package/src/components/marker.tsx +67 -0
- package/src/components/menubar.tsx +267 -0
- package/src/components/message-scroller.tsx +91 -0
- package/src/components/message.tsx +84 -0
- package/src/components/native-select.tsx +54 -0
- package/src/components/navigation-menu.tsx +160 -0
- package/src/components/pagination.tsx +118 -0
- package/src/components/popover.tsx +74 -0
- package/src/components/progress.tsx +82 -0
- package/src/components/questionnaire.tsx +150 -0
- package/src/components/radio-group.tsx +35 -0
- package/src/components/resizable.tsx +41 -0
- package/src/components/rich-text-editor.tsx +604 -0
- package/src/components/scroll-area.tsx +47 -0
- package/src/components/select.tsx +189 -0
- package/src/components/separator.tsx +18 -0
- package/src/components/sheet.tsx +125 -0
- package/src/components/sidebar.tsx +693 -0
- package/src/components/skeleton.tsx +13 -0
- package/src/components/slider.tsx +51 -0
- package/src/components/sonner.tsx +45 -0
- package/src/components/sparkline.tsx +231 -0
- package/src/components/spinner.tsx +20 -0
- package/src/components/status-badge.tsx +114 -0
- package/src/components/switch.tsx +31 -0
- package/src/components/table.tsx +91 -0
- package/src/components/tabs.tsx +72 -0
- package/src/components/textarea.tsx +17 -0
- package/src/components/toast.tsx +222 -0
- package/src/components/toggle-group.tsx +85 -0
- package/src/components/toggle.tsx +42 -0
- package/src/components/tooltip.tsx +53 -0
- package/src/design-system/README.md +55 -0
- package/src/design-system/action-intents.ts +21 -0
- package/src/design-system/agent-manifest.ts +83 -0
- package/src/design-system/block-registry.ts +272 -0
- package/src/design-system/component-registry.ts +142 -0
- package/src/design-system/contracts.ts +69 -0
- package/src/design-system/defaults.ts +3 -0
- package/src/design-system/index.ts +29 -0
- package/src/design-system/mcp-server.ts +156 -0
- package/src/design-system/page-registry.ts +74 -0
- package/src/design-system/selection.ts +67 -0
- package/src/design-system/template-registry.ts +33 -0
- package/src/features/interface-topology/index.ts +14 -0
- package/src/features/interface-topology/interface-topology.types.ts +40 -0
- package/src/features/interface-topology/topology-control.tsx +18 -0
- package/src/features/interface-topology/topology-inspector.tsx +228 -0
- package/src/features/interface-topology/topology-marker.tsx +47 -0
- package/src/features/interface-topology/topology-region.tsx +44 -0
- package/src/features/interface-topology/use-interface-topology.ts +178 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/hooks/use-scroll-follow.ts +22 -0
- package/src/index.ts +11 -0
- package/src/layouts/README.md +65 -0
- package/src/layouts/agent-workspace/agent-activity-rail.tsx +101 -0
- package/src/layouts/agent-workspace/agent-workspace.tsx +27 -0
- package/src/layouts/agent-workspace/agent-workspace.types.ts +28 -0
- package/src/layouts/agent-workspace/index.ts +7 -0
- package/src/layouts/blog-header/blog-header-types.ts +25 -0
- package/src/layouts/blog-header/blog-header.tsx +172 -0
- package/src/layouts/blog-header/index.ts +2 -0
- package/src/layouts/documentation-workspace/README.md +8 -0
- package/src/layouts/documentation-workspace/documentation-workspace.tsx +34 -0
- package/src/layouts/documentation-workspace/index.ts +1 -0
- package/src/layouts/ecommerce-header/ecommerce-actions.tsx +89 -0
- package/src/layouts/ecommerce-header/ecommerce-announcement.tsx +43 -0
- package/src/layouts/ecommerce-header/ecommerce-drawer.tsx +97 -0
- package/src/layouts/ecommerce-header/ecommerce-header-types.ts +59 -0
- package/src/layouts/ecommerce-header/ecommerce-header.tsx +47 -0
- package/src/layouts/ecommerce-header/ecommerce-navigation.tsx +98 -0
- package/src/layouts/ecommerce-header/ecommerce-search.tsx +86 -0
- package/src/layouts/ecommerce-header/index.ts +2 -0
- package/src/layouts/main-workspace/index.ts +28 -0
- package/src/layouts/mdi/index.ts +1 -0
- package/src/layouts/mdi/mdi.tsx +20 -0
- package/src/layouts/mdi-main/index.ts +27 -0
- package/src/layouts/mdi-main/mdi-app-catalog.generated.ts +159 -0
- package/src/layouts/mdi-main/mdi-app-catalog.ts +76 -0
- package/src/layouts/mdi-main/mdi-app-switcher.tsx +107 -0
- package/src/layouts/mdi-main/mdi-feature-settings.tsx +135 -0
- package/src/layouts/mdi-main/mdi-global-search.tsx +192 -0
- package/src/layouts/mdi-main/mdi-main.tsx +241 -0
- package/src/layouts/mdi-main/mdi-notifications-menu.tsx +197 -0
- package/src/layouts/mdi-main/mdi-profile-menu.tsx +79 -0
- package/src/layouts/mdi-main/mdi-sidebar-navigation.tsx +226 -0
- package/src/layouts/mdi-main/mdi-sidebar.tsx +110 -0
- package/src/layouts/mdi-main/mdi-status-bar.tsx +26 -0
- package/src/layouts/mdi-main/mdi-top-menu-control.ts +4 -0
- package/src/layouts/mdi-main/mdi-top-menu.tsx +117 -0
- package/src/layouts/mdi-main/mdi-topology.tsx +297 -0
- package/src/layouts/mdi-main/mdi-tweak-panel.tsx +104 -0
- package/src/layouts/mdi-main/mdi-types.ts +131 -0
- package/src/layouts/mdi-main/use-mdi-features.ts +36 -0
- package/src/layouts/mdi-main/use-mdi-sidebar-state.ts +59 -0
- package/src/layouts/site-header/announcement-bar.tsx +55 -0
- package/src/layouts/site-header/category-strip.tsx +41 -0
- package/src/layouts/site-header/index.ts +16 -0
- package/src/layouts/site-header/main-navbar.tsx +179 -0
- package/src/layouts/site-header/mobile-drawer.tsx +140 -0
- package/src/layouts/site-header/site-header-types.ts +63 -0
- package/src/layouts/site-header/site-header.tsx +58 -0
- package/src/lib/random-id.ts +19 -0
- package/src/lib/utils.ts +6 -0
- package/src/providers/pdf-provider.ts +137 -0
- package/src/styles/globals.css +374 -0
- package/src/templates/README.md +12 -0
- package/src/templates/dashboard-01/components/app-sidebar.tsx +177 -0
- package/src/templates/dashboard-01/components/chart-area-interactive.tsx +276 -0
- package/src/templates/dashboard-01/components/data-table-cell-viewer.tsx +213 -0
- package/src/templates/dashboard-01/components/data-table.tsx +577 -0
- package/src/templates/dashboard-01/components/nav-documents.tsx +79 -0
- package/src/templates/dashboard-01/components/nav-main.tsx +55 -0
- package/src/templates/dashboard-01/components/nav-secondary.tsx +39 -0
- package/src/templates/dashboard-01/components/nav-user.tsx +97 -0
- package/src/templates/dashboard-01/components/section-cards.tsx +99 -0
- package/src/templates/dashboard-01/components/site-header.tsx +14 -0
- package/src/templates/dashboard-01/data.json +614 -0
- package/src/templates/dashboard-01/index.ts +7 -0
- package/src/templates/documentation-sidebar/app-sidebar.tsx +95 -0
- package/src/templates/documentation-sidebar/index.ts +2 -0
- package/src/templates/sidebar-07/components/app-sidebar.tsx +175 -0
- package/src/templates/sidebar-07/components/nav-main.tsx +64 -0
- package/src/templates/sidebar-07/components/nav-projects.tsx +79 -0
- package/src/templates/sidebar-07/components/nav-user.tsx +105 -0
- package/src/templates/sidebar-07/components/team-switcher.tsx +94 -0
- package/src/templates/sidebar-07/index.ts +1 -0
- package/src/templates/ui-page/index.ts +7 -0
- package/src/templates/ui-page/ui-browser-frame.tsx +27 -0
- package/src/templates/ui-page/ui-template-code.tsx +48 -0
- package/src/templates/ui-page/ui-template-header.tsx +87 -0
- package/src/templates/ui-page/ui-template-navigation.tsx +60 -0
- package/src/templates/ui-page/ui-template-page.tsx +97 -0
- package/src/theme/index.ts +12 -0
- package/src/theme/theme-config.ts +30 -0
- package/src/theme/theme-provider.tsx +142 -0
- package/src/theme/theme-selector.tsx +75 -0
- package/src/tokens/index.ts +15 -0
- package/src/vite-env.d.ts +3 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
export type BlogPostAuthor = {
|
|
4
|
+
readonly avatar?: string
|
|
5
|
+
readonly bio?: string
|
|
6
|
+
readonly name: string
|
|
7
|
+
readonly role?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type BlogPostSummary = {
|
|
11
|
+
readonly author: BlogPostAuthor
|
|
12
|
+
readonly category: string
|
|
13
|
+
readonly coverImage: string
|
|
14
|
+
readonly date: string
|
|
15
|
+
readonly excerpt: string
|
|
16
|
+
readonly href: string
|
|
17
|
+
readonly id: string
|
|
18
|
+
readonly isFeatured?: boolean
|
|
19
|
+
readonly readingTime: string
|
|
20
|
+
readonly tags?: readonly string[]
|
|
21
|
+
readonly title: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type BlogPostArticle = BlogPostSummary & {
|
|
25
|
+
readonly contentHtml?: string
|
|
26
|
+
readonly relatedPosts?: readonly BlogPostSummary[]
|
|
27
|
+
readonly tableOfContents?: readonly {
|
|
28
|
+
readonly href: string
|
|
29
|
+
readonly id: string
|
|
30
|
+
readonly level: number
|
|
31
|
+
readonly title: string
|
|
32
|
+
}[]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type BlogCardProps = {
|
|
36
|
+
readonly className?: string
|
|
37
|
+
readonly onSelect?: (postId: string) => void
|
|
38
|
+
readonly post: BlogPostSummary
|
|
39
|
+
readonly variant?: 'compact' | 'horizontal' | 'standard'
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type BlogReaderProps = {
|
|
43
|
+
readonly article: BlogPostArticle
|
|
44
|
+
readonly children?: ReactNode
|
|
45
|
+
readonly className?: string
|
|
46
|
+
readonly onShare?: (platform: 'twitter' | 'linkedin' | 'copy') => void
|
|
47
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { FileImageIcon, MicIcon, PaperclipIcon, SquareIcon, XIcon } from 'lucide-react'
|
|
2
|
+
import { useRef, useState, type ChangeEvent, type ClipboardEvent, type DragEvent, type KeyboardEvent, type RefObject } from 'react'
|
|
3
|
+
|
|
4
|
+
import { Button } from '../../components/button'
|
|
5
|
+
import { Textarea } from '../../components/textarea'
|
|
6
|
+
import { cn } from '../../lib/utils'
|
|
7
|
+
|
|
8
|
+
export type ChatComposerAttachment = {
|
|
9
|
+
file: File
|
|
10
|
+
id: string
|
|
11
|
+
name: string
|
|
12
|
+
previewUrl?: string
|
|
13
|
+
type: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type ChatComposerProps = {
|
|
17
|
+
attachments?: readonly ChatComposerAttachment[]
|
|
18
|
+
className?: string
|
|
19
|
+
disabled?: boolean
|
|
20
|
+
isRecording?: boolean
|
|
21
|
+
isWorking?: boolean
|
|
22
|
+
onAddFiles?: (files: File[]) => void
|
|
23
|
+
onLongTextPaste?: (file: File) => void
|
|
24
|
+
onRemoveAttachment?: (id: string) => void
|
|
25
|
+
onSteer?: () => void
|
|
26
|
+
onStop?: () => void
|
|
27
|
+
onSubmit: () => void
|
|
28
|
+
onValueChange: (value: string) => void
|
|
29
|
+
onVoiceToggle?: () => void
|
|
30
|
+
placeholder?: string
|
|
31
|
+
queuedSteerCount?: number
|
|
32
|
+
textareaRef?: RefObject<HTMLTextAreaElement | null>
|
|
33
|
+
value: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function ChatComposer({
|
|
37
|
+
attachments = [],
|
|
38
|
+
className,
|
|
39
|
+
disabled = false,
|
|
40
|
+
isRecording = false,
|
|
41
|
+
isWorking = false,
|
|
42
|
+
onAddFiles,
|
|
43
|
+
onLongTextPaste,
|
|
44
|
+
onRemoveAttachment,
|
|
45
|
+
onSteer,
|
|
46
|
+
onStop,
|
|
47
|
+
onSubmit,
|
|
48
|
+
onValueChange,
|
|
49
|
+
onVoiceToggle,
|
|
50
|
+
placeholder = 'Share an idea, question, or draft…',
|
|
51
|
+
queuedSteerCount = 0,
|
|
52
|
+
textareaRef,
|
|
53
|
+
value,
|
|
54
|
+
}: ChatComposerProps) {
|
|
55
|
+
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
56
|
+
const [isDragging, setIsDragging] = useState(false)
|
|
57
|
+
|
|
58
|
+
function addFiles(files: FileList | null): void {
|
|
59
|
+
if (!files?.length) return
|
|
60
|
+
onAddFiles?.(Array.from(files))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function handleInputChange(event: ChangeEvent<HTMLInputElement>): void {
|
|
64
|
+
addFiles(event.target.files)
|
|
65
|
+
event.target.value = ''
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function handleDrop(event: DragEvent<HTMLDivElement>): void {
|
|
69
|
+
event.preventDefault()
|
|
70
|
+
setIsDragging(false)
|
|
71
|
+
addFiles(event.dataTransfer.files)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function handleKeyDown(event: KeyboardEvent<HTMLTextAreaElement>): void {
|
|
75
|
+
if (event.key !== 'Enter' || event.shiftKey) return
|
|
76
|
+
event.preventDefault()
|
|
77
|
+
if (isWorking) onSteer?.()
|
|
78
|
+
else onSubmit()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function handlePaste(event: ClipboardEvent<HTMLTextAreaElement>): void {
|
|
82
|
+
if (event.clipboardData.files.length) {
|
|
83
|
+
addFiles(event.clipboardData.files)
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
const text = event.clipboardData.getData('text/plain')
|
|
87
|
+
if (text.length < 12_000 || !onLongTextPaste) return
|
|
88
|
+
event.preventDefault()
|
|
89
|
+
onLongTextPaste(new File([text], `zetro-paste-${Date.now()}.txt`, { type: 'text/plain' }))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<div
|
|
94
|
+
className={cn('relative mx-auto w-4/5 rounded-md transition-colors', isDragging && 'ring-2 ring-primary/50', className)}
|
|
95
|
+
onDragEnter={() => setIsDragging(true)}
|
|
96
|
+
onDragLeave={(event) => {
|
|
97
|
+
if (event.currentTarget === event.target) setIsDragging(false)
|
|
98
|
+
}}
|
|
99
|
+
onDragOver={(event) => event.preventDefault()}
|
|
100
|
+
onDrop={handleDrop}
|
|
101
|
+
>
|
|
102
|
+
<input ref={fileInputRef} className="sr-only" multiple type="file" onChange={handleInputChange} />
|
|
103
|
+
{attachments.length ? (
|
|
104
|
+
<div className="absolute inset-x-3 top-3 z-10 flex flex-wrap gap-1.5">
|
|
105
|
+
{attachments.map((attachment) => (
|
|
106
|
+
<div key={attachment.id} className="flex h-8 max-w-48 items-center gap-1.5 rounded-sm border bg-background px-1.5 text-xs shadow-sm">
|
|
107
|
+
{attachment.previewUrl ? <img alt="" className="size-5 rounded-sm object-cover" src={attachment.previewUrl} /> : <FileImageIcon className="size-3.5 text-muted-foreground" />}
|
|
108
|
+
<span className="truncate">{attachment.name}</span>
|
|
109
|
+
<Button aria-label={`Remove ${attachment.name}`} className="size-5" size="icon-xs" variant="ghost" onClick={() => onRemoveAttachment?.(attachment.id)}><XIcon /></Button>
|
|
110
|
+
</div>
|
|
111
|
+
))}
|
|
112
|
+
</div>
|
|
113
|
+
) : null}
|
|
114
|
+
<Textarea
|
|
115
|
+
aria-label="Message"
|
|
116
|
+
className={cn(
|
|
117
|
+
'h-[7.25rem] min-h-0 max-h-[7.25rem] w-full resize-none border-foreground/20 pb-10 pr-28 leading-5 [field-sizing:fixed] [scrollbar-color:var(--border)_transparent] [scrollbar-width:thin] [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-border/70 [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar]:w-1',
|
|
118
|
+
attachments.length && 'pt-12',
|
|
119
|
+
)}
|
|
120
|
+
disabled={disabled}
|
|
121
|
+
placeholder={isDragging ? 'Drop files to attach' : placeholder}
|
|
122
|
+
rows={5}
|
|
123
|
+
ref={textareaRef}
|
|
124
|
+
value={value}
|
|
125
|
+
onChange={(event) => onValueChange(event.target.value)}
|
|
126
|
+
onKeyDown={handleKeyDown}
|
|
127
|
+
onPaste={handlePaste}
|
|
128
|
+
/>
|
|
129
|
+
<div className="absolute bottom-3 left-3 flex items-center gap-1">
|
|
130
|
+
<Button aria-label="Attach files" disabled={disabled} size="icon-xs" variant="ghost" onClick={() => fileInputRef.current?.click()}><PaperclipIcon /></Button>
|
|
131
|
+
<Button aria-label="Attach image" disabled={disabled} size="icon-xs" variant="ghost" onClick={() => fileInputRef.current?.click()}><FileImageIcon /></Button>
|
|
132
|
+
<Button aria-label={isRecording ? 'Stop voice input' : 'Start voice input'} className={isRecording ? 'bg-destructive/10 text-destructive hover:bg-destructive/15' : undefined} disabled={disabled} size="icon-xs" variant="ghost" onClick={onVoiceToggle}><MicIcon /></Button>
|
|
133
|
+
</div>
|
|
134
|
+
<div className="absolute right-3 bottom-3 flex items-center gap-1">
|
|
135
|
+
{isWorking ? <Button aria-label="Steer response" disabled={!value.trim()} size="sm" variant="ghost" onClick={onSteer}>Steer{queuedSteerCount ? ` (${queuedSteerCount})` : ''}</Button> : null}
|
|
136
|
+
<Button aria-label={isWorking ? 'Stop response' : 'Send message'} className={isWorking ? 'shimmer-surface shimmer-surface-orange' : undefined} disabled={!isWorking && !value.trim() && !attachments.length} size="icon-sm" onClick={isWorking ? onStop : onSubmit}>{isWorking ? <SquareIcon /> : <span aria-hidden="true">↑</span>}</Button>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ChatComposer, type ChatComposerAttachment, type ChatComposerProps } from './chat-composer'
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { ArchiveIcon, PencilIcon, PinIcon, PlusIcon, SearchIcon, Trash2Icon } from 'lucide-react'
|
|
2
|
+
import { useMemo, useState } from 'react'
|
|
3
|
+
|
|
4
|
+
import { Button } from '../../components/button'
|
|
5
|
+
import { Input } from '../../components/input'
|
|
6
|
+
import { cn } from '../../lib/utils'
|
|
7
|
+
|
|
8
|
+
export type ChatHistoryItem = {
|
|
9
|
+
id: string
|
|
10
|
+
pinned?: boolean
|
|
11
|
+
subtitle?: string
|
|
12
|
+
title: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type ChatHistoryProps = {
|
|
16
|
+
activeId?: string
|
|
17
|
+
className?: string
|
|
18
|
+
emptyLabel?: string
|
|
19
|
+
items: readonly ChatHistoryItem[]
|
|
20
|
+
onArchive?: (id: string) => void
|
|
21
|
+
onCreate: () => void
|
|
22
|
+
onDelete?: (id: string) => void
|
|
23
|
+
onPin?: (id: string) => void
|
|
24
|
+
onRename?: (id: string) => void
|
|
25
|
+
onSelect: (id: string) => void
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function ChatHistory({ activeId, className, emptyLabel = 'No conversations yet', items, onArchive, onCreate, onDelete, onPin, onRename, onSelect }: ChatHistoryProps) {
|
|
29
|
+
const [query, setQuery] = useState('')
|
|
30
|
+
const filteredItems = useMemo(() => items.filter((item) => item.title.toLocaleLowerCase().includes(query.toLocaleLowerCase())), [items, query])
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div className={cn('flex min-h-0 flex-1 flex-col gap-3 px-2 py-3', className)}>
|
|
34
|
+
<Button className="w-full" onClick={onCreate}><PlusIcon />New conversation</Button>
|
|
35
|
+
<div className="relative">
|
|
36
|
+
<SearchIcon aria-hidden="true" className="pointer-events-none absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2 text-muted-foreground" />
|
|
37
|
+
<Input aria-label="Search conversation history" className="pl-8" placeholder="Search history" value={query} onChange={(event) => setQuery(event.target.value)} />
|
|
38
|
+
</div>
|
|
39
|
+
<div className="min-h-0 flex-1 overflow-y-auto">
|
|
40
|
+
<div className="flex flex-col gap-1">
|
|
41
|
+
{filteredItems.map((item) => (
|
|
42
|
+
<div key={item.id} className="group/history-item relative overflow-hidden">
|
|
43
|
+
<Button className="w-full justify-start truncate" size="sm" variant={item.id === activeId ? 'secondary' : 'ghost'} onClick={() => onSelect(item.id)}>{item.pinned ? <PinIcon className="size-3" /> : null}{item.title}</Button>
|
|
44
|
+
{onRename || onPin || onArchive || onDelete ? (
|
|
45
|
+
<div className="invisible absolute top-1/2 right-0 flex -translate-y-1/2 items-center rounded-sm bg-background/95 opacity-0 transition-opacity group-hover/history-item:visible group-hover/history-item:opacity-100 group-focus-within/history-item:visible group-focus-within/history-item:opacity-100">
|
|
46
|
+
{onRename ? <Button aria-label={`Rename ${item.title}`} size="icon-xs" variant="ghost" onClick={() => onRename(item.id)}><PencilIcon /></Button> : null}
|
|
47
|
+
{onPin ? <Button aria-label={`${item.pinned ? 'Unpin' : 'Pin'} ${item.title}`} size="icon-xs" variant="ghost" onClick={() => onPin(item.id)}><PinIcon /></Button> : null}
|
|
48
|
+
{onArchive ? <Button aria-label={`Archive ${item.title}`} size="icon-xs" variant="ghost" onClick={() => onArchive(item.id)}><ArchiveIcon /></Button> : null}
|
|
49
|
+
{onDelete ? <Button aria-label={`Delete ${item.title}`} size="icon-xs" variant="ghost" onClick={() => onDelete(item.id)}><Trash2Icon /></Button> : null}
|
|
50
|
+
</div>
|
|
51
|
+
) : null}
|
|
52
|
+
</div>
|
|
53
|
+
))}
|
|
54
|
+
{!filteredItems.length ? <p className="px-2 py-4 text-center text-xs text-muted-foreground">{emptyLabel}</p> : null}
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ChatHistory, type ChatHistoryItem, type ChatHistoryProps } from './chat-history'
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Badge } from '../../components/badge'
|
|
2
|
+
import { Separator } from '../../components/separator'
|
|
3
|
+
|
|
4
|
+
export type ChatResponseProgressProps = {
|
|
5
|
+
elapsedSeconds: number
|
|
6
|
+
label?: string
|
|
7
|
+
tone?: 'default' | 'orange'
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type ChatDateDividerProps = {
|
|
11
|
+
label: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function ChatDateDivider({ label }: ChatDateDividerProps) {
|
|
15
|
+
return (
|
|
16
|
+
<div className="flex items-center gap-3 py-2">
|
|
17
|
+
<Separator className="w-auto flex-1 basis-0 border-t border-muted-foreground/35 bg-transparent" />
|
|
18
|
+
<Badge className="rounded-sm px-2 text-muted-foreground" variant="outline">{label}</Badge>
|
|
19
|
+
<Separator className="w-auto flex-1 basis-0 border-t border-muted-foreground/35 bg-transparent" />
|
|
20
|
+
</div>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function ChatResponseProgress({ elapsedSeconds, label = 'Working', tone = 'default' }: ChatResponseProgressProps) {
|
|
25
|
+
return (
|
|
26
|
+
<div aria-live="polite" className="py-0 text-left">
|
|
27
|
+
<span className={`shimmer text-xs font-medium ${tone === 'orange' ? 'shimmer-orange' : ''}`}>{label} for {elapsedSeconds}s</span>
|
|
28
|
+
</div>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function ChatTurnDivider() {
|
|
33
|
+
return <Separator className="w-full border-t border-muted-foreground/20 bg-transparent" />
|
|
34
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../components/select'
|
|
2
|
+
import { Separator } from '../../components/separator'
|
|
3
|
+
import { Button } from '../../components/button'
|
|
4
|
+
import { RefreshCwIcon } from 'lucide-react'
|
|
5
|
+
|
|
6
|
+
export type ChatRuntimeControlsProps = {
|
|
7
|
+
connected: boolean
|
|
8
|
+
message: string
|
|
9
|
+
model: string
|
|
10
|
+
models: readonly string[]
|
|
11
|
+
onModelChange: (value: string) => void
|
|
12
|
+
onProviderChange: (value: string) => void
|
|
13
|
+
onReasoningChange: (value: string) => void
|
|
14
|
+
onReconnect: () => void
|
|
15
|
+
provider: string
|
|
16
|
+
providers: readonly string[]
|
|
17
|
+
reasoning: string
|
|
18
|
+
reasoningLevels: readonly string[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function ChatRuntimeControls({
|
|
22
|
+
connected,
|
|
23
|
+
message,
|
|
24
|
+
model,
|
|
25
|
+
models,
|
|
26
|
+
onModelChange,
|
|
27
|
+
onProviderChange,
|
|
28
|
+
onReasoningChange,
|
|
29
|
+
onReconnect,
|
|
30
|
+
provider,
|
|
31
|
+
providers,
|
|
32
|
+
reasoning,
|
|
33
|
+
reasoningLevels,
|
|
34
|
+
}: ChatRuntimeControlsProps) {
|
|
35
|
+
return (
|
|
36
|
+
<div className="flex shrink-0 items-center gap-2">
|
|
37
|
+
<RuntimeSelect ariaLabel="Provider" disabled={!connected} items={providers} onValueChange={onProviderChange} value={provider} />
|
|
38
|
+
<Separator className="-mx-1 h-4 bg-muted-foreground/40" orientation="vertical" />
|
|
39
|
+
<RuntimeSelect ariaLabel="Model" disabled={!connected} items={models} onValueChange={onModelChange} value={model} />
|
|
40
|
+
<Separator className="-mx-1 h-4 bg-muted-foreground/40" orientation="vertical" />
|
|
41
|
+
<RuntimeSelect ariaLabel="Reasoning" disabled={!connected} items={reasoningLevels} onValueChange={onReasoningChange} value={reasoning} />
|
|
42
|
+
{connected ? <span aria-label="Connected to local Codex" className="size-2.5 rounded-full bg-green-500 shadow-[0_0_9px_2px_rgb(34_197_94_/_0.7)]" role="status" title={message} /> : <Button aria-label="Reconnect to local Codex" className="h-7 gap-1 rounded-sm px-2 text-xs" size="sm" variant="outline" onClick={onReconnect}><RefreshCwIcon className="size-3" />Reconnect</Button>}
|
|
43
|
+
</div>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function RuntimeSelect({ ariaLabel, disabled, items, onValueChange, value }: { ariaLabel: string; disabled: boolean; items: readonly string[]; onValueChange: (value: string) => void; value: string }) {
|
|
48
|
+
return (
|
|
49
|
+
<Select disabled={disabled} onValueChange={(nextValue) => { if (nextValue) onValueChange(nextValue) }} value={value}>
|
|
50
|
+
<SelectTrigger aria-label={ariaLabel} className="max-w-32 rounded-sm border-transparent bg-transparent px-2 hover:bg-accent" size="sm"><SelectValue /></SelectTrigger>
|
|
51
|
+
<SelectContent align="end" alignItemWithTrigger={false} className="rounded-sm" side="bottom" sideOffset={6}>
|
|
52
|
+
{items.map((item) => <SelectItem key={item} value={item}>{item}</SelectItem>)}
|
|
53
|
+
</SelectContent>
|
|
54
|
+
</Select>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
import { CircleAlertIcon, CircleCheckIcon, EyeIcon, FilePenLineIcon, LoaderCircleIcon, MessageSquareIcon, SearchIcon, TerminalIcon } from 'lucide-react'
|
|
3
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '../../components/collapsible'
|
|
4
|
+
import { cn } from '../../lib/utils'
|
|
5
|
+
|
|
6
|
+
export type ChatRuntimeTraceEvent = {
|
|
7
|
+
message: string
|
|
8
|
+
raw?: string
|
|
9
|
+
type: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type ChatRuntimeTraceProps = {
|
|
13
|
+
className?: string
|
|
14
|
+
elapsedSeconds: number
|
|
15
|
+
events: readonly ChatRuntimeTraceEvent[]
|
|
16
|
+
isWorking: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function ChatRuntimeTrace({ className, elapsedSeconds, events, isWorking }: ChatRuntimeTraceProps) {
|
|
20
|
+
const [open, setOpen] = useState(isWorking)
|
|
21
|
+
const latestEvent = events.at(-1)
|
|
22
|
+
|
|
23
|
+
useEffect(() => setOpen(isWorking), [isWorking])
|
|
24
|
+
|
|
25
|
+
if (!events.length) return null
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<Collapsible className={cn("w-full shrink-0 border-y border-border/50", className)} open={open} onOpenChange={setOpen}>
|
|
29
|
+
<CollapsibleTrigger className="group/runtime-trace flex w-full items-center gap-2 py-2 text-left text-xs text-muted-foreground hover:text-foreground">
|
|
30
|
+
<TerminalIcon className="size-3 shrink-0" />
|
|
31
|
+
<span className={isWorking ? 'shimmer shimmer-orange font-medium' : 'font-medium'}>{isWorking ? `${phaseLabel(latestEvent?.type)} · Working for ${elapsedSeconds}s` : `Completed · ${events.length} visible events`}</span>
|
|
32
|
+
<span className="ml-auto">{events.length} events</span>
|
|
33
|
+
</CollapsibleTrigger>
|
|
34
|
+
<CollapsibleContent className="pb-2">
|
|
35
|
+
<div className="max-h-44 space-y-1 overflow-y-auto pr-1 [scrollbar-color:var(--border)_transparent] [scrollbar-width:thin] [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-border/70 [&::-webkit-scrollbar]:w-1">
|
|
36
|
+
{events.map((event, index) => <TraceEvent key={`${event.raw ?? event.message}-${index}`} event={event} />)}
|
|
37
|
+
</div>
|
|
38
|
+
</CollapsibleContent>
|
|
39
|
+
</Collapsible>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function phaseLabel(type?: string): string {
|
|
44
|
+
if (type === 'command') return 'Running read-only command'
|
|
45
|
+
if (type === 'request') return 'Checking sources'
|
|
46
|
+
if (type === 'review') return 'Reviewing context'
|
|
47
|
+
if (type === 'change') return 'Recording file activity'
|
|
48
|
+
return 'Compacting auto'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function TraceEvent({ event }: { event: ChatRuntimeTraceEvent }) {
|
|
52
|
+
const [open, setOpen] = useState(false)
|
|
53
|
+
const content = event.raw ?? event.message
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<Collapsible open={open} onOpenChange={setOpen}>
|
|
57
|
+
<CollapsibleTrigger className="group/runtime-event flex w-full items-center gap-2 rounded-sm px-1 py-1 text-left text-xs text-muted-foreground hover:bg-muted/50 hover:text-foreground">
|
|
58
|
+
<EventIcon type={event.type} />
|
|
59
|
+
<span className="truncate">{event.message}</span>
|
|
60
|
+
</CollapsibleTrigger>
|
|
61
|
+
<CollapsibleContent className="px-1 pb-1">
|
|
62
|
+
<pre className="max-h-32 overflow-auto rounded-sm bg-muted/50 p-2 font-mono text-[11px] leading-4 text-foreground [scrollbar-color:var(--border)_transparent] [scrollbar-width:thin] [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-border/70 [&::-webkit-scrollbar]:h-1 [&::-webkit-scrollbar]:w-1">{content}</pre>
|
|
63
|
+
</CollapsibleContent>
|
|
64
|
+
</Collapsible>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function EventIcon({ type }: { type: string }) {
|
|
69
|
+
const Icon = type === 'command' ? TerminalIcon : type === 'response' ? MessageSquareIcon : type === 'request' ? SearchIcon : type === 'review' ? EyeIcon : type === 'change' ? FilePenLineIcon : type === 'complete' ? CircleCheckIcon : type === 'error' ? CircleAlertIcon : LoaderCircleIcon
|
|
70
|
+
return <Icon aria-label={type} className={cn('size-3 shrink-0', type === 'processing' && 'animate-spin')} />
|
|
71
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type ReactNode, useState } from "react";
|
|
2
|
+
import { CopyIcon, ExternalLinkIcon, KeyRoundIcon, LaptopIcon, RefreshCwIcon, ShieldCheckIcon } from "lucide-react";
|
|
3
|
+
import { Alert, AlertDescription, AlertTitle } from "@devxcrew/react-ui/components/alert";
|
|
4
|
+
import { Button } from "@devxcrew/react-ui/components/button";
|
|
5
|
+
import { Sheet, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle } from "@devxcrew/react-ui/components/sheet";
|
|
6
|
+
import { Switch } from "@devxcrew/react-ui/components/switch";
|
|
7
|
+
|
|
8
|
+
export type CodexDeviceCode = { message: string; status: "idle" | "awaiting" | "connected" | "failed"; userCode?: string; verificationUrl?: string };
|
|
9
|
+
export type CodexConnectionSettingsProps = { connected: boolean; deviceCode: CodexDeviceCode; message: string; onConnectLocal: () => void; onCopyCode: () => void; onCopyUrl: () => void; onGenerateDeviceCode: () => void; onOpenBrowser: () => void; onOpenChange: (open: boolean) => void; open: boolean };
|
|
10
|
+
|
|
11
|
+
export function CodexConnectionSettings({ connected, deviceCode, message, onConnectLocal, onCopyCode, onCopyUrl, onGenerateDeviceCode, onOpenBrowser, onOpenChange, open }: CodexConnectionSettingsProps) {
|
|
12
|
+
const [useDeviceCode, setUseDeviceCode] = useState(false);
|
|
13
|
+
const [checkingLocal, setCheckingLocal] = useState(false);
|
|
14
|
+
const awaitingCode = deviceCode.status === "awaiting";
|
|
15
|
+
|
|
16
|
+
async function checkLocalCodex(): Promise<void> {
|
|
17
|
+
setCheckingLocal(true);
|
|
18
|
+
try {
|
|
19
|
+
await onConnectLocal();
|
|
20
|
+
} finally {
|
|
21
|
+
setCheckingLocal(false);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return <Sheet open={open} onOpenChange={onOpenChange}><SheetContent className="gap-0 p-0 sm:max-w-md" side="right">
|
|
26
|
+
<SheetHeader className="border-b pr-12"><SheetTitle>Codex connection</SheetTitle><SheetDescription>Choose how this Zetro workspace connects to Codex.</SheetDescription></SheetHeader>
|
|
27
|
+
<div className="flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto p-4">
|
|
28
|
+
<section className="flex items-center gap-3" aria-live="polite"><span className={connected ? "size-2 shrink-0 rounded-full bg-emerald-500 shadow-[0_0_0_4px_rgb(16_185_129_/_0.13),0_0_12px_rgb(16_185_129_/_0.8)]" : "size-2 shrink-0 rounded-full bg-muted-foreground/50"} /><div className="min-w-0"><p className="text-sm font-medium">{connected ? "Connected" : "Not connected"}</p><p className="mt-0.5 text-sm text-muted-foreground">{message}</p></div></section>
|
|
29
|
+
<ConnectionMethod title="Local Codex" description="Use the Codex CLI already installed on this computer." icon={<LaptopIcon className="size-4" />}><Button disabled={checkingLocal} variant="outline" onClick={checkLocalCodex}><RefreshCwIcon className={checkingLocal ? "animate-spin" : undefined} /> {checkingLocal ? "Checking local Codex" : "Check local Codex"}</Button></ConnectionMethod>
|
|
30
|
+
<ConnectionMethod title="Device code" description="Sign in using a short-lived code in your browser." icon={<KeyRoundIcon className="size-4" />} trailing={<Switch aria-label="Use device-code connection" checked={useDeviceCode} onCheckedChange={setUseDeviceCode} />}>
|
|
31
|
+
<Button disabled={!useDeviceCode} onClick={onGenerateDeviceCode}><KeyRoundIcon /> Generate device code</Button>
|
|
32
|
+
</ConnectionMethod>
|
|
33
|
+
{useDeviceCode && awaitingCode ? <div className="flex flex-col gap-3 rounded-md border border-border/70 p-3"><div className="flex items-center gap-2"><p className="flex-1 text-sm font-medium">One-time device code</p><Button aria-label="Copy device code" size="icon-xs" variant="ghost" onClick={onCopyCode}><CopyIcon /></Button></div><code className="w-fit rounded-sm border bg-muted px-3 py-2 text-base font-semibold tracking-[0.12em]">{deviceCode.userCode}</code><p className="text-sm text-muted-foreground">{deviceCode.message}</p><div className="flex flex-wrap gap-2"><Button variant="outline" onClick={onOpenBrowser}><ExternalLinkIcon /> Open browser</Button><Button variant="outline" onClick={onCopyUrl}><CopyIcon /> Copy URL</Button></div><p className="text-sm leading-6 text-muted-foreground">This code is held only in running memory and is not added to Zetro history, storage, or logs.</p></div> : null}
|
|
34
|
+
{deviceCode.status === "failed" ? <Alert variant="destructive"><ShieldCheckIcon /><AlertTitle>Device code unavailable</AlertTitle><AlertDescription>{deviceCode.message}</AlertDescription></Alert> : null}
|
|
35
|
+
</div>
|
|
36
|
+
<SheetFooter className="border-t"><Button className="w-full" disabled={checkingLocal} variant="outline" onClick={checkLocalCodex}><RefreshCwIcon className={checkingLocal ? "animate-spin" : undefined} /> {checkingLocal ? "Checking local Codex" : "Recheck local Codex"}</Button></SheetFooter>
|
|
37
|
+
</SheetContent></Sheet>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function ConnectionMethod({ children, description, icon, title, trailing }: { children: ReactNode; description: string; icon: ReactNode; title: string; trailing?: ReactNode }) {
|
|
41
|
+
return <section className="flex flex-col gap-3 rounded-md border border-border/70 p-3"><div className="flex items-start gap-2"><span className="mt-0.5 text-muted-foreground">{icon}</span><div className="min-w-0 flex-1"><p className="text-sm font-medium">{title}</p><p className="mt-0.5 text-sm leading-5 text-muted-foreground">{description}</p></div>{trailing}</div>{children}</section>;
|
|
42
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { CodexConnectionSettings, type CodexConnectionSettingsProps, type CodexDeviceCode } from "./codex-connection-settings";
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { AlertCircle, CheckCircle2, File, RefreshCw, X } from 'lucide-react'
|
|
2
|
+
import { cn } from '../../lib/utils'
|
|
3
|
+
import type { DropzoneFile } from './dropzone-types'
|
|
4
|
+
|
|
5
|
+
export function formatBytes(bytes: number): string {
|
|
6
|
+
if (bytes === 0) return '0 B'
|
|
7
|
+
const k = 1024
|
|
8
|
+
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
|
9
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
10
|
+
return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function DropzoneFileItem({
|
|
14
|
+
file,
|
|
15
|
+
onRemove,
|
|
16
|
+
onRetry,
|
|
17
|
+
}: {
|
|
18
|
+
file: DropzoneFile
|
|
19
|
+
onRemove?: (id: string) => void
|
|
20
|
+
onRetry?: (id: string) => void
|
|
21
|
+
}) {
|
|
22
|
+
const isUploading = file.status === 'uploading'
|
|
23
|
+
const isComplete = file.status === 'complete'
|
|
24
|
+
const isError = file.status === 'error'
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div
|
|
28
|
+
className={cn(
|
|
29
|
+
'group flex flex-col gap-1.5 rounded-xl border border-border/80 bg-card p-3 shadow-2xs transition-colors',
|
|
30
|
+
isError && 'border-destructive/40 bg-destructive/5',
|
|
31
|
+
isComplete && 'border-emerald-500/30 bg-emerald-500/5',
|
|
32
|
+
)}
|
|
33
|
+
>
|
|
34
|
+
<div className="flex items-center justify-between gap-3">
|
|
35
|
+
<div className="flex min-w-0 items-center gap-2.5">
|
|
36
|
+
{file.previewUrl ? (
|
|
37
|
+
<img
|
|
38
|
+
alt={file.name}
|
|
39
|
+
className="size-8 rounded-lg object-cover ring-1 ring-border"
|
|
40
|
+
src={file.previewUrl}
|
|
41
|
+
/>
|
|
42
|
+
) : (
|
|
43
|
+
<div className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground">
|
|
44
|
+
<File className="size-4" />
|
|
45
|
+
</div>
|
|
46
|
+
)}
|
|
47
|
+
|
|
48
|
+
<div className="min-w-0">
|
|
49
|
+
<p className="truncate text-xs font-medium text-foreground">{file.name}</p>
|
|
50
|
+
<p className="text-[11px] text-muted-foreground">{formatBytes(file.sizeBytes)}</p>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<div className="flex items-center gap-1">
|
|
55
|
+
{isComplete && (
|
|
56
|
+
<span title="Upload complete">
|
|
57
|
+
<CheckCircle2 className="size-4 text-emerald-500" />
|
|
58
|
+
</span>
|
|
59
|
+
)}
|
|
60
|
+
|
|
61
|
+
{isError && (
|
|
62
|
+
<>
|
|
63
|
+
<span title="Upload failed">
|
|
64
|
+
<AlertCircle className="size-4 text-destructive" />
|
|
65
|
+
</span>
|
|
66
|
+
{onRetry && (
|
|
67
|
+
<button
|
|
68
|
+
aria-label="Retry upload"
|
|
69
|
+
className="rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
70
|
+
type="button"
|
|
71
|
+
onClick={() => onRetry(file.id)}
|
|
72
|
+
>
|
|
73
|
+
<RefreshCw className="size-3.5" />
|
|
74
|
+
</button>
|
|
75
|
+
)}
|
|
76
|
+
</>
|
|
77
|
+
)}
|
|
78
|
+
|
|
79
|
+
{onRemove && (
|
|
80
|
+
<button
|
|
81
|
+
aria-label={`Remove ${file.name}`}
|
|
82
|
+
className="rounded p-1 text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive"
|
|
83
|
+
type="button"
|
|
84
|
+
onClick={() => onRemove(file.id)}
|
|
85
|
+
>
|
|
86
|
+
<X className="size-3.5" />
|
|
87
|
+
</button>
|
|
88
|
+
)}
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
{isUploading && (
|
|
93
|
+
<div
|
|
94
|
+
aria-label={`Uploading ${file.name}`}
|
|
95
|
+
aria-valuemax={100}
|
|
96
|
+
aria-valuemin={0}
|
|
97
|
+
aria-valuenow={file.progress ?? 0}
|
|
98
|
+
className="relative h-1.5 w-full overflow-hidden rounded-full bg-muted"
|
|
99
|
+
role="progressbar"
|
|
100
|
+
>
|
|
101
|
+
<div
|
|
102
|
+
className="h-full rounded-full bg-primary transition-all duration-300"
|
|
103
|
+
style={{ width: `${Math.min(100, Math.max(0, file.progress ?? 0))}%` }}
|
|
104
|
+
/>
|
|
105
|
+
</div>
|
|
106
|
+
)}
|
|
107
|
+
|
|
108
|
+
{isError && file.errorMessage && (
|
|
109
|
+
<p className="text-[11px] font-medium text-destructive">{file.errorMessage}</p>
|
|
110
|
+
)}
|
|
111
|
+
</div>
|
|
112
|
+
)
|
|
113
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type DropzoneFileStatus = 'idle' | 'uploading' | 'complete' | 'error'
|
|
2
|
+
|
|
3
|
+
export interface DropzoneFile {
|
|
4
|
+
errorMessage?: string
|
|
5
|
+
file?: File
|
|
6
|
+
id: string
|
|
7
|
+
name: string
|
|
8
|
+
previewUrl?: string
|
|
9
|
+
progress?: number
|
|
10
|
+
sizeBytes: number
|
|
11
|
+
status: DropzoneFileStatus
|
|
12
|
+
type: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface DropzoneProps {
|
|
16
|
+
accept?: readonly string[]
|
|
17
|
+
allowMultiple?: boolean
|
|
18
|
+
ariaLabel?: string
|
|
19
|
+
className?: string
|
|
20
|
+
disabled?: boolean
|
|
21
|
+
emptyMessage?: string
|
|
22
|
+
files?: readonly DropzoneFile[]
|
|
23
|
+
hint?: string
|
|
24
|
+
maxFiles?: number
|
|
25
|
+
maxSizeBytes?: number
|
|
26
|
+
onClearFiles?: () => void
|
|
27
|
+
onFilesDrop?: (acceptedFiles: File[], rejectedFiles: { error: string; file: File }[]) => void
|
|
28
|
+
onRemoveFile?: (fileId: string) => void
|
|
29
|
+
onRetryFile?: (fileId: string) => void
|
|
30
|
+
showFileList?: boolean
|
|
31
|
+
title?: string
|
|
32
|
+
}
|